Windows线程同步API

本文主要总结创建、结束线程和WIN32 API提供的一些线程同步方法。同步方法包括用户态同步方式:InterLock、CriticalSection、SRWLock和内核态同步方式:Event、Semaphore、Mutex等。本文通过简单的例子演示API的使用,没有包含原理的说明,假定读者具有其他语言或者平台的并发编程经验。

创建、结束线程

WIN32 API虽然提供了CreateThead和ExitThread方法,但是在C++中,永远不应该使用这两个方法创建或结束线程。而应该使用VC++提供的_beginthread、_beginthreadex方法,相应的结束线程方法_endthread、_endthreadex。后者除了在内部调用CreateThread或ExitThread方法外,还负责CRT的初始化或销毁。虽然有直接结束线程的方法,但在C++最好通过线程方法正常返回来结束线程。直接结束线程时C++对象的析构函数不会被调用。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include "stdafx.h"
  
using namespace std;
  
class Obj
{
public :
     Obj() { cout << "Obj() called" << endl; }
     ~Obj() { cout << "~Obj() called" << endl; }
};
  
unsigned int WINAPI ThreadFunc( void * pvParam){
     cout << static_cast < char *>(pvParam) << endl;
     Obj obj;
     _endthreadex(2);
     return 1;
}
  
int _tmain( int argc, _TCHAR* argv[])
{
     unsigned int threadId;
     char *param = "param" ;
     HANDLE thread = ( HANDLE )_beginthreadex(NULL, 0, ThreadFunc, param, 0, &threadId);
  
     Sleep(100);
     DWORD exitCode;
     GetExitCodeThread( thread , &exitCode);
     cout << "ExitCode:" << exitCode << endl;
     system ( "pause" );
     return 0;
}

这段代码的输出为:

param
Obj() called
ExitCode:2
请按任意键继续. . .

_beginthreadex的第一个参数为SECURITY_ATTRIBUTES结构指针,可以指定NULL使用默认的安全配置。第二参数为cbStackSize,线程栈大小;设置为0使用默认值,可以通过链接参数/STACK:[reserve][,commit]控制。第三个参数为线程入口方法地址,方法签名如ThreadFunc所示。第四个三处为传递给入口方法的参数(值传递),具体意义由程序自己解释。最后一个参数是返回的线程ID。返回值为新创建线程的句柄。__endthreadex方法唯一的参数指定线程的ExitCode。可以通过GetExitCodeThread方法获得线程退出码。

InterLocked系列原子方法

InterLocked系列方法可视为原子的。完成其功能时,保证其他线程不会访问同一个资源。例如最简单的InterLockedIncrement方法原子自增一个共享变量。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
long g_sum(0);
  
unsigned int WINAPI ThreadFunc( void * pvParam){
     for ( int i = 0; i < 100000; ++i)
     {
         InterlockedIncrement(&g_sum);
     }
     return 0;
}
  
int _tmain( int argc, _TCHAR* argv[])
{
     unsigned int threadId;
     HANDLE thread1 = ( HANDLE )_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &threadId);
     HANDLE thread2 = ( HANDLE )_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &threadId);
  
     Sleep(1000);
     cout << "Sum:" << g_sum << endl;
     system ( "pause" );
     return 0;
}

其他方法包括:

InterlockedIncrement64:自增一个64位整数。

InterlockedExchangeAdd、InterlockedExchangeAdd64:加和两个数并赋值给第一个值。

InterlockedCompareExchange:比较并交换一个数。

还有很多InterLocked方法,具体参考MSDN文档。

CriticalSection

通过EnterCriticalSection和LeaveCriticalSection方法,可以控制同步一段代码的访问。使用前需要使用InitializeCriticalSection初始化CRITICAL_SECTION。使用方法也很简单。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
CRITICAL_SECTION g_cs;
long g_sum(0);
  
unsigned int WINAPI ThreadFunc( void * pvParam){
     for ( int i = 0; i < 100000; ++i)
     {
         EnterCriticalSection(&g_cs);
         g_sum += 2;
         LeaveCriticalSection(&g_cs);
     }
     return 0;
}
  
int _tmain( int argc, _TCHAR* argv[])
{
     InitializeCriticalSection(&g_cs);
     unsigned int threadId;
     HANDLE thread1 = ( HANDLE )_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &threadId);
     HANDLE thread2 = ( HANDLE )_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &threadId);
  
     Sleep(1000);
     cout << "Sum:" << g_sum << endl;
     DeleteCriticalSection(&g_cs);
     system ( "pause" );
     return 0;
}

