善始善终

10 篇文章 0 订阅

互斥锁的加锁解锁,动态内存的申请释放,都较难驾驭且容易出错。所幸我在Z和H公司所写的代码都有意或无意的避免了这两个问题。当时所涉及的是基于VxWorks的嵌入式编程,在进程调度方面系统控制层做了限制,每个进程处理完自己的消息任务后才会调度另一个进程运行,因此避免了进程并发的情况,也就无需对临界资源进行加锁。另一方面,嵌入式系统对实时性要求很高,系统中所需要的内存都在初始化时全部申请完成,并且一直不释放直到系统关机。我们称这样的内存为静态内存,因此也就绕开了动态申请和释放内存的情况。

但在应用系统中多线程编程不可避免,系统并发时需要对临界区访问进行控制。比如对一个公共列表中元素的插入和删除,如果不进行锁控制,当多线程并发对其操作时,可能造成对列表的破坏,最终导致程序异常。锁临界区时要把握以下几点:

  • 最小化原则

临界区的代码应该是最小化的。从加锁到解锁的这段代码,应该只包括必须的操作。任何多余的操作都要剔除到临界区外。

锁的粒度要准确,在需要针对某个对象加锁的地方,就不要使用针对类的锁(会锁住类的所有对象),这样会把更多的线程锁在外面,影响效率。

  • 所有异常分支都要解锁

加锁和解锁的操作要放在同一个函数里,这样易于维护。同时在每个异常分支return之前都需要解锁。当函数异常分支较多时,只要某个分支忘了解锁的操作,就会出现死锁,这是一个容易出错的地方。对于Java代码可以使用finally分支,因为函数在return之前,不管什么场景都会进入finally分支。

void  java_function ()

       lock.lock();

       try {

              //流程处理

       } catch (Exception e) {

              //异常处理

       }

       finally{

              lock.unlock(); ß在finally分支解锁,不会遗漏

       }

}

如果是C/C++,建议使用goto到函数末尾统一解锁,不要在中途return:

void  c_cplusplus_function ()

{

       lock(&mylock);

       err = get_some_thing();

       if (err) {

// spin_unlock(&mylock);

              // return;

              goto finish; ß不要return,goto到最后做解锁操作

       }



finish:

       spin_unlock(&mylock);

       return;

}

如果上述代码在每个异常分支都return的话,需要在每个分支都进行解锁,这样容易遗漏。

如果是C++程序,还有一种更简单的办法,只需要一行代码进行加锁,而且不需要解锁操作:

void  CPlusPlusFunction ()

{

       DoLock(mLiskLock);

       //进行程序处理,无需调用解锁函数

       return;

}

该方法的实现原理如下:第一行代码DoLock(mLiskLock)实际上构造了一个临时的锁管理对象,在该对象的构造函数中调用了mLiskLock的Enter函数进行加锁,同时在析构函数中调用mLiskLock的Leave函数进行解锁。当CPlusPlusFunction运行完成时,该临时对象会被销毁,其析构函数会被调用,从而程序保证了对解锁函数的调用。在示例程序LockDemo中给出了使用示例,你可以直接使用其中的lock.h,其代码如下:

#pragma once

#include <atlsync.h>



class CCriticalSectionLock

{

public:

       explicit CCriticalSectionLock(CCriticalSection &cs)

       {

              m_pCS=&cs;

              m_pCS->Enter();

       }

       ~CCriticalSectionLock()

       {

              m_pCS->Leave();

       }



protected:

       CCriticalSection* m_pCS;

};



#define DoLock(x)  CCriticalSectionLock x##_lock(x)  //构造临时锁管理对象
  • 避免强制结束线程

在我们的一个Windows应用系统中,有些锁是在线程中进行加锁的。由于某些业务场景的需要,在程序运行中会在上层直接kill这些线程(使用Windows API TerminateThread结束线程)。而如果在结束线程时,还没有进行相应的解锁操作,就会导致这些锁永远无法释放。当新的线程进行加锁时,就会永远被阻塞。改进的办法是,我们放弃了使用TerminateThread直接结束线程,而是通过退出标记让线程自己结束。因此在写涉及锁的程序时一定要留意系统及编程语言的特性,避免类似问题。

动态内存的申请和释放同样需要注意一下几个原则:

  • 谁申请谁释放

这里所说的“谁”是指模块。比如A模块申请了一片内存,就得A模块来释放。这样逻辑清晰便于管理。以前还碰到过类似的代码:

class CMyClass
{
public:
	~CMyClass()
	{
		delete this; 自己删除自己
	}
};

在析构函数中调用了delete this,相当于自己删除自己。首先任何对象都是由第三方模块创建的,肯定不会自己创建自己。这里的自己删除自己违反了谁申请谁释放的原则,同时非常难于维护。

  • 申请和释放相匹配

熟读API规范,内存的申请和释放函数需要相互匹配。比如C++里,new申请的内存用delete释放,malloc申请的内存用free释放。new []申请的数组要用delete []释放,否则同样会导致内存泄露[Scott Meyers]。

  • 留意GDI资源

Windows程序设计中GDI资源和内存申请一样,如果没有正确的释放GDI资源,同样会造成GDI资源泄露,严重时会影响整个操作系统。比如BeginPaint和EndPaint要配对使用:

hdc = BeginPaint (hwnd, &ps) ;
    [use GDI functions]
EndPaint (hwnd, &ps) ;

GetDC和ReleaseDC配对使用:

hdc = GetDC (hwnd) ;
    [use GDI functions]
ReleaseDC (hwnd, hdc) ;

CreateDC和DeleteDC配对使用:

hdcMem = CreateCompatibleDC (hdcEMF) ;
    [use GDI functions]
DeleteDC (hdcMem) ;

这类问题容易被忽视且难以发现。可以在Windows的任务管理器中把GDI对象勾上,这样可以在进程运行的过程中查看GDI对象的使用数量。如果该数量逐渐变大且没有变小的迹象,则很可能存在GDI资源泄露。此时你需要对代码进行排查了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值