第三章--Win32程序的执行单元(部分概念及代码讲解)(中-线程同步
学习《Windows程序设计》记录
概念贴士:
1. 同步可以保证在一个时间内只有一个线程对其共享资源有控制权。PS:共享资源包括全局变量、公共数据成员或者句柄等。
2. 临界区内核对象和时间内核对象可以很好地用于多线程同步和它们之间的通信。
3. 线程同步必要性:当多个线程在同一个进程中执行时,可能有不止一个线程同时执行同一段代码,访问同一段内存中的数据。多个线程同时读取共享数据没有问题,但是如果同时读写,情况就不同,也许会产生极大的错误。(如:程序CountErr)。解决同步问题,就是保证整个存取过程的独占性。
4. 临界区对象是定义在数据段中的一个CRITICAL_SECTION结构,Windows内部使用这个结构记录一些信息,确保在同一时间段只有一个线程访问该数据段中的数据。(临界区又叫关键代码段)。
5. 临界区的使用步骤:编程时,要把临界区对象定义在想保护的数据段中,然后在任何线程使用此临界区对象之前对它进行初始化。之后,线程访问临界区中的数据时,必须先调用EnterCriticalSection函数,申请进入临界区。当操作完成时,还要将临界区交还给Windows,以便其他线程可以申请使用。这一步由LeaveCriticalSection函数完成。当程序不再使用临界区对象时,必须使用DeleteCriticalSection函数将它删除。(如:程序CriticalSection)
6. 同一时间内,Windows只允许一个线程进入临界区。所以在申请的时候,如果有另一个线程在临界区的话,EnterCriticalSection函数会一直等待下去,直到其他线程离开临界区才返回。
7. 临界区对象能够很好地保护共享数据,但是它不能够用于进程之间资源的锁定,因为它不是内核对象。如果要在进程之间维持线程的同步必须使用事件内核对象。
8. 互锁函数为同步访问多线程共享变量提供了一个简单的机制。如果变量在共享内存,不同进程的线程也可以使用此机制。
9. 多线程程序设计大多会涉及线程间相互通信。使用编程就要涉及线程的问题。
10. 事件对象是一种抽象的对象,它也有未受信和受信两种状态,编程人员也可以使用WaitForSingleObject函数等待其变成受信状态。不同于其他内核对象的是一些函数可以使事件对象在这两种状态之间转化。
11. 如果想使用事件对象,首先需要调用CreateEvent函数去创建它。
12. 当一个人工重置的事件对象受信之后,所有等待在这个事件上的线程都会变成可调度状态;可是当一个自动重置的时间对象受信以后,Windows仅允许一个等待在该事件上的线程变成可调度状态,然后就自动重置此事件对象为未守信状态。
13. 为事件对象命名是为了在其他地方(如:其他进程的线程中)使用OpenEvent或CreateEvent函数获取此内核对象的句柄。系统创建或打开一个事件内核对象后,会返回事件的句柄。当变成人员不适用此内核对象的时候,应该调用CloseHandle函数释放它占用的资源。
14. 事件对象主要用于线程间通信,因为它是一个内核对象,所以也可以跨进程使用。
15. 线程局部存储(Thread Local Storage,TLS)是一个使用方便的存储线程局部数据的系统。利用TLS机制可以为进程中所有的线程关于关联若干个数据,各个线程通过由TLS分配的全局索引来访问与自己关联的数据。
16. Microsoft保证至少有TLS_MINIMUM_AVAILABLE(定义在WinNT.h文件中)个标志位可用。
17. 动态使用TLS典型步骤如下:(如:程序UseTLS)
1)主线程调用TlsAlloc函数为线程局部存储分配索引。TlsAlloc的返回值就是数组的一个下标(索引)。这个位数组的唯一用途就是记忆哪一个下标在使用中。成员初始值均为FREE。当调用TlsAlloc时,系统会检查数组中数据,直到找到一个值为FREE的成员。将其值改为INUSE后,TlsAlloc函数返回该成员的索引。
2)每个线程调用TlsSetValue和TlsGetValue设置或读取线程数组中的值。调用TlsSetValu函数,一个线程只能改变自己线程数组中成员的值,不可以为其他线程设置TLS值。到现在为止,将数据从一个线程传到另一个线程的唯一办法是在创建线程时使用线程函数的参数。TlsSetValue和TlsSetValue分别用于设置和取得线程数组中的特定成员的值,而它们使用的索引就是TlsAlloc函数的返回值。
3)主线程调用TlsFree释放局部存储索引。函数的唯一参数是TlsAlloc返回的索引。
18. 一般情况下,为各线程分配TLS索引的工作要在主线程中完成,而分配的索引值应该保存在全局变量中,以方便各线程访问。
19. 用于线程同步的内核对象还有互斥体和信号量。不作介绍。
代码解释:
1.CountErr
PS:程序中多个线程执行任务,可以看出多个线程共享数据,产生的问题。
1 #include <stdio.h> 2 #include <windows.h> 3 #include <process.h> 4 5 int g_nCount1 = 0; 6 int g_nCount2 = 0; 7 BOOL g_bContinue = TRUE; 8 9 UINT __stdcall ThreadFunc(LPVOID); 10 11 int main(int argc, char* argv[]) 12 { 13 UINT uId; 14 HANDLE h[2]; 15 16 h[0] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId); 17 h[1] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId); 18 19 20 // 等待1秒后通知两个计数线程结束,关闭句柄 21 Sleep(1000); 22 g_bContinue = FALSE; 23 ::WaitForMultipleObjects(2, h, TRUE, INFINITE); 24 ::CloseHandle(h[0]); 25 ::CloseHandle(h[1]); 26 27 printf("g_nCount1 = %d \n", g_nCount1); 28 printf("g_nCount2 = %d \n", g_nCount2); 29 30 return 0; 31 } 32 33 UINT __stdcall ThreadFunc(LPVOID) 34 { 35 while(g_bContinue) 36 { 37 g_nCount1++; 38 g_nCount2++; 39 } 40 return 0; 41 }
2.CriticalSection
PS:通过使用临界区对象来改写之前存在同步问题的程序。
1 #include <stdio.h> 2 #include <windows.h> 3 #include <process.h> 4 5 BOOL g_bContinue = TRUE; 6 int g_nCount1 = 0; 7 int g_nCount2 = 0; 8 CRITICAL_SECTION g_cs; // 对存在同步问题的代码段使用临界区对象 9 10 UINT __stdcall ThreadFunc(LPVOID); 11 12 int main(int argc, char* argv[]) 13 { 14 UINT uId; 15 HANDLE h[2]; 16 17 // 初始化临界区对象 18 ::InitializeCriticalSection(&g_cs); 19 20 h[0] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId); 21 h[1] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId); 22 23 // 等待1秒后通知两个计数线程结束,关闭句柄 24 Sleep(1000); 25 g_bContinue = FALSE; 26 ::WaitForMultipleObjects(2, h, TRUE, INFINITE); 27 ::CloseHandle(h[0]); 28 ::CloseHandle(h[1]); 29 30 // 删除临界区对象 31 ::DeleteCriticalSection(&g_cs); 32 33 printf("g_nCount1 = %d \n", g_nCount1); 34 printf("g_nCount2 = %d \n", g_nCount2); 35 36 return 0; 37 } 38 39 UINT __stdcall ThreadFunc(LPVOID) 40 { 41 while(g_bContinue) 42 { 43 ::EnterCriticalSection(&g_cs); 44 g_nCount1++; 45 g_nCount2++; 46 ::LeaveCriticalSection(&g_cs); 47 } 48 return 0; 49 }
3.EventDemo
PS:主线程通过将事件状态设置为”受信来通知它的子线程开始工作。
1 #include <stdio.h> 2 #include <windows.h> 3 #include <process.h> 4 5 HANDLE g_hEvent; 6 UINT __stdcall ChildFunc(LPVOID); 7 8 int main(int argc, char* argv[]) 9 { 10 HANDLE hChildThread; 11 UINT uId; 12 13 // 创建一个自动重置的(auto-reset events),未受信的(nonsignaled)事件内核对象 14 g_hEvent = ::CreateEvent(NULL, FALSE, FALSE, NULL); 15 16 hChildThread = (HANDLE)::_beginthreadex(NULL, 0, ChildFunc, NULL, 0, &uId); 17 18 // 通知子线程开始工作 19 printf("Please input a char to tell the Child Thread to work: \n"); 20 getchar(); 21 ::SetEvent(g_hEvent); 22 23 // 等待子线程完成工作,释放资源 24 ::WaitForSingleObject(hChildThread, INFINITE); 25 printf("All the work has been finished. \n"); 26 ::CloseHandle(hChildThread); 27 ::CloseHandle(g_hEvent); 28 return 0; 29 } 30 31 UINT __stdcall ChildFunc(LPVOID) 32 { 33 ::WaitForSingleObject(g_hEvent, INFINITE); 34 printf(" Child thread is working...... \n"); 35 ::Sleep(5*1000); // 暂停5秒,模拟真正的工作 36 return 0; 37 }
4.UseTLS
PS:通过TLS将每个线程的创建时间与线程关联起来,从而可以在线程终止时得到线程的生命周期。
1 #include <stdio.h> 2 #include <windows.h> 3 #include <process.h> 4 5 // 利用TLS记录线程的运行时间 6 7 DWORD g_tlsUsedTime; 8 void InitStartTime(); 9 DWORD GetUsedTime(); 10 11 12 UINT __stdcall ThreadFunc(LPVOID) 13 { 14 int i; 15 16 // 初始化开始时间 17 InitStartTime(); 18 19 // 模拟长时间工作 20 i = 10000*10000; 21 while(i--) { } 22 23 // 打印出本线程运行的时间 24 printf(" This thread is coming to end. Thread ID: %-5d, Used Time: %d \n", 25 ::GetCurrentThreadId(), GetUsedTime()); 26 return 0; 27 } 28 29 int main(int argc, char* argv[]) 30 { 31 UINT uId; 32 int i; 33 HANDLE h[10]; 34 35 // 通过在进程位数组中申请一个索引,初始化线程运行时间记录系统 36 g_tlsUsedTime = ::TlsAlloc(); 37 38 // 令十个线程同时运行,并等待它们各自的输出结果 39 for(i=0; i<10; i++) 40 { 41 h[i] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId); 42 } 43 for(i=0; i<10; i++) 44 { 45 ::WaitForSingleObject(h[i], INFINITE); 46 ::CloseHandle(h[i]); 47 } 48 49 // 通过释放线程局部存储索引,释放时间记录系统占用的资源 50 ::TlsFree(g_tlsUsedTime); 51 return 0; 52 } 53 54 // 初始化线程的开始时间 55 void InitStartTime() 56 { 57 // 获得当前时间,将线程的创建时间与线程对象相关联 58 DWORD dwStart = ::GetTickCount(); 59 ::TlsSetValue(g_tlsUsedTime, (LPVOID)dwStart); 60 } 61 62 // 取得一个线程已经运行的时间 63 DWORD GetUsedTime() 64 { 65 // 获得当前时间,返回当前时间和线程创建时间的差值 66 DWORD dwElapsed = ::GetTickCount(); 67 dwElapsed = dwElapsed - (DWORD)::TlsGetValue(g_tlsUsedTime); 68 return dwElapsed; 69 }