20.3 线程的同步
20.3.1 产生同步问题的原因
(1)线程的只能是在两条指令之间被打断,不可能在一条指令执行到一半被打断,因为指令是CPU最小的执行单位。
(2)线程的切换是无法预测的,他无法知道自己的什么时候自己的时间片会结束,也无法知道下一个时间片被分配给哪一个线程。线程可以在任何地方被Windows打断。
(3)各线程可能要读写同一内享区,如果不加保护就会出现错误。
20.3.2 各种用于线程间同步的对象
(1)使用临界区(Critical Section)
①使用方法
A、CRITICAL_SECTION cs; //声明为全局变量,它的字段被Windows内部使用
B、InitializeCriticalSection(&cs); //初始化临界区,注意cs是临界区对象,不能被移动和复制
C、EnterCriticalSection(&cs); //当某个线程调用该函数时,在函数内部会检查是否己经有其他线己进入临界区。如果没有,则该线程拥有临界区,然后函数返回,表示可以独占数据了,于是执行该线程剩下的代码。如果发现己经有别的线程占有了临界区,则线程会将自己挂起(CPU不分配时间片了,等待其他线程或Windows来唤醒)。注意,这个函数会一直等于占有临界区才会返回,否则一直停止在函数内部)。
D、LeaveCriticalSection(&cs); //离开临界区,将临界区交还给Windows,以便其他线程可以申请使用,该函数会唤醒使用该临界区的其他线程(因为使用该临界区的对象会被记录下来,该函数就知道哪些线程该被唤醒)。如果该线程在没占用临界区的情况下,调用这样的函数,会抛出一个错误。
E、DeleteCriticalSection(&cs);//不再需要临界区对象。
②临界区的特点:
A、只能在同一个进程中使用,不能跨进程同步。临界区不能给对象命名。
B、每次只能有一个线程拥有临界区。
C、如果某个线程进入临界区后就挂掉,那么无法被其他等待的线程检测到,因为他们都“陷”在EnterCriticalSection函数中
D、临界区对象在同一进程使用,占用的资源更少,速度优势也明显。
(2)使用互斥量(Mutex)——允许跨进程,可命名,可检测其他线程是否挂掉。
①创建互斥量:CreateMutex
参数 |
含义 |
lpMutexAttributes |
互斥对象的安全属性。如果不需被继承,这里设为NULL |
bInitialOwner |
TRUE——创建它的线程直接获取该互斥量 FALSE——互斥量被创建后处于空闲状态 |
lpName |
互斥量的名称,如果对象有了名称,可在其他进程中用OpenMutix函数打开,以便用于进程间的线程同步。如果不需要,则设为NULL |
②释放互斥量:ReleaseMutex。(注意必须首先拥有互斥量)
③关闭互斥量:CloseHandle(不再使用时候)
④获取互斥量:WaitForSingleObject函数。//获取互斥量后,会将互斥量变为无信号状态(加锁),其他线程无法获取,得继续等待。直到线程调用ReleaseMutex变为有信号(即解锁互斥量)。
【代码示例】
CreateMutex(…); …… //在线程函数中 WaitForSingleObject(hMutex,INFINITE);//当线程检测到无信号时,会一直处于等待信号状态(即将自己挂起。但如果在指定在时间内等不到信号,又会被Windows唤醒并直接返回) …… ReleaseMutex(hMutex); //将互斥量变为有信号状态,并唤配使用该互斥量的其他线程。
(3)使用信号量(Semaphore)
①信号灯对象允许指定数量的线程获取对象。如服务器设置3个工作线程以及n个客户端连接,但3个工作线程都忙的时候,客户端就必须等待。否则将其他空闲的信号灯对象分配给客户端,以进行服务。当信号灯对象设为1个的时候,相当于互斥量。
②创建信号灯:CreateSemaphore
参数 |
含义 |
lpSemaphoreAttributes |
安全属性,可以在这里指定为NULL |
dwInitialCount |
初始化的时候,设置信号灯对象的数量 |
dwMaximunCount |
最大的信号灯数量 |
lpName |
用来为信号灯对象的命名,以\0结尾的字符串。可以在其他进程中用OpenSemaphore函数打开。也可以设为NULL |
③增加信号灯:ReleaseSemaphore(hSemaphore,dwReleaseCount,lpPreviousCount);
★dwReleaseCount是在当前对象数量的基础上,当释放信号灯后要增加的计数值,一般为1,如果信号量加上这个值会导致信号量的当前值大于信号量创建时指定的最大值,那么这个信号量的当前值不变,同时这个函数返回FALSE;
★lpPreviousCount:返回释放前的计数值。不需要时可设为NULL
④关闭信号灯对象:CloseHandle
⑤获取信号灯对象,也是用WaitForSingleObject函数。当某个对象被获取时,计数值减1.当计数值未减到0时,对象的状态是有信号的。当减到0时,对象被设为无信号状态(即被锁定)。
【代码示例】
CreateSemaphore(……); //线程函数内部 WaitForSingleObject(hSemaphore,INFINTE); //有信号时,获取它。当对象无信号时,线程自己挂起。 …… ReleaseSemaphore(hSemaphore,1,NULL); //释放信号灯对象(加1个),并唤醒其他使用该对象的线程。
20.3.3 WaitForSingleObject——等待某一核心对象变为有信号状态
(1)参数及含义
参数 |
含义 |
hHandle |
等待某一核心对象 |
dwMilliseconds |
等待的最长时间,时间终于,即使handle尚未成为激发状态,此函数还是要返回。此值为0时表示立刻返回,也可以是INFINITE代表无限等待。 |
(2)返回值:失败时返回WAIT_FAILED,成功时,返回
①WAIT_OBJECT_0:目标对象变为激发状态。
②WAIT_TIMEOUT:等待时间终了,但目标对象仍未被激发
③WAIT_ABANDONED:如果是互斥对象(Mutex),且在拥有该对象的线程,在结束前没有仍没释放这一Mutex对象,该对象会被设为非激活状态,同时等待的线程返回该值。
注意:当线程1在等待线程2释放对象,由于Windows会持续追踪线程2,即使线程2失事或被强迫结束,WaitForSingleObject仍然可以正常工作。
(3)WaitForSingleObject可等待的对象:
①Change notification;②Console input;③Job;④Mutex;
⑤Process;⑧Semaphore ⑨ Thread。
注意:当等待的目标对象是某线程时,当该线程对象仍处在执行过程中,则是不可等待(即非激活的),执行完毕后,变为激活状态,可以通过WaitForSingleObject获得该对象。
20.4 触发事件——即可用于线程间的通信,又可以用来进行线程的同步
(1)该对象用户可能通过SetEvent或ResetEvent来设置状态,以进行人工干预线程的执行。以达到线程间的通信。如通过设置为有信号状态,来通知子线程继续执行,或子线程通过ResetEvent设置为无信号状态,告知主线程任务结束。
(2)创建事件对象:CreateEvent
参数 |
含义 |
lpEventAttributes |
安全属性,可设为NULL |
bManualReset |
TRUE——该对象会一直保持着有信号的状态,哪怕是调用WaitForSingleObject后也会保持有信号,必须手动地ResetEvent,使事件对象变为无信号状态。当线程函数结束前,要执行SetEvent变为有信号状态,以便其他等待线程可以继续工作。 FALSE——调用WaitForSingleObject后,会自动设置为无信号状态,当线程函数结束前,同样也要执行SetEvent变为有信号状态。同时,当进程中仅剩的一个等待线程执行完毕后,会自动设为无信号状态。(课本P952) |
bInitialState |
设置对象的初始状态。 TRUE——有信号状态 FALSE——无信号状态 |
lpName |
事件的对象的名称,以\0结尾,可在其他进程用OpenEvent打开。 |
(3)设置对象的信号状态:SetEvent——有信号;ResetEvent——无信号状态
(4)关闭事件对象:CloseHandle
【示例代码】
CreateEvent(…); …… //线程函数内部 WaitForSingleObject(hEvent,INFINITE); //ResetEvent(hEvent); //如果bManualReset为TRUE时,要手动变为无信号 …… SetEvent(hEvent); //变为有信号状态
【BigJob1程序】——利用消息进行线程间的通信
①该程序每次按左键时,都会开辟一个线程。
②主线程是通过附加参数(pParams)与子线程通信,而子线程则是通过发送消息给主线程进行通信。
③线程退出的情况:利用bContinue字段或计算结束,线程函数结束,子线程正常退出。KillThread函数只有在没办法正常退出才使用。这主要是为了防止在子线程中分配的资源无法释放。
④本程序一般会创建1个主线程和1个工作线程。但当Windows在SendMessage调用和_endthread调用之间从工作线程切换到主线程时,并且这时主线程处理好WM_CACL_DONE消息,即iStatus己被设置为 STATUS_DONE,然后用户又按了左键,则可能会创建第3个线程。尽管在这里,这种情况不会带来什么问题。但也应该使用临界区来避免线程冲突。
【效果图】
/*------------------------------------------------------------ BIGJOB1.C -- Multithreading Demo(通过发送消息来进行线程间的通信) (c) Charles Petzold, 1998 ------------------------------------------------------------*/ #include <windows.h> #include <math.h> #include <process.h> //重复计数 #define REP 10000000 //当前工作状态 #define STATUS_READY 0 #define STATUS_WORKING 1 #define STATUS_DONE 2 //自定义消息 #define WM_CALC_DONE (WM_USER + 0) #define WM_CALC_ABORTED (WM_USER + 1) //传给线程函数的额外数据 typedef struct { HWND hwnd; BOOL bContinue; //用户可通过该变量,决定是否结束线程 }PARAMS,*PPARAMS; LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("BigJob1") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, // window class name TEXT ("Multithreading Demo"), // window caption WS_OVERLAPPEDWINDOW, // window style CW_USEDEFAULT, // initial x position CW_USEDEFAULT, // initial y position CW_USEDEFAULT, // initial x size CW_USEDEFAULT, // initial y size NULL, // parent window handle NULL, // window menu handle hInstance, // program instance handle NULL) ; // creation parameters ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } //大任务的工作线程,蛮力计算:tan(arctan(exp(log(sqrt(x^2)))))+1; void Thread(PVOID pvoid) { LONG lTime; double A = 1.0; int i; volatile PPARAMS pParams; //volatile,强制每次都到内存中读取pParams的值。 pParams = (PPARAMS)pvoid; lTime = GetCurrentTime(); //开始计算所用时间 for (i = 0; i < REP && pParams->bContinue; i++) A = tan(atan(exp(log(sqrt(A*A))))) + 1.0; if (i == REP) //己经达到计算次数 { lTime = GetCurrentTime() - lTime; //计算所用时间; SendMessage(pParams->hwnd, WM_CALC_DONE, 0, lTime); //通知主线程己计算完毕,主线程将更新窗口, //注意,不在工作线程中更新界面 } else SendMessage(pParams->hwnd, WM_CALC_ABORTED, 0, 0); //如果用户按右键取消线程 _endthread(); //结束线程,一般该函数结束会,会自己结束该线程,但在复杂环境中,该调用也是很有必要。 } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static int iStatus; static LONG lTime; static PARAMS params; static TCHAR* szMessage[] = { TEXT("准备就绪(按鼠标左键开始)"), TEXT("工作中...(按鼠标右键结束)"), TEXT("%d次的重复,总共花了%ld毫秒")}; TCHAR szBuffer[64]; HDC hdc ; PAINTSTRUCT ps ; RECT rect ; switch (message) { case WM_LBUTTONDOWN: //如果己经正在工作,则发出提示声,并直接返回。 if (iStatus == STATUS_WORKING) { MessageBeep(0); return 0; } iStatus = STATUS_WORKING; params.hwnd = hwnd; params.bContinue = TRUE; //开始线程(注意这里每次都会开辟一个新线程) _beginthread(Thread, 0, ¶ms); InvalidateRect(hwnd, NULL, TRUE); return 0; case WM_RBUTTONDOWN: params.bContinue = FALSE; return 0; case WM_CALC_DONE: lTime = lParam; //所用时间:放在消息的lParam参数中 iStatus = STATUS_DONE; InvalidateRect(hwnd, NULL, TRUE); return 0; case WM_CALC_ABORTED: //计算过程中如果按右键线程将被停止,并释放。 iStatus = STATUS_READY; InvalidateRect(hwnd, NULL, TRUE); return 0; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rect) ; wsprintf(szBuffer, szMessage[iStatus], REP, lTime); DrawText (hdc, szBuffer, -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
【BigJob2程序】——利用事件和消息进行线程间的通信
①该程序整个程序生命期内,只创建一个子线程。
②线程函数包含一个无限的while循环,每次循环调用WaitForSingleObject等待事件通知。事件的初始状态被我们设为无信号,所以线程会自动挂起。当鼠标左键时,事件被设为有信号,因为是自动设置状态的,所以WaitForSingleObject函数返回,事件被设为无信号,然后开始计算一次蛮力计算。计算结束后,线程再次调用WaitForSingleObject,然后又挂起自己,等待下一次鼠标单击事件的触发。
③主线程利用控制事件状态与子线程进行通信,子线程通过消息与主线程通信。
【效果图】与BigJob1程序一样
/*------------------------------------------------------------ BIGJOB2.C -- Multithreading Demo(通过事件对象来进行线程间的通信) (c) Charles Petzold, 1998 ------------------------------------------------------------*/ #include <windows.h> #include <math.h> #include <process.h> //重复计数 #define REP 10000000 //当前工作状态 #define STATUS_READY 0 #define STATUS_WORKING 1 #define STATUS_DONE 2 //自定义消息 #define WM_CALC_DONE (WM_USER + 0) #define WM_CALC_ABORTED (WM_USER + 1) //传给线程函数的额外数据 typedef struct { HWND hwnd; HANDLE hEvent; //比BigJob1了这个字段,因为线程函数要判断事件的状态 BOOL bContinue; //用户可通过该变量,决定是否结束线程 }PARAMS, *PPARAMS; LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT("BigJob2"); HWND hwnd; MSG msg; WNDCLASS wndclass; wndclass.style = CS_HREDRAW | CS_VREDRAW; wndclass.lpfnWndProc = WndProc; wndclass.cbClsExtra = 0; wndclass.cbWndExtra = 0; wndclass.hInstance = hInstance; wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION); wndclass.hCursor = LoadCursor(NULL, IDC_ARROW); wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); wndclass.lpszMenuName = NULL; wndclass.lpszClassName = szAppName; if (!RegisterClass(&wndclass)) { MessageBox(NULL, TEXT("This program requires Windows NT!"), szAppName, MB_ICONERROR); return 0; } hwnd = CreateWindow(szAppName, // window class name TEXT("Multithreading Demo"), // window caption WS_OVERLAPPEDWINDOW, // window style CW_USEDEFAULT, // initial x position CW_USEDEFAULT, // initial y position CW_USEDEFAULT, // initial x size CW_USEDEFAULT, // initial y size NULL, // parent window handle NULL, // window menu handle hInstance, // program instance handle NULL); // creation parameters ShowWindow(hwnd, iCmdShow); UpdateWindow(hwnd); while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } //大任务的工作线程,蛮力计算:tan(arctan(exp(log(sqrt(x^2)))))+1; void Thread(PVOID pvoid) { LONG lTime; double A = 1.0; int i; volatile PPARAMS pParams; //volatile,强制每次都到内存中读取pParams的值。 pParams = (PPARAMS)pvoid; while (TRUE) { //因事件是个自动Reset的对象,调用WaitForSingleObject时, //会在其内部进入等待事件的状态,获得事件对象后,会自动设为无信号状态 //让下次循环执行到该语句时,线程自动挂起,等待事件对象。 WaitForSingleObject(pParams->hEvent, INFINITE); lTime = GetCurrentTime(); //开始计算所用时间 for (i = 0; i < REP && pParams->bContinue; i++) A = tan(atan(exp(log(sqrt(A*A))))) + 1.0; if (i == REP) //己经达到计算次数 { lTime = GetCurrentTime() - lTime; //计算所用时间; PostMessage(pParams->hwnd, WM_CALC_DONE, 0, lTime); //通知主线程己计算完毕,主线程将更新窗口, //注意,不在工作线程中更新界面 } else //如果是用户按右键取消线程 PostMessage(pParams->hwnd, WM_CALC_ABORTED, 0, 0); } _endthread(); //结束线程,一般该函数结束会,会自己结束该线程,但在复杂环境中,该调用也是很有必要。 } LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static HANDLE hEvent; static int iStatus; static LONG lTime; static PARAMS params; static TCHAR* szMessage[] = { TEXT("准备就绪(按鼠标左键开始)"), TEXT("工作中...(按鼠标右键结束)"), TEXT("%d次的重复,总共花了%ld毫秒") }; TCHAR szBuffer[64]; HDC hdc; PAINTSTRUCT ps; RECT rect; switch (message) { case WM_CREATE: //第2个参数:bManualReset,第3个参数:初始设为无信号状态 hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); params.hEvent = hEvent; params.hwnd = hwnd; params.bContinue = FALSE; //开始线程(注意这里整个程序生命周期内只开一个子线程) _beginthread(Thread, 0, ¶ms); return 0; case WM_LBUTTONDOWN: //如果己经正在工作,则发出提示声,并直接返回。 if (iStatus == STATUS_WORKING) { MessageBeep(0); return 0; } iStatus = STATUS_WORKING; params.bContinue = TRUE; SetEvent(hEvent); InvalidateRect(hwnd, NULL, TRUE); return 0; case WM_RBUTTONDOWN: params.bContinue = FALSE; return 0; case WM_CALC_DONE: lTime = lParam; //所用时间:放在消息的lParam参数中 iStatus = STATUS_DONE; InvalidateRect(hwnd, NULL, TRUE); return 0; case WM_CALC_ABORTED: //计算过程中如果按右键线程将被停止,并释放。 iStatus = STATUS_READY; InvalidateRect(hwnd, NULL, TRUE); return 0; case WM_PAINT: hdc = BeginPaint(hwnd, &ps); GetClientRect(hwnd, &rect); wsprintf(szBuffer, szMessage[iStatus], REP, lTime); DrawText(hdc, szBuffer, -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER); EndPaint(hwnd, &ps); return 0; case WM_DESTROY: PostQuitMessage(0); return 0; } return DefWindowProc(hwnd, message, wParam, lParam); }
【Ticket1程序】——多线程模拟火车站多窗口售票
/*------------------------------------------------------------ TICKETS.C -- 利用临界区模拟多窗口售票的多线程程序 (c) Charles Petzold, 1998 ------------------------------------------------------------*/ #include <windows.h> #define WM_USER_SELLED (WM_USER + 1) DWORD WINAPI ThreadProc1(PVOID pVoid); DWORD WINAPI ThreadProc2(PVOID pVoid); DWORD WINAPI ThreadProc3(PVOID pVoid); LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int tickets = 100; //总票数 CRITICAL_SECTION cs; typedef struct { HWND hwnd; int nTicket; int nID; }PARAMS,*PPARAMS; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("HelloWin") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, // window class name TEXT ("Tickets System"), // window caption WS_OVERLAPPEDWINDOW, // window style CW_USEDEFAULT, // initial x position CW_USEDEFAULT, // initial y position CW_USEDEFAULT, // initial x size CW_USEDEFAULT, // initial y size NULL, // parent window handle NULL, // window menu handle hInstance, // program instance handle NULL) ; // creation parameters ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } DWORD WINAPI ThreadProc1(PVOID pVoid) { PPARAMS pParam = (PPARAMS)pVoid; while (TRUE) { EnterCriticalSection(&cs); //调用函数,如果有人进入临界区,则挂起自己 if (tickets>0) { pParam->nTicket = --tickets; pParam->nID = 1; SendMessage(pParam->hwnd, WM_USER_SELLED, 0, 0); } LeaveCriticalSection(&cs); //退出临界区,唤醒其他挂起的线程——谁拥有,谁释放! Sleep(1); //退出临界区后,先休眠一下,让别人也有机会来进入,否则 //如果在临界区中休眠,因该线程仍然拥有这个临界区对象, //在休眠(挂起)的这段时间内,其他线程还是无法进入临界区。 //而离开临界区后,会马上又获得该对象,导致只有一个线程在 //卖票的现象。 if (tickets == 0) break; //票如果全部售完,就结束线程 } return TRUE; } DWORD WINAPI ThreadProc2(PVOID pVoid) { PPARAMS pParam = (PPARAMS)pVoid; while (TRUE) { EnterCriticalSection(&cs); if (tickets>0) { pParam->nTicket = --tickets; pParam->nID = 2; SendMessage(pParam->hwnd, WM_USER_SELLED, 0, 0); } LeaveCriticalSection(&cs); Sleep(1); if (tickets == 0) break; } return TRUE; } DWORD WINAPI ThreadProc3(PVOID pVoid) { PPARAMS pParam = (PPARAMS)pVoid; while (TRUE) { Sleep(1); EnterCriticalSection(&cs); if (tickets>0) { pParam->nTicket = --tickets; pParam->nID = 3; SendMessage(pParam->hwnd, WM_USER_SELLED, 0, 0); } LeaveCriticalSection(&cs); Sleep(1); if (tickets == 0) break; } return TRUE; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { HDC hdc ; PAINTSTRUCT ps ; RECT rect ; static int cxClient, cyClient, iLines, cxChar, cyChar,iCol; static HANDLE hThread1, hThread2, hThread3; static PARAMS params; TCHAR szBuffer[64]; switch (message) { case WM_CREATE: cxChar = LOWORD(GetDialogBaseUnits()); cyChar = HIWORD(GetDialogBaseUnits()); params.hwnd = hwnd; params.nID = 0; //初始化临界区对象 InitializeCriticalSection(&cs); //创建3个线程,立即执行(第5个参数为0) hThread1 = CreateThread(NULL, 0, ThreadProc1, ¶ms, 0, 0); hThread2 = CreateThread(NULL, 0, ThreadProc2, ¶ms, 0, 0); hThread3 = CreateThread(NULL, 0, ThreadProc3, ¶ms, 0, 0); iCol = -1; return 0 ; case WM_SIZE: cxClient = LOWORD(lParam); cyClient = HIWORD(lParam); return 0; case WM_USER_SELLED: if (iLines % 25 == 0) { iCol++; iLines = 0; } InvalidateRect(hwnd, NULL, FALSE); UpdateWindow(hwnd); iLines++; return 0; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rect) ; if (params.nID>0) { TextOut(hdc, iCol*25*cxChar, iLines*cyChar, szBuffer, wsprintf(szBuffer, TEXT("%d:线程%d卖出第%d张票"), iCol * 25 + iLines + 1, params.nID, 100 - params.nTicket)); } EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: DeleteCriticalSection(&cs); PostQuitMessage (0); return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
【Ticket2程序】——多线程模态多火车站窗口售票2
/*------------------------------------------------------------ TICKETS.C -- 利用事件对象模拟多窗口售票的多线程程序 (c) Charles Petzold, 1998 ------------------------------------------------------------*/ #include <windows.h> #define WM_USER_SELLED (WM_USER + 1) DWORD WINAPI ThreadProc1(PVOID pVoid); DWORD WINAPI ThreadProc2(PVOID pVoid); DWORD WINAPI ThreadProc3(PVOID pVoid); LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); int tickets = 100; //总票数 HANDLE hEvent; typedef struct { HWND hwnd; int nTicket; int nID; }PARAMS, *PPARAMS; int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT("HelloWin"); HWND hwnd; MSG msg; WNDCLASS wndclass; wndclass.style = CS_HREDRAW | CS_VREDRAW; wndclass.lpfnWndProc = WndProc; wndclass.cbClsExtra = 0; wndclass.cbWndExtra = 0; wndclass.hInstance = hInstance; wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION); wndclass.hCursor = LoadCursor(NULL, IDC_ARROW); wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); wndclass.lpszMenuName = NULL; wndclass.lpszClassName = szAppName; if (!RegisterClass(&wndclass)) { MessageBox(NULL, TEXT("This program requires Windows NT!"), szAppName, MB_ICONERROR); return 0; } hwnd = CreateWindow(szAppName, // window class name TEXT("Tickets System"), // window caption WS_OVERLAPPEDWINDOW, // window style CW_USEDEFAULT, // initial x position CW_USEDEFAULT, // initial y position CW_USEDEFAULT, // initial x size CW_USEDEFAULT, // initial y size NULL, // parent window handle NULL, // window menu handle hInstance, // program instance handle NULL); // creation parameters ShowWindow(hwnd, iCmdShow); UpdateWindow(hwnd); while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } DWORD WINAPI ThreadProc1(PVOID pVoid) { PPARAMS pParam = (PPARAMS)pVoid; while (TRUE) { WaitForSingleObject(hEvent, INFINITE); //等待事件对象,有信号时会获取,无信号则挂起自己。 if (tickets>0) { pParam->nTicket = --tickets; pParam->nID = 1; SendMessage(pParam->hwnd, WM_USER_SELLED, 0, 0); } SetEvent(hEvent); Sleep(1); //强烈建议,Sleep在WaitFor...SetEvent之外。(见本文Sleep函数的说明) if (tickets == 0) break; //票如果全部售完,就结束线程 } return TRUE; } DWORD WINAPI ThreadProc2(PVOID pVoid) { PPARAMS pParam = (PPARAMS)pVoid; while (TRUE) { WaitForSingleObject(hEvent, INFINITE); //等待事件,获得后自动变为无信号状态 if (tickets>0) { pParam->nTicket = --tickets; pParam->nID = 2; SendMessage(pParam->hwnd, WM_USER_SELLED, 0, 0); } SetEvent(hEvent); //变为有信号状态,并唤醒其他线程 Sleep(1); if (tickets == 0) break; } return TRUE; } DWORD WINAPI ThreadProc3(PVOID pVoid) { PPARAMS pParam = (PPARAMS)pVoid; while (TRUE) { WaitForSingleObject(hEvent, INFINITE); //等待事件 if (tickets>0) { pParam->nTicket = --tickets; pParam->nID = 3; SendMessage(pParam->hwnd, WM_USER_SELLED, 0, 0); } SetEvent(hEvent); Sleep(1); if (tickets == 0) break; } return TRUE; } LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { HDC hdc; PAINTSTRUCT ps; RECT rect; static int cxClient, cyClient, iLines, cxChar, cyChar, iCol; static HANDLE hThread1, hThread2, hThread3; static PARAMS params; TCHAR szBuffer[64]; switch (message) { case WM_CREATE: cxChar = LOWORD(GetDialogBaseUnits()); cyChar = HIWORD(GetDialogBaseUnits()); params.hwnd = hwnd; params.nID = 0; hEvent = CreateEvent(NULL, FALSE, TRUE, NULL); //自动对象,初始状态为有信号 //创建3个线程,立即执行(第5个参数为0) hThread1 = CreateThread(NULL, 0, ThreadProc1, ¶ms, 0, 0); hThread2 = CreateThread(NULL, 0, ThreadProc2, ¶ms, 0, 0); hThread3 = CreateThread(NULL, 0, ThreadProc3, ¶ms, 0, 0); iCol = -1; return 0; case WM_SIZE: cxClient = LOWORD(lParam); cyClient = HIWORD(lParam); return 0; case WM_USER_SELLED: if (iLines % 25 == 0) { iCol++; iLines = 0; } InvalidateRect(hwnd, NULL, FALSE); UpdateWindow(hwnd); iLines++; return 0; case WM_PAINT: hdc = BeginPaint(hwnd, &ps); GetClientRect(hwnd, &rect); if (params.nID>0) { TextOut(hdc, iCol * 25 * cxChar, iLines*cyChar, szBuffer, wsprintf(szBuffer, TEXT("%d:线程%d卖出第%d张票"), iCol * 25 + iLines + 1, params.nID, 100 - params.nTicket)); } EndPaint(hwnd, &ps); return 0; case WM_DESTROY: CloseHandle(hEvent); PostQuitMessage(0); return 0; } return DefWindowProc(hwnd, message, wParam, lParam); }
20.5 线程本地存储(TLS)
20.5.1 为什么要有TLS?
①原因在于,进程中的全局变量与函数内定义的静态(static)变量,是各个线程都可以访问的共享变量(如:在个函数内定义了static int iCount变量,每调用该函数一次,都会使iCount计数加1。当线程1调用时,iCount++,线程2调用该函数,iCount也会在原来的基础上自增,即iCount在两个线程中是共享的)。在一个线程修改其内存内容,对所有线程都生效。这是一个优点也是一个缺点。说它是优点,线程的数据交换变得非常便捷。说它是缺点,一个线程死掉了,其它线程也性命不保; 多个线程访问共享数据,需要昂贵的同步开销,也容易造成同步相关的BUG。
②而函数中的局部变量分配在线程堆栈上,在函数结束以后会被释放,不能被多线程共享。
20.5.2 线程本地存储(TLS):
①能被多线程“共享”且互不影响的变量(可视为某个全局变量或静态变量的副本)。
②如需要依赖全局变量或者静态变量,那有没有办法保证在多线程程序中能访问而不互相影响呢?(被称为static memory local to a thread 线程局部静态变量),就需要新的机制来实现。这就是TLS。
20.5.3 TLS的实现方法
(1)静态TLS实现:——编译时这些变量被存在PE文件的.tls节中。
①先定义全局的静态TLS变量,如:
全局变量:__declspec(thread) iGlobal = 1;
函数内部:__declspec(thread) static iGlobal = 1
②在的线程函数中,就可以自由的访问或赋值,因该值是iGlobal的副本,其他线程对iGlobal的更改并不会影响到该线程的iGlobal的值。
(2)动态TLS实现:——每个线程创建时系统给它分配一个LPVOID指针的数组(TLS数组)
①这个数组从C编程角度是隐藏着的不能直接访问,需要通过一些API函数调用访问。
②TLS数据结构
A、线程本地存储器的位标志显示了该进程中所有运行的线程正在使用的一组标志。每个标志均可设置为FREE或者INUSE,表示TLS插槽(slot)是否正在使用。
B、为了使用动态TLS,我们首先调用TlsAlloc()来命令系统对进程的位标志进行扫描,找到一个可用的位置,并返回该索引。该索引值一般保存为全局变量(设为dwTlsIndex)或通过参数传递给线程函数。注意,当前线程实际上访问的是这个TLS数组索引变量的线程内的拷贝版本。也就说,不同线程虽然看起来用的是同名的TLS数组索引变量,但实际上各个线程得到的可能是不同DWORD值。
C、然后,可以调用TlsSetValue函数将对应的索引位保存一个特定的值,可以调用TlsGetValue()来返回该索引位的值。
★在这步中,通过GlobalAlloc()为当前线程动态分配一块内存区域,并将返回的指针通过TlsSetValue保存于对应的索引位置。
★在当前线程的任何函数内,都可以通过TLS数组的索引变量,使用TlsGetValue()函数得到上一步的那块内存区域的指针,然后就可以进行内存区域的读写操作了。
D、当所有线程都不需要保留TLS数组某个索引位的时候,应该调用TlsFree。
【Tls程序】
/*------------------------------------------------------------ THREADLOCALSTORAGE.C -- 线程本地存储程序 (c) Charles Petzold, 1998 ------------------------------------------------------------*/ #include <windows.h> #define THREADCOUNT 8 #define WM_USER_SHOW (WM_USER + 1) DWORD WINAPI ThreadProc(PVOID pVoid); LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); DWORD dwTlsIndex; CRITICAL_SECTION cs; typedef struct { HWND hwnd; PVOID pvData; DWORD dwThreadID; BOOL bComm; }PARAMS, *PPARAMS; int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT("TLS"); HWND hwnd; MSG msg; WNDCLASS wndclass; wndclass.style = CS_HREDRAW | CS_VREDRAW; wndclass.lpfnWndProc = WndProc; wndclass.cbClsExtra = 0; wndclass.cbWndExtra = 0; wndclass.hInstance = hInstance; wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION); wndclass.hCursor = LoadCursor(NULL, IDC_ARROW); wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); wndclass.lpszMenuName = NULL; wndclass.lpszClassName = szAppName; if (!RegisterClass(&wndclass)) { MessageBox(NULL, TEXT("This program requires Windows NT!"), szAppName, MB_ICONERROR); return 0; } hwnd = CreateWindow(szAppName, // window class name TEXT("Thread Local Storage"), // window caption WS_OVERLAPPEDWINDOW, // window style CW_USEDEFAULT, // initial x position CW_USEDEFAULT, // initial y position 400, // initial x size 300, // initial y size NULL, // parent window handle NULL, // window menu handle hInstance, // program instance handle NULL); // creation parameters ShowWindow(hwnd, iCmdShow); UpdateWindow(hwnd); while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } //普通函数 void CommonFunc(PPARAMS pParams) { PVOID pvData; //获取当前线程的pvData指针 pvData = TlsGetValue(dwTlsIndex); //if ((pvData == 0) && (GetLastError() != ERROR_SUCCESS)) // return; //获取当前线程的一些信息 pParams->dwThreadID = GetCurrentThreadId(); //保存线程ID pParams->pvData = pvData; //当前线程的TLS数组中的变量 pParams->bComm = TRUE; SendMessage(pParams->hwnd, WM_USER_SHOW, 0, 0); } DWORD WINAPI ThreadProc(PVOID pVoid) { PVOID pvData; PPARAMS pParams; EnterCriticalSection(&cs); pParams = (PPARAMS)pVoid; //初始化本线程的TLS索引 pvData = (PVOID)GlobalAlloc(GPTR, 256); //GPTR=LMEM_FIXED | GMEM_ZEROINIT //设置数值到TLS数组相应的索引位 if (!TlsSetValue(dwTlsIndex,pvData)) return 0; pParams->dwThreadID = GetCurrentThreadId(); pParams->pvData = pvData; pParams->bComm = FALSE; //在窗口中显示:线程函数中获得的线程ID和动态分配内存的地址 SendMessage(pParams->hwnd, WM_USER_SHOW, 0, 0); //在窗口中显示:普通函数中获得的线程ID和动态分配内存的地址 CommonFunc(pParams); LeaveCriticalSection(&cs); Sleep(1); //在线程返回前,释放动态分配的内存 pvData = TlsGetValue(dwTlsIndex); if (pvData !=0) GlobalFree((HGLOBAL)pvData); return 0; } LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { HDC hdc; PAINTSTRUCT ps; RECT rect; static int cxClient, cyClient, iLines, cxChar, cyChar, iCol; static HANDLE hThread[THREADCOUNT]; static PARAMS params; int i; TCHAR szBuffer[64]; switch (message) { case WM_CREATE: cxChar = LOWORD(GetDialogBaseUnits()); cyChar = HIWORD(GetDialogBaseUnits()); params.hwnd = hwnd; params.dwThreadID = 0; //分配TLS数组索引,并保存在全局变量中 if ((dwTlsIndex = TlsAlloc()) == TLS_OUT_OF_INDEXES) { return 0; } //初始化临界区对象 InitializeCriticalSection(&cs); //创建4个线程,立即执行(第5个参数为0),并共用一个线程函数。 for (i = 0; i < THREADCOUNT; i++) { hThread[i] = CreateThread(NULL, 0, ThreadProc, ¶ms, 0, NULL); if (hThread[i] == NULL) break; } iCol = -1; return 0; case WM_SIZE: cxClient = LOWORD(lParam); cyClient = HIWORD(lParam); return 0; case WM_USER_SHOW: if (iLines % 25 == 0) { iCol++; iLines = 0; } InvalidateRect(hwnd, NULL, FALSE); UpdateWindow(hwnd); iLines++; return 0; case WM_PAINT: hdc = BeginPaint(hwnd, &ps); GetClientRect(hwnd, &rect); if (params.dwThreadID>0) { if (params.bComm) { TextOut(hdc, iCol * 25 * cxChar, iLines*cyChar, szBuffer, wsprintf(szBuffer, TEXT("%d—Common:thread %d, lpvData = %lX"), iCol * 25 + iLines + 1, params.dwThreadID, params.pvData)); } else TextOut(hdc, iCol * 25 * cxChar, iLines*cyChar, szBuffer, wsprintf(szBuffer, TEXT("%d—Thread %d, lpvData = %lX"), iCol * 25 + iLines + 1, params.dwThreadID, params.pvData)); } EndPaint(hwnd, &ps); return 0; case WM_DESTROY: DeleteCriticalSection(&cs); TlsFree(dwTlsIndex); //释放索引 for (i = 0; i < THREADCOUNT; i++) CloseHandle(hThread[i]); PostQuitMessage(0); return 0; } return DefWindowProc(hwnd, message, wParam, lParam); }