对比及区别
| 速度、资源开销 | 跨进程 | 资源统计 | 安全性 |
Critical Section | 速度快,非内核对象 | 不能用于不同进程
| 不能进行资源统计(每次只可以有一个线程对共享资源进行存取) |
|
Mutex | 速度慢,内核对象 | 可用于不同进程 | 不能进行资源统计 |
|
Semaphore | 速度慢,内核对象 | 可用于不同进程 | 可进行资源统计(可以让一个或超过一个线程对共享资源进行存取) |
|
Event | 速度慢,内核对象 | 可用于不同进程 | 可进行资源统计 |
|
第一、保护与同步。
在这里要强调的是:保护与同步是两个不同的概念。而我们经常会混合这两个概念。保护是指在多线程的环境下对共享资源的保护。这样的共享资源大多数情况下是一段内存块,它会被很多线程试图访问和修改。而同步更多的强调的是线程之间的协作,协同工作是需要同步支持的。
基于这一性质,我们可以看出:Critical Section对象其本质更多的强调的是保护,而Event对象、Mutex对象与Semaphore对象更多的强调的是同步。不过,这样的区别,只是概念上的区别,其本身不会对程序本身产生影响。
第二、锁的等待超时
在开发并发的多进/线程程序时,为了避免死锁之类的问题,引入了“等超时“的概念,即当一个线程需要获得一个锁来执行某些代码的时候,它可以在所等待的锁上设置超时值。如果在确定的时间(超时值)内无法获得该锁,它可以选择放弃执行该段代码的权利,这样可以在一定程度上避免出现死锁的问题。这就是锁的等待超时的基本含义。基于这一行为特征,我们来对上面四种同步对象做一个划分:Critical Section对象是无法设置等待超时的,而其他三个对象则可以设置等待超时。从这一点来讲,在使用Critical Section对象时,由于在等待进入关键代码段时无法设置等待超时,很容易造成死锁。
第三、线程锁与进程锁
这里所说的线程锁指的是该锁只在一个进程的所有线程中可见,而进程锁指的是该锁可以被不同的进程所访问,可用于进程间的同步与互斥。当然进程锁仍然可以被用于同一个进程的不同线程之间的同步与互斥。进程锁的概念是大于线程锁的。基于这一特点划分的话,Critical Section对象是线程锁,而其他三个对象是进程锁。这一点从本质上来分析,Critical Section对象是用户态模式下面实现线程同步的方法,而其他三个对象均是内核对象。内核对象机制的适应性远远优于用户方式机制。实际上,内核对象机制的唯一不足之处在于它的速度比较慢,这是因为当调用内核机制对象时,必须从用户方式转到内核方式。这样的转换需要付出很大的代价,是一件很费时的操作。在X86平台上,这样往返一次需要占用1000个CPU周期(这并不包括执行内核方式的代码)。当然需要注意的是:使用Critical Section对象并不意味着线程不会陷入核心态执行。当一个线程试图进入另一个线程拥有的关键代码段时,该线程就会进入等待状态。这意味着:该线程必须从用户态转为核心态。(为了提高这一方面的性能,Microsoft将循环锁的概念纳入到了Critical Section对象中,该线程可以有选择地不进入核心态等待.具体请参阅MSDN)
第四、锁的递归特质
所谓递归锁指的是当一个线程拥有一个同步锁时,而递归地想再次取得该锁.如果这次获得操作不会阻塞当前线程的执行,则称该锁为递归锁.递归锁主要是在"保护"的概念上提出的,而"保护"概念下的锁包括Critical Section对象和Mutext 对象.这两种锁在Windows平台上都是递归锁。需要注意的是:调用线程获得几次递归锁必须释放几次递归锁。
第五、读写锁
读写锁允许高效的并发的访问多线程环境下的共享资源。对于一种共享资源,多个线程可以获得读锁,共享地读该共享资源。而在同一时刻,只允许一个线程拥有写锁改变该共享资源.这就是读写锁的概念。很遗憾的是在Windows平台上没有这样的读写锁,你需要自己去实现。
来自 <http://www.voidcn.com/article/p-quoygwnv-q.html>
死锁
在开发并发的多进/线程程序时,为了避免死锁之类的问题,引入了“等超时“的概念,即当一个线程需要获得一个锁来执行某些代码的时候,它可以在所等待的锁上设置超时值。如果在确定的时间(超时值)内无法获得该锁,它可以选择放弃执行该段代码的权利,这样可以在一定程度上避免出现死锁的问题。这就是锁的等待超时的基本含义。基于这一行为特征,我们来对上面四种同步对象做一个划分:Critical Section对象是无法设置等待超时的,而其他三个对象则可以设置等待超时。从这一点来讲,在使用Critical Section对象时,由于在等待进入关键代码段时无法设置等待超时,很容易造成死锁。
一、CRITICAL_SECTION 代码临界区
非内核对象
CRITICAL_SECTION提供对代码的临界访问,同一时间只有一个线程对资源访问
示例:
#include <windows.h>
#include <stdio.h>
#include <process.h>
#include <stdlib.h>
#define NUM_THREAD 50
unsigned WINAPI threadInc(void *arg);
unsigned WINAPI threadDes(void *arg);
long long num = 0;
CRITICAL_SECTION cs;
int main()
{
HANDLE hHandles[NUM_THREAD];
int i;
InitializeCriticalSection(&cs); //初始化临界区
for(i = 0; i < NUM_THREAD; i++)
{
if(i % 2)
hHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadInc, NULL, 0, NULL);
else
hHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0, NULL);
}
WaitForMultipleObject(NUM_THREAD, hHandles, TRUE, INFINITE);
DeleteCriticalSection(&cs); //释放临界区
printf("result: %lld \n", num);
return 0;
}
unsigned WINAPI threadInc(void *arg)
{
int i;
EnterCriticalSection(&cs); //进入临界区
for(i = 0; i < 50000000; i++)
num += 1;
LeaveCriticalSection(&cs); //离开临界区
return 0;
}
unsigned WINAPI threadDes(void *arg)
{
int i;
EnterCriticalSection(&cs); //进入临界区
for(i = 0; i < 50000000; i++)
num -= 1;
LeaveCriticalSection(&cs); //离开临界区
return 0;
}
二、Mutex 互斥量对象
内核对象
通过如下函数销毁:BOOL CloseHandle( HANDLE hObject //要销毁内核对象的句柄);
在线程中调用WaitForSingleObject\WaitForMultiplieObjects,如果此时同步对象处于Non-Signaled状态,线程将被阻塞,当同步对象切换到Signaled状态后,此函数返回,线程切换到就绪态,同时互斥量对象自动进入non-signaled状态,当此线程释放互斥量对象后,互斥量对象进入signaled状态。
互斥量是“auto-reset”模式的内核对象。
创建互斥量的函数:
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes, //传递安全相关的配置信息,使用默认安全设置时可以传递NULL
BOOL bInitialOwner, //如果为TRUE,则创建出的互斥量对象属于调用该函数的线程,同时进入non-signaled状态;
//如果为FALSE,则创建出的互斥量对象不属于任何线程,此时状态为signaled
LPCTSTR lpName //用于命名互斥量对象。传入NULL时创建无名的互斥量对象
);
如果互斥量对象不属于任何拥有者,则将进入signaled状态
获取互斥量的函数: WaitForSingleObject,函数返回时自动进入non-signaled状态
释放互斥量的函数: BOOL ReleaseMutex(HANDLE hMutex //需要释放的对象的句柄);
例子
#include <windows.h>
#include <process.h>
#include <stdio.h>
#include <stdlib.h>
#define NUM_THREAD 50
unsigned WINAPI threadInc(void * arg);
unsigned WINAPI threadDes(void * arg);
long long num = 0;
HANDLE hMutex;
int main()
{
HANDLE tHandles[NUM_THREAD];
int i;
hMutex = CreateMutex(NULL, FALSE, NULL); //创建互斥量,此时为signaled状态
for (i = 0; i < NUM_THREAD; i++)
{
if (i % 2)
tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadInc, NULL, 0, NULL);
else
tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0, NULL);
}
WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);
CloseHandle(hMutex); //销毁对象
printf("result: %lld \n", num);
return 0;
}
unsigned WINAPI threadInc(void * arg)
{
int i;
WaitForSingleObject(hMutex, INFINITE); //获取,进入的钥匙
for (i = 0; i < 50000000; i++)
num += 1;
ReleaseMutex(hMutex); //释放,离开时上交钥匙
return 0;
}
unsigned WINAPI threadDes(void * arg)
{
int i;
WaitForSingleObject(hMutex, INFINITE);
for (i = 0; i < 50000000; i++)
num -= 1;
ReleaseMutex(hMutex);
return 0;
}
三、Semaphore 信号量对象
内核对象
通过如下函数销毁:BOOL CloseHandle( HANDLE hObject //要销毁内核对象的句柄);
用于限定同时访问资源的数目,创建时指定最大同时访问的数目,任一线程\进程占用后,计数减1,线程\进程释放该信号量后,计数加1
信号量对象大于0时成为signaled对象,为0时成为non-signaled对象。
调用WaitForSingleObject函数时,若信号量counter等于0,则阻塞此线程;当信号量counter大于0,则此函数返回,此线程从阻塞态变为就绪态,返回的同时将信号量的值减1,同时进入non-signaled状态。
信号量对象可以有名字或者无名字,有名字的信号量可以跨进程使用
创建信号量的函数
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, //安全配置信息,采用默认安全设置时NULL
LONG lInitialCount, // 指定信号量的初始值,应大于0小于lMaximumCount
LONG lMaximumCount, // 信号量的最大值。该值为1时,信号量变为只能表示0和1的二进制信号量
LPCTSTR lpName // 用于命名信号量对象。传递NULL时创建无名的信号量对象
);
可以利用“信号量值为0时进入non-signaled状态,大于0时进入signaled状态”的特性进行同步。向lInitialCount参数传递0时,创建non-signaled状态的信号量对象。而向lMaximumCount传入3时,信号量最大值为3,因此可以实现3个线程同时访问临界区时的同步。
获取信号量对象的函数: WaitForSingleObject函数返回,信号量变为non-signaled状态
释放信号量对象的函数:
BOOL ReleaseSemaphore(
HANDLE hSemaphore, //传递需要释放的信号量对象
LONG lReleaseCount, //释放将增加Counter计数器,通过该参数可以指定增加的值。超过最大值则不增加,返回FALSE
LPLONG lpPreviousCount //用于保存之前值得变量地址,不需要时可传递NULL
);
例子:
#include <windows.h>
#include <process.h>
#include <stdio.h>
unsigned WINAPI Read(void * arg);
unsigned WINAPI Accu(void * arg);
static HANDLE semOne;
static HANDLE semTwo;
static int num;
int main(int argc, char *argv[])
{
HANDLE hThread1, hThread2;
//创建信号量对象,设置为0进入non-signaled状态
semOne = CreateSemaphore(NULL, 0, 1, NULL);
//创建信号量对象,设置为1进入signaled状态
semTwo = CreateSemaphore(NULL, 1, 1, NULL);
hThread1 = (HANDLE)_beginthreadex(NULL, 0, Read, NULL, 0, NULL);
hThread2 = (HANDLE)_beginthreadex(NULL, 0, Accu, NULL, 0, NULL);
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
CloseHandle(semOne); //销毁
CloseHandle(semTwo); //销毁
return 0;
}
unsigned WINAPI Read(void * arg)
{
int i;
for (i = 0; i < 5; i++)
{
fputs("Input num: ", stdout);
//临界区的开始 signaled状态
WaitForSingleObject(semTwo, INFINITE);
scanf("%d", &num);
//临界区的结束 non-signaled状态
ReleaseSemaphore(semOne, 1, NULL);
}
return 0;
}
unsigned WINAPI Accu(void * arg)
{
int sum = 0, i;
for (i = 0; i < 5; i++)
{
//临界区的开始 non-signaled状态
WaitForSingleObject(semOne, INFINITE);
sum += num;
//临界区的结束 signaled状态
ReleaseSemaphore(semTwo, 1, NULL);
}
printf("Result: %d \n", sum);
return 0;
}
四、Event 事件对象
内核对象
通过如下函数销毁:BOOL CloseHandle( HANDLE hObject //要销毁内核对象的句柄);
事件同步对象与前2种同步方法相比有很大不同,区别在于:该方法下创建对象时,可以选择创建auto-reset或者manual-reset类型的事件。
线程调用WaitForSingleObject函数时,若Event为non-signaled状态,则阻塞此线程;当Event变为Signaled状态,则此函数返回,此线程从阻塞态变为就绪态,
- 如果该Event为AutoReset,则该函数返回的同时该Event会被设置为non-signaled状态。如果需要将Event变为Signaled状态,则需要用户调用SetEvent函数;
- 如果该Event为ManualReset,则该函数返回后,Event状态也不会被设置为non-signaled状态,必须由用户使用ResetEvent函数来手工将事件的状态复原到无信号状态;
创建事件对象的函数:
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全配置相关参数,采用默认安全配置时传入NULL
BOOL bManualReset, // 传入TRUE时创建manual-reset模式的事件对象,传入FALSE时创建auto-reset模式的事件对象
BOOL bInitialState, // 传入TRUE时创建signaled状态,传入FALSE时创建non-signaled状态的事件对象
LPCTSTR lpName // 用于命名事件对象。传递NULL时创建无名的事件对象
);
获取事件对象的函数: WaitForSingleObject,对于auto-reset类型的事件,返回后,事件对象变为non-signaled状态; 对于manual-reset类型的事件,函数返回后,事件对象也不会变为non-signaled状态,需要手动更改事件状态。
改变事件对象的状态:
BOOL ResetEvent(HANDLE hEvent); //to the non-signaled
BOOL SetEvent(HANDLE hEvent ); //to the signaled
示例:
#include <windows.h>
#include <stdio.h>
#include <process.h>
#define STR_LEN 100
unsigned WINAPI NumberOfA(void *arg);
unsigned WINAPI NumberOfOthers(void *arg);
static char str[STR_LEN];
static HANDLE hEvent;
int main(int argc, char *argv[])
{
HANDLE hThread1, hThread2;
//以non-signaled创建manual-reset模式的事件对象
hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
hThread1 = (HANDLE)_beginthreadex(NULL, 0, NumberOfA, NULL, 0, NULL);
hThread2 = (HANDLE)_beginthreadex(NULL, 0, NumberOfOthers, NULL, 0, NULL);
fputs("Input string: ", stdout);
fgets(str, STR_LEN, stdin);
//读入字符串后改为signaled状态
SetEvent(hEvent);
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
//non-signaled 如果不更改,对象继续停留在signaled
ResetEvent(hEvent);
CloseHandle(hEvent);
return 0;
}
unsigned WINAPI NumberOfA(void *arg)
{
int i, cnt = 0;
WaitForSingleObject(hEvent, INFINITE);
for (i = 0; str[i] != 0; i++)
{
if (str[i] == 'A')
cnt++;
}
printf("Num of A: %d \n", cnt);
return 0;
}
unsigned WINAPI NumberOfOthers(void *arg)
{
int i, cnt = 0;
WaitForSingleObject(hEvent, INFINITE);
for (i = 0; str[i] != 0; i++)
{
if (str[i] != 'A')
cnt++;
}
printf("Num of others: %d \n", cnt - 1);
return 0;
}