1.线程不安全的原因
一个进程的4GB空间中可能有多个线程;
每一个线程都有自己的堆栈;
堆栈中放参数、局部变量等;
如果线程只使用参数和局部变量不会有线程安全问题;
当多个线程访问同一资源例如放在全局变量区中的全局变量时可能会有线程安全问题;
例如一个线程读另一个线程写或者两个线程都写同一个全局变量,最后得到的值可能并不是准确的;
线程A读全局变量x,此时x=10;
切换到线程B,B读到x=10;
B执行x+1并写出;此时x=11;
切换到线程A,A执行x+1并写出,此时由于之前A读到的是10,A从contex结构中得到x的值为10,而不是B修改后的11;x=10+1=11;
最后x=11,而不是预期的12;
2.如何解决线程安全问题
就像火车上的厕所一样,一次只能有一个人进去;
如果一个人进去,就锁上门,后面的人看到门锁了也就进不去了;
等第一个人使用完,就打开门锁;
在win32程序中,用临界区来解决线程安全的问题;
临界区相当于一个访问资源前要获取的令牌;
临界区结构:
typedef struct _RTL_CRITICAL_SECTION { PRTL_CRITICAL_SECTION_DEBUG DebugInfo; LONG LockCount; LONG RecursionCount; HANDLE OwningThread; HANDLE LockSemaphore; DWORD SpinCount; } RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;
LockCount:
它被初始化为数值 -1
此数值等于或大于 0 时,表示此临界区被占用
等待获得临界区的线程数:LockCount - (RecursionCount -1)
RecursionCount:
此字段包含所有者线程已经获得该临界区的次数
OwningThread:
此字段包含当前占用此临界区的线程的线程标识符
此线程 ID 与GetCurrentThreadId 所返回的 ID 相同
临界区的使用步骤://1、创建CRITICAL_SECTION: CRITICAL_SECTION cs;
//2、在使用前进行初始化 InitializeCriticalSection(&cs); //3、在函数中使用: DWORD WINAPI 线程A(PVOID pvParam) { EnterCriticalSection(&cs); //获取令牌 //对全局遍历X的操作 LeaveCriticalSection(&cs); //与上一函数配对使用,释放令牌 return(0); } DWORD WINAPI 线程B(PVOID pvParam) { EnterCriticalSection(&g_cs); //对全局遍历X的操作 LeaveCriticalSection(&g_cs); return(0); } //4、删除CRITICAL_SECTION VOID DeleteCriticalSection(PCRITICAL_SECTION pcs); //当线程不再试图访问共享资源时
3.使用临界区的实例
代码:
#include "stdio.h" #include <windows.h> CRITICAL_SECTION cs; DWORD WINAPI ThreadProc1(LPVOID lpParameter) { for(int x=0;x<1000;x++) { EnterCriticalSection(&cs); Sleep(1000); printf("11111:%x %x %x ",cs.LockCount,cs.RecursionCount,cs.OwningThread); LeaveCriticalSection(&cs); } return 0; } DWORD WINAPI ThreadProc2(LPVOID lpParameter) { for(int x=0;x<1000;x++) { EnterCriticalSection(&cs); Sleep(1000); printf("22222:%x %x %x ",cs.LockCount,cs.RecursionCount,cs.OwningThread); LeaveCriticalSection(&cs); } return 0; } DWORD WINAPI ThreadProc3(LPVOID lpParameter) { for(int x=0;x<1000;x++) { EnterCriticalSection(&cs); Sleep(1000); printf("33333:%x %x %x ",cs.LockCount,cs.RecursionCount,cs.OwningThread); LeaveCriticalSection(&cs); } return 0; } DWORD WINAPI ThreadProc4(LPVOID lpParameter) { for(int x=0;x<1000;x++) { EnterCriticalSection(&cs); Sleep(1000); printf("44444:%x %x %x ",cs.LockCount,cs.RecursionCount,cs.OwningThread); LeaveCriticalSection(&cs); } return 0; } int main(int argc, char* argv[]) { InitializeCriticalSection(&cs); //printf("主线程:%x %x %x ",cs.LockCount,cs.RecursionCount,cs.OwningThread); //创建一个新的线程 HANDLE hThread1 = ::CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL); //创建一个新的线程 HANDLE hThread2 = ::CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL); //创建一个新的线程 HANDLE hThread3 = ::CreateThread(NULL, 0, ThreadProc3, NULL, 0, NULL); //创建一个新的线程 HANDLE hThread4 = ::CreateThread(NULL, 0, ThreadProc4, NULL, 0, NULL); //如果不在其他的地方引用它 关闭句柄 /** ::CloseHandle(hThread1); ::CloseHandle(hThread2); ::CloseHandle(hThread3); ::CloseHandle(hThread4); **/ Sleep(1000*60*60); return 0; }
遇到的坑:
1】始终只有一个线程在运行
原因:因为线程的时间片还没完,(线程每次分配的时间片大概20ms)又会重新进入临界区
解决:在每次离开临界区的LeaveCriticalSection(&cs)后面在Sleep(10);
2】LockCount的值小于-1;
In Microsoft Windows Server 2003 Service Pack 1 and later versions of Windows, the LockCount field is parsed as follows: The lowest bit shows the lock status. If this bit is 0, the critical section is locked; if it is 1, the critical section is not locked. The next bit shows whether a thread has been woken for this lock. If this bit is 0, then a thread has been woken for this lock; if it is 1, no thread has been woken. The remaining bits are the ones-complement of the number of threads waiting for the lock.
大概是说在windows2003之后LockCount的意义发生了改变;
最后一位0表示临界区被锁,1表示未锁,倒数第二位用0和1表示是否有线程被唤醒,剩下的几位你取反所得的十进制的值就等待的线程数;
4.可能的坑
场景一:
DWORD WINAPI 线程A(PVOID pvParam) { EnterCriticalSection(&cs); while(g_nIndex < MAX_TIMES) { //对全局遍历X的操作 } LeaveCriticalSection(&cs); return(0); } DWORD WINAPI 线程B(PVOID pvParam) { EnterCriticalSection(&cs); while(g_nIndex < MAX_TIMES) { //对全局遍历X的操作 } LeaveCriticalSection(&cs); return(0); }
两个线程操作同一个全局变量;
但是当一个线程功能执行完后才释放临界区,导致实际上相当于只有一个线程,失去了多线程的优势;
场景二:
DWORD WINAPI 线程A(PVOID pvParam) { EnterCriticalSection(&cs); //代码xxxxxx //代码xxxxxx //对全局遍历X的操作 //代码xxxxxx //代码xxxxxx LeaveCriticalSection(&cs); } DWORD WINAPI 线程A(PVOID pvParam) { EnterCriticalSection(&cs); //代码xxxxxx //代码xxxxxx //对全局遍历X的操作 //代码xxxxxx //代码xxxxxx LeaveCriticalSection(&cs); } 把不必要的代码放在了同步区;失去了多线程的优势; 场景三: DWORD WINAPI 线程A(PVOID pvParam) { //代码xxxxxx //代码xxxxxx EnterCriticalSection(&cs); //对全局遍历X的操作 LeaveCriticalSection(&cs); //代码xxxxxx //代码xxxxxx } DWORD WINAPI 线程A(PVOID pvParam) { //代码xxxxxx //代码xxxxxx //对全局遍历X的操作 //代码xxxxxx //代码xxxxxx }
只有一个线程被令牌限制,另一个线程没被限制;
相当于,一个人在排队等厕所,另一个人不管又没人直接进去了;
5.多个临界区
全局变量X 全局变量Y 全局变量Z 线程1 DWORD WINAPI ThreadFunc(PVOID pvParam) { EnterCriticalSection(&g_cs); 使用X 使用Y LeaveCriticalSection(&g_cs); return(0); } 线程2 DWORD WINAPI ThreadFunc(PVOID pvParam) { EnterCriticalSection(&g_cs); 使用X 使用Z LeaveCriticalSection(&g_cs); return(0); } 线程3 DWORD WINAPI ThreadFunc(PVOID pvParam) { EnterCriticalSection(&g_cs); 使用Y 使用X LeaveCriticalSection(&g_cs); return(0); } //效率差,随便修改一个全局变量,其它线程都不能改不相关的其它全局变量了 解决方案: CRITICAL_SECTION g_csX; CRITICAL_SECTION g_csY; CRITICAL_SECTION g_csZ; 线程1 DWORD WINAPI ThreadFunc(PVOID pvParam) { EnterCriticalSection(&g_csX); 使用X LeaveCriticalSection(&g_csX); EnterCriticalSection(&g_csY); 使用Y LeaveCriticalSection(&g_csY); return(0); } 线程2 DWORD WINAPI ThreadFunc(PVOID pvParam) { EnterCriticalSection(&g_csX); 使用X LeaveCriticalSection(&g_csX); EnterCriticalSection(&g_csZ); 使用Z LeaveCriticalSection(&g_csZ); return(0); } 线程3 DWORD WINAPI ThreadFunc(PVOID pvParam) { EnterCriticalSection(&g_csX); 使用X LeaveCriticalSection(&g_csX); return(0); }
使用多个临界区还可能导致死锁的问题;
大概就是线程A要同时获得令牌a、b;
线程B也要同时获得令牌a、b;
当A获取了a,需要获取b;此时B获取了b要获取a;
导致两个线程都无法继续执行下去,并且都无法释放已获得的令牌;然后程序卡死了;