这里有一个问题是,如果同步的代码块不是简单g_sum += 2,而是可能抛出异常的复杂代码。就需要确保LeaveCriticalSection一定被调用。不再使用后使用DeleteCriticalSection方法删除之。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class CSManager
{
public :
     CSManager(CRITICAL_SECTION *cs) : m_cs(cs)
     {
         EnterCriticalSection(m_cs);
     }
     ~CSManager()
     {
         LeaveCriticalSection(m_cs);
     }
private :
     CRITICAL_SECTION *m_cs;
};
  
//...
     for ( int i = 0; i < 100000; ++i)
     {
         CSManager CSMgr(&g_cs);
         g_sum += 2;
     }
//...

CSManager在构造函数中调用EnterCriticalSection,析构函数中调用LeaveCriticalSection。保证在代码块结束时调用Leave方法。

另外除了使用阻塞的Enter方法,还有一个TryEnterCriticalSection,该方法尝试进去CriticalSetion,如果失败,不会阻塞,而是立即返回FALSE。

SRWLOCK

SRWLOCK具有和CriticalSection类似的功能。另外还具有读写锁分离的分离的功能。可以使用AcquireSRWLockShared获取共享的读锁。使用AcquireSRWLockExclusive获取独占的写锁。使用对应的ReleaseSRWLockShared/Exclusive方法施放锁。同样地,使用前需要使用InitializeSRWLock初始化。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
SRWLOCK g_lock;
long g_sum(0);
  
unsigned int WINAPI ReadThreadFunc( void * pvParam){
     for ( int i = 0; i < 10; ++i)
     {
         AcquireSRWLockShared(&g_lock);
         cout << g_sum << endl;
         ReleaseSRWLockShared(&g_lock);
         Sleep(1);
     }
     return 0;
}
  
unsigned int WINAPI WriteThreadFunc( void * pvParam){
     for ( int i = 0; i < 100000; ++i)
     {
         AcquireSRWLockExclusive(&g_lock);
         g_sum += 2;
         ReleaseSRWLockExclusive(&g_lock);
     }
     return 0;
}
  
int _tmain( int argc, _TCHAR* argv[])
{
     InitializeSRWLock(&g_lock);
  
     unsigned int threadId;
     HANDLE thread1 = ( HANDLE )_beginthreadex(NULL, 0, ReadThreadFunc, NULL, 0, &threadId);
     HANDLE thread2 = ( HANDLE )_beginthreadex(NULL, 0, ReadThreadFunc, NULL, 0, &threadId);
     HANDLE thread3 = ( HANDLE )_beginthreadex(NULL, 0, WriteThreadFunc, NULL, 0, &threadId);
  
     Sleep(1000);
     cout << "Sum:" << g_sum << endl;
     system ( "pause" );
     return 0;
}

SRWLOCK不具备类似于TryEnterCriticalSection的非阻塞方法。大多数情况下,SRWLOCK比CRITICAL_SECTION有更好的性能。

Condition Variable

为实现近点的生产者消费者问题。我们可以使用两个CONDITION_VARIABLE:g_full,g_empty来实现。在缓冲区满的时候,生产者线程调用SleepConditionVariableSRW(&g_full, &g_lock, INFINITE, 0)施放获得的锁并等待g_full。缓冲区空的时候,消费者可以调用leepConditionVariableSRW(&g_empty, &g_lock, INFINITE, 0)施放获得的锁并等待g_empty。掉进满足后,可是使用WakeAllConditionVariable唤醒所有等待的线程或者使用WakeConditionVariable唤醒一个等待的线程。

和Condition Variable配置使用的可以使CrticalSection也可以使SRWLock。

?
1
2
3
4
5
6
7
8
9
10
BOOL SleepConditionVariableCS(
    PCONDITION_VARIABLE pConditionVariable,
    PCRITICAL_SECTION pCriticalSection,
    DWORD dwMilliseconds);
  
BOOL SleepConditionVariableSRW(
    PCONDITION_VARIABLE pConditionVariable,
    PSRWLOCK pSRWLock,
    DWORD dwMilliseconds,
    ULONG Flags);

参数dwMilliseconds指定等待超时的时间,如果超时方法返回FASLE;INFINITE指定等待不超时。参数Flags指定被唤醒时尝试获得的锁的类型。CONDITION_VARIABLE_LOCKMODE_ SHARED指定获得共享锁或者0指定获得独占锁。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
const int MAX_SIZE = 10;
  
CONDITION_VARIABLE g_full;
CONDITION_VARIABLE g_empty;
SRWLOCK g_lock;
  
list<Product> products;
  
