目录
2,Critical Sections(关键区域、临界区域)
4,哲学家进餐问题(The Dining Philosophers)
第四章 同步控制
1,Postmessage和SendMessage
在 Windows 系统中,PostMessage() 是把消息放到对方的消息队列中,然后不管三七二十一,就回到原调用点继续执行,所以这是异步(asynchronous)行为。而 SendMessage() 根本就像是“直接调用窗口之窗口函数”,除非等该窗口函数结束,是不会回到原调用点的,所以它是同步(synchronous)行为。
2,Critical Sections(关键区域、临界区域)
1)应用情景: “资源”每一次(同一时间内)只能够被一个线程处理
2)特点:Critical section 并不是核心对象。因此,没有所谓 handle 这样的东西。 它和核心对象不同,它存在于进程的内存空间中。你不需要使用像“Create” 这样的 API 函数获得一个 critical section handle。你应该做的是将一个类型为 CRITICAL_SECTION 的局部变量初始化和删除
3)使用方法:
VOID InitializeCriticalSection( LPCRITICAL_SECTION lpCriticalSection ); 参数 lpCriticalSection 一个指针,指向欲被初始化的 CRITICAL_SECTION 变量。这个变量应该在你的 程序中定义。 返回值 此函数传回 void 。
VOID DeleteCriticalSection( LPCRITICAL_SECTION lpCriticalSection ); 参数 lpCriticalSection 指向一个不再需要的 CRITICAL_SECTION 变量。 返回值 此函数传回 void 。
注意:当你用毕 critical section 时,你必须调用 DeleteCriticalSection() 清除它。 这个函数并没有“释放对象”的意义在里头,不要把它和 C++ 的 delete 运算 符混淆了。
VOID EnterCriticalSection( LPCRITICAL_SECTION lpCriticalSection ); 参数 lpCriticalSection 指向一个你即将锁定的 CRITICAL_SECTION 变量。 返回值 此函数传回 void 。
VOID LeaveCriticalSection( LPCRITICAL_SECTION lpCriticalSection ); 参数 lpCriticalSection 指向一个你即将解除锁定的 CRITICAL_SECTION 变量。 返回值 此函数传回 void 。
4)CRITICAL_SECTION的结构
注意:这是微软对于RTL_CRITICAL_SECTION的定义,里面是有handle的因此,一定不要初始化之后就不管了,在不使用的时候一定要DeleteCriticalSection()!
typedef struct _RTL_CRITICAL_SECTION { PRTL_CRITICAL_SECTION_DEBUG DebugInfo; // // The following three fields control entering and exiting the critical // section for the resource // LONG LockCount; LONG RecursionCount; HANDLE OwningThread; // from the thread's ClientId->UniqueThread HANDLE LockSemaphore; ULONG_PTR SpinCount; // force size on 64-bit systems when packed } RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;
5)测试代码:
typedef struct _Node { struct _Node* next; int data; }Node; typedef struct _List { Node* head; CRITICAL_SECTION critical_sec; }List; List* CreateList() { List* plist = (List*)malloc(sizeof(List)); plist->head = NULL; InitializeCriticalSection(&plist->critical_sec); return plist; } void DeleteList(List* pList) { DeleteCriticalSection(&pList->critical_sec); free(pList); } void AddHead(List* pList, Node* node) { EnterCriticalSection(&pList->critical_sec); node->next = pList->head; pList->head = node; LeaveCriticalSection(&pList->critical_sec); }
6)最小锁定时间:
1> 不要长时间锁住一份资源
我能够给你的最牢靠而最立即的警告就是,千万不要在一个 critical section 之中调用 Sleep() 或任何 Wait...() API 函数。
当你以一个同步机制保护一份资源时,有一点必须常记在心,那就是:这 项资源被使用的频率如何?线程必须多快释放这份资源,才能确保整个程序的 运作很平顺?
2> 如果线程在 critical sections 中停 很久,会怎样?
答案是:不会怎样!
操作系统不会当掉。用户不会获得任何错误信息。最坏的情况是,当主线 程(一个 GUI 线程)需要使用这被锁定的资源时,程序会挂在那儿,动也不 动。真的,同步机制并没有什么神奇魔法。
7)避免 Dangling Critical Sections
1> 如果线程在 critical sections 中结束,会怎样?
Critical section 的一个缺点就是,没有办法获知进入 critical section 中的 那个线程是生是死。
从另一个角度看,由于 critical section 不是核心对象,如果进入 critical section 的那个线程结束了或当掉 了,而没有调用 LeaveCriticalSection() 的话,系统没有办法将该 critical section 清除。如果你需要那样的机能,你应该使用 mutex(本章稍后将介绍 mutex)。
3,死锁(Deadlock)
1)问题:如何交换两个链表内容?
void SwapLists(List *list, List *list2) { List *tmp_list; EnterCriticalSection(list1->critical_sec); EnterCriticalSection(list2->critical_sec); tmp->list = list1->head; list1->head = list2->head; list2->head = temp->list; LeaveCriticalSection(list1->critical_sec); LeaveCriticalSection(list2->critical_sec); }
看出以上代码的问题了吗?
线程A Sw apLists(home_address_list, work_address_list);
线程B Sw apLists(work_address_list, home_address_list);
这样A,B线程都会陷入“你等我,我等你”的轮回
2)我如何避免死锁?
强迫将资 源锁定,使它们成为 " all-or-nothing"(要不统统获得,要不统统没有),可以 阻止死锁的发生。
4,哲学家进餐问题(The Dining Philosophers)
终于来到了经典的哲学家进餐问题!
1)哲学家进餐问题是这样子的:
好几位哲学家围绕着餐桌坐,每一位哲学家 要么思考,要么等待,要么就吃饭。为了吃饭,哲学家必须拿起两支筷子(分放于左右两端)。不幸的是,筷子的数量和哲学家相等,所以每支筷子必须由 两位哲学家共享。图 4-1 显现出这种状态。(图片来自《win32》)
哲学家都是有点倔强的人,他们不愿意在吃完之前放下他们的筷子。因 此,如果每位哲学家都抓住了左手边的筷子,他们就不可能抓到右手边的筷 子,因为右边的哲学家正在使用那支筷子,而且拒绝出让。(代码在介绍完Mutex之后献上)
2)我能够等待一 个以上的 critical sections 吗?
不幸的是,critical sections 没办法使用于这种情况。
WaitForMultipleObjects() 等待的只是核心对象,critical section 并不是核心对象(它没有 handle )。因 此,我们必须寻求另一种 Win32 同步机制:mutex。
5,互斥器(Mutexes)
1)应用情景:一个时间内只能够有一个线程拥有 mutex。
2)特点:
1> Win32 的 Mutex 用途和 critical section 非常类似,但是它牺牲速度以增加弹性。
锁住一个未被拥有的 mu tex,比锁住一个未被拥有的 critical section, 需要花费几乎 100 倍的时间。因为 critical section 不需要进入操作系统核 心,直接在“user mode”就可以进行操作。
2> Mutexes 可以跨进程使用。Critical section 则只能够在同一个进程中使用。
为了能够跨进程使用同一个 mutex,你可以在产生 mutex 时指定其名称。如果你指定了名称,系统中的其他任何线程就可以使用这个名称来处理 该 mutex。一定要使用名称,因为你没有办法把 handle 交给一个执行中的 进程。
记住,其他程序也可能使用这个同步机制,所以 mutex 名称对整个系统而言是全局性的。不要把你的 mutex 对象命名为“Object”或“Mutex” 之类,那太过普遍。请使用一些独一无二的名称,如公司名称或应用程序名称等等。
3> 等待一个 mutex 时,你可以指定“结束等待”的时间长度。但对于 critical section 则不行。
3)使用
1> 产生一个互斥器(Mutex)
HANDLE CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCTSTR lpName ); ---------------------------------------------------------------------- 参数 ---------------------------------------------------------------------- lpMutexAttributes 安全属性。NULL 表示使用默认的属性。这一指 定在 Window s 95 中无效。 ---------------------------------------------------------------------- bInitialOwner 如果你希望“调用 CreateMutex() 的这个线程” 拥有产生出来的 mu tex,就将此值设为 TRUE 。 ---------------------------------------------------------------------- lpName mu tex 的名称(一个字符串)。任何进程或线程都 可以根据此名称使用这一 mu tex。名称可以是任意 字符串,只要不含反斜线(backslash,\)即可。 ---------------------------------------------------------------------- 返回值 如果成功,则传回一个 handle,否则传回 NULL。调用 GetLastError() 可 以获得更进一步的信息。如果指定的 mu tex 名称已经存在,GetLastError() 会 传回 ERROR_ ALREADY_EXISTS。 ----------------------------------------------------------------------
HANDLE hMutex; void CreateAndDeleteMutex() { hMutex = CreateMutex(NULL, FALSE, "Demonstration Mutex"); /* Do something here */ CloseHandle(hMutex); }
2> 打开一个互斥器(Mutex)
调用 CreateMutex() 并指定一个早已存在的 mutex 名称,Win32 会回给你一个 m utex handle,而不会为你产生一个新的 mu tex。就像上面所说 的,GetLastError() 会传回 ERROR_ ALREADY_EXISTS。
你也可以使用 OpenMutex() 打开(而非产生)一个原已存在的 mu tex。
这种情况通常是因为,你写了一个 client 进程,并与同一台机器上的 server 进 程交谈,而只有 server 进程才应该产生 mutex,因为它保护了 server 所定义 的结构体。
注意:这是打开,获取一个mutex的handle,而不是锁住一个mutex
3> 锁住一个Mutex
如何获取一个Mutex的拥有权?(锁定他)
欲获得一个 mu tex 的拥有权,请使用 Win32 的 Wait...() 函数。Wait...() 对 mu tex 所做的事情和 EnterCriticalSection() 对 critical section 所做的事情 差不多
一旦没有任何线程拥有 mutex,这个 mu tex 便处于激发状态。因此,如果没有任何线程拥有那个 mu tex,Wait...() 便会成功。反过来说,当线程拥有 mutex 时,它便不处于激发状态。如果有某个线程正在等待一个未被激发的 mutex,它便将进入“blocking”(阻塞)状态。也就是说,该线程会停止执行, 直到 mutex 被其拥有者释放并处于激发状态。
4> 释放一个Mutex
ReleaseMutex() 的规格如下: BOOL ReleaseMutex( HANDLE hMutex ); 参数 hMutex 欲释放之 mu tex 的 handle 。 返回值 如果成功,传回 TRUE 。如果失败,传回 FALSE 。
4)以下是关于Mutex的一些容易忽视的问题
1> 获取Mutex拥有权的流程:
1. 我们有一个 mutex,此时没有任何线程拥有它,也就是说,它处于非激发状态(译注)。
2. 某个线程调用 WaitForSingleObject()(或任何其他的 Wait... () 函数), 并指定该 mutex handle 为参数。
3. Win32 于是将该 mutex 的拥有权给予这个线程,然后将此 mutex 的状 态短暂地设为激发状态,于是 Wait...() 函数返回。
4. Mutex 立刻又被设定为非激发状态,使任何处于等待状态下的其他线程 没有办法获得其拥有权。
5. 获得该 mu tex 之线程调用 ReleaseMutex(),将 mu tex 释放掉。于是循 环回到第一场景,周而复始。
【译注 我想你很容易被作者的上一段文字迷惑,因为它的第一点和更前一段文字中的“一旦没有任何线程拥有 mu tex,这个 mu tex 便处于激发状态”有点背道 而驰。基本上,或许更精密地说,所谓的“mutex 激发状态”应该是:当没 有任何线程拥有该 mu tex 而且有一个线程正以 Wait...() 等待该 mu tex,该 mutex 就会短暂地出现激发状态,使 Wait...() 得以返回。】
2> Mutex 的拥有权
Mutex 的拥有权并非属于那个产生它的线程,而是那个最后对此 mutex 进行 Wait...() 操作并且尚未 进行 ReleaseMutex() 操作的线程。一次只能有一个线程拥有该 mutex。
3> Mutxe何时被摧毁?
如果拥有某 mutex 之线程结束了,该 mutex 会被自动清除的唯一情况是: 此线程是最后一个与该 m utex handle 有关联的线程。否则此核心对象的引用计数仍然是比 0 大,其他线程(以及进程)仍然可以拥有此 mutex 的合法 handle。然而,当线程结束而没有释放某个 mutex 时,有一种特殊的处理方式。
4> 如果一个拥有Mutex的线程在结束时没有释放Mutex会怎样?
如果线程拥有一个 mutex 而在结束前没有调用 ReleaseMutex() ,mutex 不会被摧毁。取而代之的是,该 mutex 会被视 为“未被拥有”以及“未被激发”,而下一个等待中的线程会被以 WAIT_ABANDONED_0 通知。
如果其他线程正以 WaitForMultipleObjects() 等待此 mu tex,该函数也会 返回,传回值介于 WAIT_ ABANDONED_0 和 (WAIT_ABANDONED_0_n +1) 之间,其中的 n 是指 handle 数组的元素个数。线程可以根据这个值了解到究 竟哪一个 mu tex 被放弃了。至于 WaitForSingleO bject() ,则只是传回 WAIT_ABANDONED_0。
5)又来到了喜闻乐见的哲学家进餐问题
#include "windows.h" #include "mutex" #define MAX_NUMBER 5 typedef struct tag_philosopherDining { int iID; int iChopsticksID[2]; //HANDLE hRightChopstick; //HANDLE hLeftChopstick; HANDLE hChopsticks[2]; HANDLE hLock; }*LPPhilosopherDining; DWORD WINAPI P4_2ThreadFunc(LPVOID); int P4_2_DiningPhilosophers() { HANDLE hChopsticks[MAX_NUMBER]; LPPhilosopherDining lpPhilosopherDining[MAX_NUMBER]; HANDLE hThread[MAX_NUMBER]; DWORD dwThreadID[MAX_NUMBER]; for (int i = 0; i < MAX_NUMBER; i++) { hChopsticks[i] = CreateMutex(NULL, FALSE, NULL); if (hChopsticks[i] == NULL) { printf("Mutex创建失败!"); return -1; } lpPhilosopherDining[i] = new tag_philosopherDining(); lpPhilosopherDining[i]->iID = i; lpPhilosopherDining[i]->hLock = CreateMutex(NULL, FALSE, NULL); lpPhilosopherDining[i]->iChopsticksID[0] = i % MAX_NUMBER; lpPhilosopherDining[i]->iChopsticksID[1] = (i + 1) % MAX_NUMBER; lpPhilosopherDining[i]->hChopsticks[0] = hChopsticks[i % MAX_NUMBER]; lpPhilosopherDining[i]->hChopsticks[1] = hChopsticks[(i + 1) % MAX_NUMBER]; if (lpPhilosopherDining[i]->hLock == NULL) { printf("Mutex创建失败!"); return -1; } } for (int i = 0; i < MAX_NUMBER; i++) { hThread[i] = CreateThread(NULL, 0, P4_2ThreadFunc, (LPVOID)lpPhilosopherDining[i], 0, &dwThreadID[i]); } WaitForMultipleObjects(5, hThread, TRUE, INFINITE); printf("哲学家们用餐完毕\n"); for (int i = 0; i < MAX_NUMBER; i++) { if (lpPhilosopherDining[i] == NULL) continue; CloseHandle(lpPhilosopherDining[i]->hChopsticks[0]); //CloseHandle(lpPhilosopherDining[i]->hChopsticks[1]); CloseHandle(lpPhilosopherDining[i]->hLock); delete lpPhilosopherDining[i]; lpPhilosopherDining[i] = NULL; } getchar(); return 1; } DWORD WINAPI P4_2ThreadFunc(LPVOID p) { LPPhilosopherDining pPD = (LPPhilosopherDining)p; WaitForMultipleObjects(2, pPD->hChopsticks, TRUE, INFINITE); WaitForSingleObject(pPD->hLock, INFINITE); printf("%d 哲学家在用餐 餐具:%d号,%d号\n", pPD->iID+1, pPD->iChopsticksID[0]+1, pPD->iChopsticksID[1]+1); ReleaseMutex(pPD->hLock); ReleaseMutex(pPD->hChopsticks[0]); ReleaseMutex(pPD->hChopsticks[1]); return 0; }
注意:筷子只能用左右的筷子哦~
运行结果:
2 哲学家在用餐 餐具:2号,3号 1 哲学家在用餐 餐具:1号,2号 4 哲学家在用餐 餐具:4号,5号 5 哲学家在用餐 餐具:5号,1号 3 哲学家在用餐 餐具:3号,4号 哲学家们用餐完毕
6)为什么有一个最初拥有者?
HANDLE hMutex = CreateMutex(NULL, FALSE, "Sample Name"); int result = WaitForSingleObject(hMutex, INFINITE);
由于mutexes可以跨进程使用,以及跨线程使用。Mutex可以根据其名称而被开启。所以,另一个进程可以完全不需要和产生mutex 的进程打声招呼,就根据名称开启一个mutex。
因此,第二句可能会发生了context Switch产生 race condition。
CreateMutex() 的第二个参数 bInitialOwner,允许你指定现行线程(current thread)是否立刻拥有即将产生出来的 mutex。
6,信号量(Semaphores)
1)应用情景:是解决各种 producer/consum er 问题的关键要素。这种 问题会存有一个缓冲区,可能在同一时间内被读出数据或被写入数据。
2)特点:一个 sem aphore 可以被锁住最多 n 次,其中 n 是 semaphore 被产生时指定的。n 常常被设计用来代表“可以锁住一份资源”的线程个数,不 过并非单独一个线程就不能够拥有所有的锁定。
3)实例:租车行的n辆同款的车型可以用一个设置为n的semaphore来处理。
4)使用:
1>产生信号量(Semaphore)
HANDLE CreateSemaphore( LPSECURITY_ATTRIBUTES lpAttributes, LONG lInitialCount, LONG lMaximumCount, LPCTSTR lpName ); ----------------------------------------------------------------- 参数 ----------------------------------------------------------------- lpAttributes 安全属性。如果是 NULL 就表示要使用默认属 性。Windows 95 忽略这一参数。 ----------------------------------------------------------------- lInitialCount semaphore的初值。必须大于或等于 0,并且小于 或等于 lMaxim umCount。 ----------------------------------------------------------------- lMaximumCount Semaphore的最大值。这也就是在同一时间内能够 锁住 sem aphore 之线程的最多个数。 ----------------------------------------------------------------- lpName Semaphore 的名称(一个字符串)。任何线程(或 进程)都可以根据这一名称引用到这个 semaphore。这个值可以是 NULL ,意思是产生一 个没有名字的 sem aphore。 ----------------------------------------------------------------- 返回值 如果成功就传回一个 handle ,否则传回 NULL 。不论哪一种情况, GetLastError() 都会传回一个合理的结果。如果指定的 sem aphore 名称已经存 在,则该函数还是成功的,GetLastError() 会传回 ERRO R_ALREADY_EXISTS。
2> 获得锁定
你可以使用任何一 种 Wait...() 函数(例如 WaitForSingleO bject())要求锁定一个 sem aphore。因 此,如果 sem aphore 的现值不为 0,Wait...() 函数会立刻返回。
3> 解除锁定(Releasing Locks)
BOOL ReleaseSemaphore( HANDLE hSemaphore, LONG lReleaseCount, LPLONG lpPreviousCount ); 参数 hSemaphore Semaphore 的 handle 。 lReleaseCount Semaphore 现值的增额。该值不可以是负值或 0。 lpPreviousCount 藉此传回 sem aphore 原来的现值。 返回值 如果成功,则传回 TRUE。否则传回 FALSE。失败时可调用 GetLastError() 获得原因。
ReleaseSemaphore() 对于 sem aphore 所造成的现值的增加,绝对不会超过 CreateSemaphore() 时所指定的 lMaximumCount。
请记住,lpPreviousCount 所传回来的是一个瞬间值。你不可以把 lReleaseCount 加上 *lpPreviousCount ,就当作是 semaphore 的现值,因为其 他线程可能已经改变了 semaphore 的值。
与 mutex 不同的是,调用 ReleaseSemaphore() 的那个线程,并不一定就 得是调用 Wait...() 的那个线程。任何线程都 可以在任何时间调用 ReleaseSemaphore(),解除被任何线程锁定的 semaphore。
7,事件(Event Objects)
1)应用情景和特点:Event 对象是一种核 心对象,它的唯一目的就是成为激发状态或未激发状态。这两种状态全由程序 来控制,不会成为 Wait...() 函数的副作用。
2)使用:
HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset, BOOL bInitialState, LPCTSTR lpName ); ------------------------------------------------------------------------ 参数 ------------------------------------------------------------------------ lpEventAttributes 安全属性。NULL 表示使用默认属性。该属性在 Windows 95 中会被忽略。 ------------------------------------------------------------------------ bManualReset 如为 FALSE ,表示这个 event 将在变成激发状态 (因而唤醒一个线程)之后,自动重置(reset)为 非激发状态。如果是 TRUE,表示不会自动重置, 必须靠程序操作(调用 ResetEvent() )才能将激发 状态的 event 重置为非激发状态。 ------------------------------------------------------------------------ bInitialState 如为 TRUE ,表示这个 event 一开始处于激发状 态。如为 FALSE ,则表示这个 event 一开始处于 非激发状态。 ------------------------------------------------------------------------ lpName Event对象的名称。任何线程或进程都可以根据这 个文字名称,使用这一 event 对象。 ------------------------------------------------------------------------ 返回值 如果调用成功,会传回一个 event handle,GetLastError() 会传回 0。如果 lpName 所指定的 event 对象已经存在,CreateEvent() 传回的是该 event handle ,而不会产生一个新的。这时候 GetLastError() 会传回 ERROR_ALREADY_EXISTS。如果 CreateEvent() 失败,传回的是 NULL , GetLastError() 可以获得更进一步的失败信息。 ------------------------------------------------------------------------
-------------------------------------------------------------------------------- SetEvent() 把 event 对象设为激发状态 -------------------------------------------------------------------------------- ResetEvent() 把 event 对象设为非激发状态(译注:在此我要提醒读者, "Reset" 的意思是“设定为非激发状态”,而非“重新设定为 激发状态”。) -------------------------------------------------------------------------------- PulseEvent() 如果是一个 Manual Reset Event (手动reset的时间): 把 event 对象设为激发状态,唤醒“所有”等待中的线程,然后 event 恢复为非激发状态。 如果是一个 Auto Reset Event:把 event 对象设为激 发状态,唤醒“一个”等待中的线程,然后 event 恢复为非 激发状态 --------------------------------------------------------------------------------
3)例子:
有三个线程在等待一个event。
如果是“Automatic”,则 event 对象总是处于非激发状态,ResetEvent不会产生什么效果。但【SetEvent】和【PulseEvent】 会唤醒一个等待中的线程。
如果你选择的是“Manual”,event 对象的状态可能是激发,也可能是非激发,视上一次调用的是 SetEvent() 或 ResetEvent() 而定。【SetEvent】 会使得每一个等待中的线程立刻苏醒,所以你会在执行画面上看到不断有等 待、苏醒、等待、苏醒……的信息跑出来。【PulseEvent】会使得目前等 待中的所有线程苏醒过来(随后立刻又进入等待状态)。
4)操作系统会强迫让等待中 的线程有轮番更替的机会
如果操作系统没有强迫实现某种层次的公平性,可能会有某个线程不断 获得执行机会,而某个线程一直未能获得 CPU 的青睐。这种情况被称为 starvation(饥饿)。
5)如果我对着一 个 event 对象 调 用 Pulse Event()并且没 有线程正在等 待,会怎样?
如果是一个 AutoReset event对象调用 SetEvent() 或 PulseEvent() , 而彼时并没有任何线程正在等待,这种情况下这个 event 会被遗失。换句话说,除非有线程正在等待, 否则 event 不会被保存下来。
情况1:
对象调用 SetEvent() 或 PulseEvent() , 而彼时并没有任何线程正在等待,会怎样?EVENTTST 程序并没有实地论证 这一点。这种情况下这个 event 会被遗失。换句话说,除非有线程正在等待, 否则 event 不会被保存下来
情况2:
况可能会引起死锁。假设“receiver”线程检查队列中是否有字 符,这时候发生 context switch,切换到“sender”线程,它对一个 event 对象 进行 pulse 操作,这时候又发生 context switch ,回到 receiver 线程,调用 WaitForSingleObject(),等待 event 对象。由于这个动作发生在 sender 线程激 发 event 之后,所以 event 会遗失,于是 receiver 永远不会醒来,程序进入 死锁状态。这正是 sem aphore 之所以被创造用以解决问题的地方
8,Worker 线程中显示输出
请注意,我一直仰赖一件事实:所有的数据可以被所有的线程取用。我使用 sprintf() 在线程的堆栈中产生一个字符串,然后将此字符串地址以 SendMessage() 送出。主线程在更新列表框的画面时,即使用到这个地址,一旦主线程完成这 个消息的处理,SendMessage() 便返回,worker 线程于是继续进行下去。
想象一下,如果我以 PostMessage() 代替 SendMessage() ,会发生什么情 况?由于 PostMessage() 会立刻返回,所以当主线程抓取字符串内容要显示 时,或许该字符串内容早已又被 wo rker 线程改写了。这就是多线程序设计中 最常见的一种两难取舍:在最佳速度和最佳安全性之间取舍。在这里我宁愿选 择比较慢但是比较安全的做法。
9,Interlocked Variable
1)应用情景:
InterlockedDecrement() 可以双效合一,它先将计数器内容减 1,再将其值与 0做比较,并且传回比较结果。
ONG InterlockedIncrement( LPLONG lpTarget ); LONG InterlockedDecrement( LPLONG lpTarget ); ---------------------------------------------------------- 参数 ---------------------------------------------------------- lpTarget 32 位变量的地址。这个变量内容将被递增或递 减,结果将与 0 作比较。这个地址必须指向 long word。 ---------------------------------------------------------- 返回值 变量值经过运算(加 1 或减 1)后,如果等于 0,传回 0;如果大于 0, 传回一个正值;如果小于 0,传回一个负值 ----------------------------------------------------------
这两个函数都只能够和 0 做比较,不能和任何其他数值比较。
2)例子:引用计数
Interlocked...() 函数的传回值代表计数器和 0 的比较结果。这一点对于实 现我们曾经提过的所谓“引用计数”(reference counting )非常重要,因为我 们必须知道“引用计数”何时到达 0。如果没有这个比较,问题就回到了原 点,你必须在增减操作之前先锁定该计数器,以使增减操作成为一个“不可切 割”的操作。
3)InterlockedExchange()
InterlockedExchange() 可以设定一个新值并传回旧值。
LONG InterlockedExchange( LPLONG lpTarget, LONG lValue ); ------------------------------------------------------------------- 参数 ------------------------------------------------------------------- lpTarget 32 位变量的地址。这个指针必须指向 long word。 ------------------------------------------------------------------- lValue 用以取代 lpTarget 所指内容之新值。 ------------------------------------------------------------------- 返回值 传回先前由 lpTarget 所指之内容。 -------------------------------------------------------------------
摘要
Critical Section Critical section(临界区)
用来实现“排他性占有”。适用范围是单一进程 的各线程之间。它是: i 一个局部性对象,不是一个核心对象。
i 快速而有效率。
i 不能够同时有一个以上的 critical section 被等待。
i 无法侦测是否已被某个线程放弃。
Mutex
Mutex 是一个核心对象,可以在不同的线程之间实现“排他性占有”,甚 至即使那些线程分属不同进程。它是:
i 一个核心对象。
i 如果拥有 mu tex 的那个线程结束,则会产生一个 “abandoned” 错 误信息。
i 可以使用 Wait...() 等待一个 mutex。
i 可以具名,因此可以被其他进程开启。
i 只能被拥有它的那个线程释放(released)。
Semaphore
Semaphore 被用来追踪有限的资源。它是:
i 一个核心对象。
i 没有拥有者
i 可以具名,因此可以被其他进程开启。
i 可以被任何一个线程释放(released)。
Event Object
Event object 通常使用于 overlapped I/O(第6章),或用来设计某些自定 义的同步对象。它是:
i 一个核心对象。
i 完全在程序掌控之下。
i 适用于设计新的同步对象。
i “要求苏醒”的请求并不会被储存起来,可能会遗失掉。
i 可以具名,因此可以被其他进程开启。
Interlocked Variable
如果 Interlocked...() 函数被使用于所谓的 spin-lock,那么它们只是一种同 步机制。所谓 spin-lock 是一种 busy loop,被预期在极短时间内执行,所以有 最小的额外负担(overhead)。系统核心偶尔会使用它们。除此之外,interlocked variables 主要用于引用计数。它们:
i 允许对 4 字节的数值有些基本的同步操作,不需动用到 critical section 或 mu tex 之类。
i 在 SMP (Symmetric Multi-Processors)操作系统中亦可有效运作。