《Win32多线程程序设计》–Jim Beveridge & Robert Wiener
同步(synchronous):当程序1调用程序2时,程序1 停下不动,直到程序2完成回到程序1来,程序1才继续下去;
异步(asynchronous):如果程序1调用程序2后,径自继续自己的下一个动作,那么两者之间就是asynchronous;
- Win32 中关于进程和线程的协调工作是由同步机制(synchronous mechanism)来完成的。
- 在任何关于同步机制的讨论中,不论是在 Win32 或 Unix 或其他操作系统,你一定会一再地听到这样一条规则:不要长时间锁住一份资源。最牢靠而最立即的警告:千万不要在一个 critical section 之中调用 Sleep() 或任何 Wait…() API 函数。
某些人会关心这样的问题:如果我再也不释放资源(或不离开 critical section,或不释放 mutex……等等),会怎样?答案是:不会怎样!操作系统不会当掉。用户不会获得任何错误信息。最坏的情况是,当主线程(一个 GUI 线程)需要使用这被锁定的资源时,程序会挂在那儿,动也不动。
Critical section 并不是核心对象。 因此,没有所谓 handle 这样的东西。它和核心对象不同,它存在于进程的内存空间中。
一旦线程进入一个 critical section,它就能够一再地重复进入该 critical section。唯一的警告就是,每一个“进入”操作都必须有一个对应的“离开”操 作 。 如果某个线程调用EnterCriticalSection() 5 次,它也必须调用LeaveCriticalSection() 5 次,该 critical section 才能够被释放。
Critical section 的一个缺点就是,没有办法获知进入 critical section 中的那个线程是生是死。从另一个角度看,由于 critical section 不是核心对象,如果进入 critical section 的那个线程结束了或当掉了,而没有调用LeaveCriticalSection() 的话,系统没有办法将该 critical section 清除。如果你需要那样的机能,你应该使用 mutex。
任何时候当一段代码需要两个(或更多)资源时,都有潜在性的死锁阴影。强迫将资源锁定,使它们成为 ” all-or-nothing”(要不统统获得,要不统统没有),可以阻止死锁的发生,这样做往往会影响程序性能。更好的方式是使用下文的Mutex和WaitForMultipleObjects解决死锁问题。
#include
#include
#define LOOPNUM 10000000
typedef struct tagSAFTDATA {
int sum;
CRITICAL_SECTION lock;
} SAFTDATA;
static SAFTDATA g_SaftData = {0};
static void AddSafety() {
EnterCriticalSection(&g_SaftData.lock);
g_SaftData.sum += 1;
LeaveCriticalSection(&g_SaftData.lock);
}
static void LessSafety() {
EnterCriticalSection(&g_SaftData.lock);
g_SaftData.sum -= 1;
LeaveCriticalSection(&g_SaftData.lock);
}
DWORD WINAPI Thread(void *arg) {
for (int i = 0; i < LOOPNUM; i++) {
EnterCriticalSection(&g_SaftData.lock);
LessSafety();
g_SaftData.sum -= 1;
LeaveCriticalSection(&g_SaftData.lock);
}
return 0;
}
int main(void) {
DWORD start = GetTickCount();
InitializeCriticalSection(&g_SaftData.lock);
HANDLE hThread = CreateThread(NULL, 0, Thread, NULL, 0, NULL);
for (int i = 0; i < LOOPNUM; i++) {
AddSafety();
}
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
DeleteCriticalSection(&g_SaftData.lock);
DWORD end = GetTickCount();
printf("%d main, usedtime=%d\n", g_SaftData.sum, end-start);
return 0;
}
虽然 mutex 和 critical section 做相同的事情,但是它们的运作还是有差别的:
Mutexes行为特征:
(示例:Mutex同步进程间访问共享内存)
#include
#include
#define SAFTDATA_MUTEX "SAFTDATA_MUTEX"
#define SAFTDATA_FILEMAP "SAFTDATA_FILEMAP"
#define SAFTDATA_FILEMAP_SIZE sizeof(int)
#define LOOPNUM 1000000
typedef struct tagSAFTDATA {
int sum;
LPVOID filemapBuf;
HANDLE filemap;
HANDLE mutex;
} SAFTDATA;
static SAFTDATA g_SaftData = {0};
#if 0
// server
int main(void) {
DWORD start = GetTickCount();
g_SaftData.mutex = CreateMutex(NULL, FALSE, SAFTDATA_MUTEX); // 如果mutex名称已经存在,GetLastError()会传回ERROR_ALREADY_EXISTS
g_SaftData.filemap = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, SAFTDATA_FILEMAP_SIZE, SAFTDATA_FILEMAP);
printf("[server] %#p(mutex), %#p(filemap)\n", g_SaftData.mutex, g_SaftData.filemap);
g_SaftData.filemapBuf = MapViewOfFile(g_SaftData.filemap, FILE_MAP_ALL_ACCESS, 0, 0, 0);
memset(g_SaftData.filemapBuf, 0, SAFTDATA_FILEMAP_SIZE);
UnmapViewOfFile(g_SaftData.filemapBuf);
for (int i = 0; i < LOOPNUM; i++) {
WaitForSingleObject(g_SaftData.mutex, INFINITE);
g_SaftData.filemapBuf = MapViewOfFile(g_SaftData.filemap, FILE_MAP_ALL_ACCESS, 0, 0, 0);
memcpy(&g_SaftData.sum, g_SaftData.filemapBuf, SAFTDATA_FILEMAP_SIZE);
g_SaftData.sum += 1;
memcpy(g_SaftData.filemapBuf, &g_SaftData.sum, SAFTDATA_FILEMAP_SIZE);
UnmapViewOfFile(g_SaftData.filemapBuf);
ReleaseMutex(g_SaftData.mutex);
//printf("%d ", i);
}
CloseHandle(g_SaftData.filemap);
CloseHandle(g_SaftData.mutex);
DWORD end = GetTickCount();
printf("[server] %d main, usedtime=%d, %#p(mutex), %#p(filemap)\n", g_SaftData.sum, end-start, g_SaftData.mutex, g_SaftData.filemap);
system("pause");
return 0;
}
#else
// client
int main(void) {
DWORD start = GetTickCount();
g_SaftData.mutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, SAFTDATA_MUTEX);
g_SaftData.filemap = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, SAFTDATA_FILEMAP);
printf("[client] %#p(mutex), %#p(filemap)\n", g_SaftData.mutex, g_SaftData.filemap);
for (int i = 0; i < LOOPNUM; i++) {
WaitForSingleObject(g_SaftData.mutex, INFINITE);
g_SaftData.filemapBuf = MapViewOfFile(g_SaftData.filemap, FILE_MAP_ALL_ACCESS, 0, 0, 0);
memcpy(&g_SaftData.sum, g_SaftData.filemapBuf, SAFTDATA_FILEMAP_SIZE);
g_SaftData.sum -= 1;
memcpy(g_SaftData.filemapBuf, &g_SaftData.sum, SAFTDATA_FILEMAP_SIZE);
UnmapViewOfFile(g_SaftData.filemapBuf);
ReleaseMutex(g_SaftData.mutex);
//printf("%d ", i);
}
CloseHandle(g_SaftData.filemap);
CloseHandle(g_SaftData.mutex);
DWORD end = GetTickCount();
printf("[client] %d main, usedtime=%d, %#p(mutex), %#p(filemap)\n", g_SaftData.sum, end-start, g_SaftData.mutex, g_SaftData.filemap);
system("pause");
return 0;
}
#endif
semaphores(信号量)在电脑科学中它是最具历史的同步机制。它是解决各种 producer/consumer 问题的关键要素,这种问题会存有一个缓冲区,可能在同一时间内被读出数据或被写入数据。
在许多系统中, semaphores 常被使用,因为 mutexes 可能并不存在。在 Win32 中 semaphores 被使用的情况就少得多,因为 mutex 存在的缘故。理论可以证明, mutex 是 semaphore 的一种退化。如果你产生一个 semaphore 并令最大值为 1,那就是一个 mutex。Win32 中的一个 semaphore 可以被锁住最多 n 次,其中 n 是 semaphore 被产生时指定的,semaphores 不像 mutexes,它并没有所谓的“wait abandoned”状态可以被其他线程侦测到。
semaphore 的现值代表的意义是目前可用的资源数。如果现值为 5,就表示还有五个锁定动作可以成功。每当一个锁定动作成功, semaphore 的现值就会减 1。你可以使用任何一种 Wait…() 函数(例如 WaitForSingleO bject())要求锁定一个 semaphore。因此,如果 sem aphore 的现值不为 0, Wait…() 函数会立刻返回。
如果锁定成功,你也不会收到 semaphore 的拥有权,因为可以有一个以上的线程同时锁定一个 semaphore,在 semaphore 身上并没有拥有权的观念,一个线程可以反复调用 Wait…() 函数以产生新的锁定。这和 mutex绝不相同:拥有 mutex 的线程不论再调用多少次 Wait…() 函数,也不会被阻塞住。一旦 semaphore 的现值降到 0,就表示资源已经耗尽。此时,任何线程如果调用 Wait…() 函数,必然要等待,直到某个锁定被解除为止。
为了解除锁定,你必须调用 ReleaseSem aphore()。这个函数将 semaphore的现值增加一个定额,通常是 1,并传回 semaphore 的前一个现值。
(示例:信号量解决 producer/consumer 的问题)
#include
#include
#include
using namespace std;
//#define SEMAPHORE
#ifdef SEMAPHORE
#define QUEUE_SIZE 10
static HANDLE g_hSemEmpty = NULL;
static HANDLE g_hSemFull = NULL;
#endif
#define THREAD_NUM 5
static HANDLE g_hEvent = NULL;
static CRITICAL_SECTION g_queueLock;
static queue<int> g_Queue;
static int g_startItem = 0;
static void RecordQueueSizeMax() {
char curDir[256], confPath[256];
GetCurrentDirectory(sizeof(curDir), curDir);
sprintf(confPath, "%s\\record.txt", curDir);
int size = GetPrivateProfileInt("queue", "size", -1, confPath);
EnterCriticalSection(&g_queueLock);
//printf("---%d---\n", g_Queue.size());
int queuesize = g_Queue.size();
LeaveCriticalSection(&g_queueLock);
if (size < queuesize) {
char sizebuf[16];
sprintf(sizebuf, "%d", queuesize);
WritePrivateProfileString("queue", "size", sizebuf, confPath);
}
}
DWORD WINAPI ProducerThread(void *arg) {
int startItem = 0;
while (1) {
if (WAIT_TIMEOUT != WaitForSingleObject(g_hEvent, 0))
break; // 激发Event对象后退出线程
RecordQueueSizeMax(); // 记录queue队列中最大数量
#ifdef SEMAPHORE
WaitForSingleObject(g_hSemEmpty, INFINITE); // g_hSemEmpty--
#endif
EnterCriticalSection(&g_queueLock);
g_Queue.push(g_startItem++);
printf("+%d ", g_Queue.back());
LeaveCriticalSection(&g_queueLock);
#ifdef SEMAPHORE
ReleaseSemaphore(g_hSemFull, 1, NULL); // g_hSemFull++
#endif
}
return 0;
}
DWORD WINAPI ConsumerThread(void *arg) {
while (1) {
if (WAIT_TIMEOUT != WaitForSingleObject(g_hEvent, 0))
break; // 激发Event对象后退出线程
#ifdef SEMAPHORE
WaitForSingleObject(g_hSemFull, INFINITE); // g_hSemFull--
#endif
EnterCriticalSection(&g_queueLock);
#ifndef SEMAPHORE
if (0 < g_Queue.size()) // 使用信号量时,无需此判断
#endif
{
printf("-%d ", g_Queue.front());
g_Queue.pop();
}
LeaveCriticalSection(&g_queueLock);
#ifdef SEMAPHORE
ReleaseSemaphore(g_hSemEmpty, 1, NULL); // g_hSemEmpty++
#endif
}
return 0;
}
int main(void) {
DWORD start, end;
HANDLE hThreadArray[THREAD_NUM];
start = GetTickCount();
InitializeCriticalSection(&g_queueLock); // queue读写同步
g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); // 手动控制、初始为非激发状态
#ifdef SEMAPHORE
g_hSemEmpty = CreateSemaphore(NULL, QUEUE_SIZE, QUEUE_SIZE, NULL); // queue大小为QUEUE_SIZE是,生产者不能继续增加数据到queue
g_hSemFull = CreateSemaphore(NULL, 0, QUEUE_SIZE, NULL); // queue大小为0是,消费者不能继续从queue中取出数据
#endif
for (int i = 0; i < THREAD_NUM; i++) {
if (0 == i%2)
hThreadArray[i] = CreateThread(NULL, 0, ProducerThread, NULL, 0, NULL);
else
hThreadArray[i] = CreateThread(NULL, 0, ConsumerThread, NULL, 0, NULL);
}
{ // 5s后激发Event对象,用于退出线程
Sleep(5000);
SetEvent(g_hEvent);
}
WaitForMultipleObjects(THREAD_NUM, hThreadArray, TRUE, INFINITE); // 等待所有线程
for (int i = 0; i < THREAD_NUM; i++)
CloseHandle(hThreadArray[i]);
#ifdef SEMAPHORE
CloseHandle(g_hSemEmpty);
CloseHandle(g_hSemFull);
#endif
CloseHandle(g_hEvent);
DeleteCriticalSection(&g_queueLock);
end = GetTickCount();
printf("elapseMSec=%lu, queueSize=%d\n", end-start, g_Queue.size());
system("pause");
return 0;
}
Win32 中最具弹性的同步机制就属 events 对象了。 Event 对象是一种核心对象,它的唯一目的就是成为激发状态或未激发状态。这两种状态全由程序来控制,不会成为 Wait…() 函数的副作用。
示例参考: Win32线程——在某个线程内终止另一个正在运行的线程(2)(Event对象)
同步机制的最简单类型是使用 interlocked 函数,对着标准的 32 位变量进行操作。这些函数并没有提供“等待”机能,它们只是保证对某个特定变量的存取操作是“一个一个接顺序来”。
Interlocked适合去维护一个 32 位计数器的“排他性存取”性质,由于一个 32 位变量的存取操作只需要 2~3 个机器指令,这时候使用一个 critical section 或一个 mutex 太浪费时间和资源。
#include
#include
#define OPERAT_COUNT 10000000
static int g_RefCount = 0;
static volatile int g_RefCount_s = 0;
DWORD WINAPI AddThread(void *arg) {
for (int i = 0; i < OPERAT_COUNT; i++)
g_RefCount++;
return 0;
}
DWORD WINAPI LessThread(void *arg) {
for (int i = 0; i < OPERAT_COUNT; i++)
g_RefCount--;
return 0;
}
DWORD WINAPI AddThread_s(void *arg) {
for (int i = 0; i < OPERAT_COUNT; i++)
InterlockedIncrement((LONG *)&g_RefCount_s);
return 0;
}
DWORD WINAPI LessThread_s(void *arg) {
for (int i = 0; i < OPERAT_COUNT; i++)
InterlockedDecrement((LONG *)&g_RefCount_s);
return 0;
}
int main(void) {
DWORD start, end;
HANDLE hThreadArray[2];
start = GetTickCount();
hThreadArray[0] = CreateThread(NULL, 0, AddThread, NULL, 0, NULL);
hThreadArray[1] = CreateThread(NULL, 0, LessThread, NULL, 0, NULL);
WaitForMultipleObjects(2, hThreadArray, TRUE, INFINITE); // 等待所有线程
CloseHandle(hThreadArray[0]);
CloseHandle(hThreadArray[1]);
end = GetTickCount();
printf("g_RefCount=%d, elapseMSec=%lu\n", g_RefCount, end-start);
start = GetTickCount();
hThreadArray[0] = CreateThread(NULL, 0, AddThread_s, NULL, 0, NULL);
hThreadArray[1] = CreateThread(NULL, 0, LessThread_s, NULL, 0, NULL);
WaitForMultipleObjects(2, hThreadArray, TRUE, INFINITE); // 等待所有线程
CloseHandle(hThreadArray[0]);
CloseHandle(hThreadArray[1]);
end = GetTickCount();
printf("g_RefCount_s=%d, elapseMSec=%lu\n", g_RefCount_s, end-start);
return 0;
}
(示例:Interlocked…() 函数作为一种同步机制被使用于 spin-lock)
#include
#include
static volatile BOOL g_spinFlag = FALSE;
DWORD WINAPI Thread(void *arg) {
while(TRUE == InterlockedExchange((LONG *)&g_spinFlag, TRUE)) {
Sleep(100); // 进入自旋锁,等待
}
for(int i = 0; i < 5; i++) {
printf("%d ", *(int *)arg);
Sleep(1000);
}
InterlockedExchange((LONG *)&g_spinFlag, FALSE); // 释放自旋锁
return 0;
}
int main(void) {
DWORD start, end;
int arg[2] = {1, 2};
HANDLE hThreadArray[2];
start = GetTickCount();
hThreadArray[0] = CreateThread(NULL, 0, Thread, &arg[0], 0, NULL);
hThreadArray[1] = CreateThread(NULL, 0, Thread, &arg[1], 0, NULL);
WaitForMultipleObjects(2, hThreadArray, TRUE, INFINITE); // 等待所有线程
CloseHandle(hThreadArray[0]);
CloseHandle(hThreadArray[1]);
end = GetTickCount();
printf("elapseMSec=%lu\n", end-start);
return 0;
}