unsigned int WINAPI ProduceThreadFunc( void * pvParam)
{
     int i(0);
     while ( true )
     {
         Sleep( rand () % 100);
         AcquireSRWLockExclusive(&g_lock);
  
         if (products.size() >= MAX_SIZE)
         {
             SleepConditionVariableSRW(&g_full, &g_lock, INFINITE, 0);
         }
         else
         {
             cout << "Produce Product:" << i << " by thread " << GetThreadId(GetCurrentThread()) << endl;
             products.push_back(Product(i++));
         }
  
         WakeAllConditionVariable(&g_empty);
         ReleaseSRWLockExclusive(&g_lock);
          
     }
     return 0;
}
  
unsigned int WINAPI ConsumeThreadFunc( void * pvParam)
{
     while ( true )
     {
         Sleep( rand () % 100);
  
         AcquireSRWLockExclusive(&g_lock);
         if (products.size() == 0)
         {
             SleepConditionVariableSRW(&g_empty, &g_lock, INFINITE, 0);
         }
         else
         {
             Product p = products.front();
             products.pop_front();
             cout << "Consume Product:" << p.m_no << " by thread " << GetThreadId(GetCurrentThread()) << endl;
         }
  
         WakeAllConditionVariable(&g_full);
         ReleaseSRWLockExclusive(&g_lock);
          
  
     }
     return 0;
}
  
int _tmain( int argc, _TCHAR* argv[])
{
     srand ((unsigned) time (NULL));
     InitializeSRWLock(&g_lock);
     unsigned int threadId;
     HANDLE thread1 = ( HANDLE )_beginthreadex(NULL, 0, ProduceThreadFunc, NULL, 0, &threadId);
     HANDLE thread2 = ( HANDLE )_beginthreadex(NULL, 0, ConsumeThreadFunc, NULL, 0, &threadId);
     HANDLE thread3 = ( HANDLE )_beginthreadex(NULL, 0, ConsumeThreadFunc, NULL, 0, &threadId);
  
     WaitForSingleObject(thread1, INFINITE);
     WaitForSingleObject(thread2, INFINITE);
     WaitForSingleObject(thread3, INFINITE);
     system ( "pause" );
     return 0;
}

内核态线程同步方法

除了上面介绍的用户态的线程同步方法。本文继续通过几个简单例子演示内核态的线程同步方法的使用。内核态线程同步方法在性能上肯定比用户态同步方法要差很多。但可以在多个进程间共享。

创建所有的内核态同步对象都范围一个内核对象句柄HANDLE。通过WaitForSingleObject或者WaitForMultipleObjects等待内核同步对象转换为已传信状态(signaled)。如果等待的是线程或者进程对象,那么对应线程或进程结束后即转换为已传信状态。同时还可以指定一个超时时间。WaitForSingleObject包括WAIT_OBJECT_0,WAIT_TIMEOUT和WAIT_FAILED。不再使用后调用CloseHandle释放引用。

?
1
2
3
4
5
6
7
8
9
10
11
12
DWORD dw = WaitForSingleObject(hProcess, 5000);
switch (dw) {
    case WAIT_OBJECT_0:
      // The process terminated.
      break ;
    case WAIT_TIMEOUT:
       // The process did not terminate within 5000 milliseconds.
       break ;
    case WAIT_FAILED:
       // Bad call to function (invalid handle?)
       break ;
}

WaitForMultipleObjects如果指定参数bWaitAll为TRUE,则等待所有对象都转换为已传信状态后才返回,如果为指定bWaitAll为FALSE,则任意对象转换为已传信状态即返回。可以通过以下方法来判断是那个内核同步对象。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
h[0] = hProcess1;
h[1] = hProcess2;
h[2] = hProcess3;
DWORD dw = WaitForMultipleObjects(3, h, FALSE, 5000);
switch (dw) {
    case WAIT_FAILED:
       // Bad call to function (invalid handle?)
       break ;
  
    case WAIT_TIMEOUT:
       // None of the objects became signaled within 5000 milliseconds.
       break ;
  
    case WAIT_OBJECT_0 + 0:
       // The process identified by h[0] (hProcess1) terminated.
       break ;
  
    case WAIT_OBJECT_0 + 1:
       // The process identified by h[1] (hProcess2) terminated.
       break ;
  
    case WAIT_OBJECT_0 + 2:
       // The process identified by h[2] (hProcess3) terminated.
       break ;
}

Event

