互斥锁(mutex)是实现线程或进程同步与互斥的一种通信机制。它在不同平台下具有不同的实现形式。我们先介绍下linux和windows下的互斥锁的实现。Linux下的互斥锁包括一下几个实现函数:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t * restrict attr );或者静态的声明为PTHREAD_MUTEX_INITIALIZER;
int pthread_mutex_lock(pthread_mutex_t *restrict mutex);
int pthread_mutex_unlock(pthread_mutex_t *restrict mutex);
int pthread_mutex_destroy(pthread_mutex_t *restrict mutex);
上面四个函数分别是互斥锁的初始化,上锁,解锁,销毁的函数原型。其中pthread_mutex_t 是Linux下定义的互斥锁的类型; pthread_mutexattr_t是
互斥锁属性的类型,可以通过相关的函数设置互斥锁的属性。
Windows下的互斥对象的实现函数:
HANDLE CreateMutex (LPSECURITY_ATTRIBUTES lpMutexAttributes, bool bInitialOwner, LPCTSTR lpname);
这个函数是创建一个互斥体,该API返回一个句柄,如果失败则返回0。LPSECURITY_ATTRIBUTES是互斥对象属性的类型; bool类型的bInitialOwner
如果为true表示进程立刻获得该互斥对象使用权限,反之亦然;LPCTSTR指定互斥对象的名字,如果该名字是未创建的则创建,否则打开该名字的互
斥对象。
HANDLE WINAPI OpenMutex(
_In_ DWORD dwDesiredAccess,
_In_ BOOL bInheritHandle,
_In_ LPCTSTR lpName
);
其中, DWORD dwDesiredAccess 可以是以下三个常数之一:
MUTEX_ALL_ACCESS(请求对互斥体的完全访问);MUTEX_MODIFY_STATE(允许使用ReleaseMutex函数);SYNCHRONIZE(允许互斥体对象同步使用);
BOOL bInheritHandle:若为ture则允许子进程继承
LPCTSTR lpname:互斥对象的名字。
CreateMutex()和OpenMutex()都是返回一个互斥对象的句柄,那么为什么还需要OpenMutex()呢?因为利用OpenMutex()可以为不同子进程产生各自的对应同一互斥对象的句柄,方便操作。
OpenMutex()函数调用成功返回一个句柄,如果失败则返回0,并且可以调用函数GetLastError()获得OpenMutex()返回的错误信息。需要注意的是如
果对象的句柄不在有作用一定的要调用CloseHandle()取消该句柄,若互斥对象的所有句柄都被销毁,那么对象也就被destroy。为什么样这样啊?因
为Windows下没有提供类型Linux的pthread_mutex_destroy()的函数。
下面继续介绍Windows下的类似Linux中的上锁和解锁的函数:
DWORD WINAPI WaitForSingleObject(
_In_ HANDLE hHandle,
_In_ DWORD dwMilliseconds
);
WaitForSingleObject()类似Linux中的上锁函数,它集成了Linux中的Lock和try_lock两个函数的功能。
BOOL WINAPI ReleaseMutex(
_In_ HANDLE hMutex
);
ReleaseMutex()等同于Linux中的解锁函数。
以上是Linux和Windows中的互斥锁和互斥对象的部分操作函数,当然还有很大一部分这里没有提到的。下面我就着重介绍下嵌套锁和读写锁。在介绍嵌
套锁之前,我们先了解下多平台下使用互斥锁的情况。因为Linux和Windows下的互斥锁的实现是不同的,我们编写的程序就只局限于某种平台的使用
了。那么如和解决类似的问题呢?最直接的办法是提供一个可以适用于Linux和Windows互斥锁的接口。QT就是这样的典范,它糅合了多种平台的实现函
数,为我们提供了同一的接口,方便我们实现多平台的编程,实现了DRY(don't repeat yourself)。那么我们就尝试着实现一个适用于Linux和
Windows的互斥锁的接口。
struct _Locker;
typedef struct _Locker stLocker;
typedef struct _Locker * pLocker;
typedef Ret (*LockerLockFunc)(pLocker thiz);//Ret 是自定义的返回变量,可以用一个enum实现
typedef Ret (*LockerUnlockFunc)(pLocker thiz);
typedef Ret (*LockerDestroyFunc)(pLocker thiz);
typedef Ret (*LockerOpenFunc)(pLocker thiz);
struct _Locker
{
LockerLockFunc lock;
LockerUnlockFunc unlock;
LockerDestroyFunc destroy;
LockerOpenFunc open;
char priv[0];
};
这里是用c语言实现的接口,当然也可以用c++中的类实现接口,可以参考一些c++的书籍有关接口继承和实现继承的部分。上面代码中的结构体中的
priv成员变量是一个柔性数组,它的大小是可变的;其中的lock,unlock,destroy以及open是_Locker的接口函数指针,可以用不同平台的WPI实现。
下面以Linux为例,
#define return_val_if_fail(p,ret) if(!(p))\
{printf("%s : %d , Warning : "#p" failed\n",\
__func__,__LINE__);return ret;}
typedef struct _PrivInfo
{
pthread_mutex_t mutex;
}stPrivInfo;
static Ret locker_pthread_lock(pLocker thiz)
{
return_val_if_fail(thiz != NULL, RET_INVALID_PARAMS);
stPrivInfo * priv = (stPrivInfo*)thiz->priv;
int ret = pthread_mutex_lock(&priv->mutex);
return ret == 0? RET_OK:RET_FAIL;
}
Locker * locker_pthread_create()
{
int ret = 0;
pLocker thiz = (pLocker)malloc(sizeof(stLocker) + sizeof(stPrivInfo));
if(thiz != NULL)
{
stPrivInfo * priv = (stPrivInfo*)thiz->priv;//请注意这里的用法
thiz->lock = locker_pthread_lock;
...
ret = pthread_mutex_init(&(priv->mutex), NULL);
if(ret != 0)
{
free(thiz);
thiz = NULL;
}
}
return thiz;
}
这里指给出了lock函数的实现方法,其他的接口函数的实现和这个的方法一致。我们在针对Windows下的互斥对象实现该接口的各个函数,我们就可以
通过调用不同的创建函数,利用同样的接口函数实现不同平台的互斥锁或互斥对象的使用。
在互斥锁接口的基础上我们再引入嵌套锁的概念。那什么是嵌套锁呢?首先假设我们有多个线程对链表进行操作,但每个线程对该链表也有多次操作。
这里有两种方法实现这种操作:1)每个线程操作链表时加锁,解锁,也就是加锁,解锁由调用者实现;2)我们也可以写一个具有锁的功能的链表,调
用每个链表的操作函数他们都有自带的锁功能。第一种方法容易导致调用者在加锁和解锁时出现错误。而第二种方法是不错的,但也有一个误区,比如
为每个函数都进行加锁解锁,那么如果一个线程中有多个链表函数的操作,那么它就包含多个加锁和解锁操作,也就是多个原子操作,这样在并发中是
危险的。因为在线程1获得锁对该链表进行append操作后解锁,然后想在对该链表做length操作,由于其不再是原子操作,那么有可能另一个线程2获得
了互斥锁,等到线程1在对该链表的length操作获得锁时,得到的结果可能就不是预期的结果了。
而如果方法2使用的是嵌套锁就可以有效的解决类似的问题了。我们先了解下嵌套锁的加锁和解锁方法:
加锁:
1)如果没有任何线程加锁,就直接加锁,并且记录下当前线程的ID;
2)如果当前线程加过锁了,就不用加锁了,只是将加锁的技术增加一;
3)如果其他线程加锁了,那么就等待直到加锁成功,后继步骤与第一种情况相同。
解锁:
1)如果不是当前线程加的锁或者没有人加锁,那这是错误的调用,直接返回。
2)如果是当前线程加锁了,将加锁的计数减1.如果计数仍大于0,说明当前线程加了多个锁,直接返回就行了。如果计数为0,说明当前线程只加了一
次锁,则执行解锁操作。
这里我们采用装饰模式来实现嵌套锁:装饰模式的作用是在不改变对象本质(接口)的前提下,给对象添加附加功能。和继承不同,它不是针对整个类
的,而是针对某个对象的。下面我们就嵌套锁的例子实现装饰模式
typedef int(*TashSelfFunc)();
Locker * locker_nest_create(pLocker real_locker, TaskSelfFunc task_self);
typedef struct _PrivInfo
{
int owner;
int refcount;
pLocker* real_locker;
TaskSelfFunc task_self;
}stPrivInfo;
/*lock的实现*/
static Ret locker_nest_locker(Locker *thiz)
{
Ret ret = RET_OK;
stPrivInfo* priv = (stPrivInfo *)thiz->priv;
if(priv->owner == priv->task_self())
{
priv->refcount++;
}
else
{
if((ret = locker_lock(priv->real_locker)) == RET_OK)//locker_lock 是封装后的pthread_mutex_lock函数,这里是加锁的操作
{
priv->refcount = 1;
priv->owner = priv->task_self();
}
}
return ret;
}
static Ret locker_nest_unlock(pLocker thiz)
{
Ret ret = RET_OK;
stPrivInfo * priv = (stPrivInfo*)thiz->priv;
return_val_if_fail(priv->owner == priv->task_self(), RET_FAIL);
priv->refcount --;
if(priv->refcount == 0)
{
priv->owner = 0;
ret = locker_unlock(priv->real_locker);
}
return ret;
}
装饰模式产生的嵌套锁与普通锁相比较,他们的接口函数是一样的,只不过他们的创建函数是不同的。嵌套锁需要传入一个普通锁的指针和一个返回当
前线程ID的函数指针,它返回的也是一个stLocker结构体的指针,不过这个指针指向的内容与传入是的stLocker指针指向的内容是不同的,前者指向嵌
套锁,后者指向普通锁。从产生的结果上就像是对原来的普通锁的指针指向的内容进行了装饰,增加了引用计数等成员变量,产生了一个具有嵌套锁性
质的stLocker接口。
下面我们介绍什么是读写锁?我们会遇到这样的数据结构:它允许写和读,而且他支持多个人同时读,因为在读某个数据结构的时候,不会改变该数据
结构的状态因此不用对其加锁。就像写博客一样:在写博客的时候不允许读者读该博文,当读者读博文的时候不允许写/更改博文,但是可以允许其他
的读者读该博文。这里就不能用普通锁来实现了。因为利用普通锁确实可以实现单个写与单个读的互斥,但是当遇到单个写与多个读的情况,我们怎么
确定给读加锁呢?大师们给这种问题的解决方法就是读写锁:即用两个锁(A,B)来实现写锁和读锁
写锁:
和普通的锁一样,实现写和读的互斥。可以理解为A锁
读锁:
读锁需要维护一个读的引用计数,即目前有多少个读的线程。它的实现流程是:先加A锁,然后加B锁,维护引用计数,解B锁,如果引用计数为0,则解A锁,否则不解A锁。
这里我们还是继续利用上面的Locker接口实现A,B锁。以下是代码,
typedef enum MODE{RW_LOCKER_NONE,RW_LOCKER_WRITER, RW_LOCKER_READER} Mode;
struct _RWLocker
{
size_t readers;
Mode mode;
stLocker * rw_locker;//A锁
stLocker * rd_locker;//B锁
};
typedef _RWLocker * pRWLocker;
typedef _RWLocker stRWLocker;
/*创建读写锁*/
pRWLocker rw_locker_create(pLocker rw_locker, pLocker rd_locker)
{
pRWLocker thiz = NULL;
return_val_if_fail(rw_locker != NULL && rd_locker != NULL, RET_INVALID_PARAMS);
thiz = (stRWLocker *)malloc(sizeof(stRWLocker));
if(thiz != NULL)
{
thiz->readers = 0;
thiz->mode = RW_LOCKER_NONE;
thiz->rw_locker = rw_locker;
thiz->rd_locker = rd_locker;
}
return thiz;
}
/*加读锁*/
Ret rw_locker_rdlock(pRWLocker thiz)
{
return_val_if_fail(thiz != NULL, RET_INVALID_PARAMS);
Ret ret = RET_OK;
if((ret = locker_lock(thiz->rd_locker)) == RET_OK)
{
thiz->readers ++;
/*如果是第一个读者,那么加A锁*/
if(thiz->readers == 1)
{
ret = locker_lock(thiz->rw_locker);
thiz->mode = RW_LOCKER_READER;
}
locker_unlock(thiz->rd_locker); // B锁解锁,不解A锁
}
return ret;
}
/*加写锁*/
Ret rw_locker_rwlocker(pRWLocker thiz)
{
return_val_if_fail(thiz != NULL, RET_INVALID_PARAMS);
Ret ret = RET_OK;
if((ret = locker_lock(thiz->rw_locker)) == RET_OK)
{
thiz->mode = RW_LOCKER_WRITER;
}
return ret;
}
/*解锁*/
Ret rw_locker_unlock(pWRLocker thiz)
{
return_val_if_fail(thiz != NULL, RET_INVALID_PARAMS);
Ret ret = RET_OK;
if(thiz->mode == RW_LOCKER_WRITER)//根据mode解锁
{
thiz->mode = RW_LOCKER_NONE;
ret = locker_unlock(thiz->rw_locker);
}
else
{
assert(thiz->mode == RW_LOCKER_READER);
if((ret = locker_lock(thiz->rd_locker)) == RET_OK)
{
thiz->readers --;
if(thiz -> readers == 0)
{
thiz->mode = RW_LOCKER_NONE;
ret = locker_unlock(thiz->rw_locker);
}
locker_unlock(thiz->rd_locker);
}
}
return ret;
}
读写锁中比较麻烦点的实现是:读锁的实现和解锁的实现。解锁首先根据mode判断是读锁还是写锁,如果是写锁则直接解写锁;如果是读锁则访问
readers并且减一,若此时readers等于0,即没有读者了,那么就解写锁。