Event语义上可以理解为一个事件是否发生。SetEvent方法设置Event为Signaled状态。Event有两种类型。第一种是自动重置的事件,调用SetEvent方法后,唤醒一个等待的线程后即自动转换为未传信状态。第二种是手动重置事件,调用SetEvent方法后,需要调用ResetEvent方法设置事件为未传信状态。PulseEvent相当于调用SetEvent后立即调用ResetEvent。对于手动重置时间,PulseEvent会唤醒所有等待的线程。而对于自动重置的事件PulseEvent只会唤醒一个等待的线程。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
HANDLE g_taskEvent;
  
unsigned int WINAPI ComputationTask( void * pvParam)
{
     WaitForSingleObject(g_taskEvent, INFINITE);
     for ( int i = 0; i < 10; ++i)
     {
         cout << "comput " << i << endl;
     }
     return 0;
}
  
int _tmain( int argc, _TCHAR* argv[])
{
     g_taskEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
     unsigned int threadId;
     HANDLE thread1 = ( HANDLE )_beginthreadex(NULL, 0, ComputationTask, NULL, 0, &threadId);
  
     system ( "pause" );
     SetEvent(g_taskEvent);
     ResetEvent(g_taskEvent);
  
     WaitForSingleObject(thread1, INFINITE);
     system ( "pause" );
     return 0;
}

上面是一个简单的例子,ComputationTask线程等待用户输入后才开始计算任务。

Semaphore

Semaphore维护一个资源计数count和一个最大计数maxCount。

当count大于0时,semaphore处于已传信状态。

当count等于0是,semaphore处于未传信状态。

通过ReleaseSemaphore增加count计数,WaitForSingleObject减少cout计数。count不会小于0,也不能大于maxCount。

例如,可以使用semaphore控制能够同时处理的最大任务线程数。当有超过最大数的更多任务线程开启时只能等待其他任务完成并调用ReleaseSemaphore方法施放资源引用计数。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
HANDLE g_semaphore;
  
unsigned int WINAPI RequstProcesor( void * pvParam)
{
     WaitForSingleObject(g_semaphore, INFINITE);
     cout << "Start process request " << GetThreadId(GetCurrentThread()) << endl;
     Sleep(1000);
     ReleaseSemaphore(g_semaphore, 1, NULL);
     return 0;
}
  
int _tmain( int argc, _TCHAR* argv[])
{
  
     g_semaphore = CreateSemaphore(NULL, 2, 2, NULL);
      
     HANDLE threads[10];
     for ( int i = 0; i < 10; i++)
     {
         threads[i] = ( HANDLE )_beginthreadex(NULL, 0, RequstProcesor, NULL, 0, NULL);
     }
  
     WaitForMultipleObjects(10, threads, TRUE, INFINITE);
     system ( "pause" );
     return 0;
}

上面的代码,启动了10个线程,但只能有2个现场可以同时执行,更多的线程只能等待。

Mutex

mutex的功能和CriticalSection功能很相似。都是控制一段临界代码的互斥访问。通过WaitForSingleObject等待mutex。ReleaseMutex释放mutex。

mutex维护一个threadId和一个使用计数count。如果CreateMutex的参数bInitialOwner为TRUE,这threadId为调用线程,cout为1。否则都初始为0。

如果threadId为0,mutex没有被任何线程使用,处于已传信状态。如果threadId不为0,mutex处于未传信状态。mutex和其他内核同步对象一个不同的特殊地方在于。即时mutex处于未传信状态。如果调用WaitForSingleObject的线程是mutex的threadId对应的线程,WaitForSingleObject不会阻塞相当于处于已传信状态。下面的例子演示了mutex的使用。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
HANDLE g_mutex;
  
void ProcessA()
{
     WaitForSingleObject(g_mutex, INFINITE);
     cout << "ProcessA" << " by thread " << GetThreadId(GetCurrentThread()) << endl;
     ReleaseMutex(g_mutex);
}
  
void ProcessB()
{
     WaitForSingleObject(g_mutex, INFINITE);
     ProcessA();
     cout << "ProcessB" << " by thread " << GetThreadId(GetCurrentThread()) << endl;
     ReleaseMutex(g_mutex);
}
  
unsigned int WINAPI ThreadFunc( void * pvParam)
{
     ProcessB();
     return 0;
}
  
int _tmain( int argc, _TCHAR* argv[])
{
     g_mutex = CreateMutex(NULL, FALSE, NULL);
      
     HANDLE threads[10];
     for ( int i = 0; i < 10; i++)
     {
         threads[i] = ( HANDLE )_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, NULL);
     }
  
     WaitForMultipleObjects(10, threads, TRUE, INFINITE);
     system ( "pause" );
     return 0;
}

你可能感兴趣的:(windows)