线程同步
多线程实现线程同步有三种方式:互斥对象、事件对象和关键代码段。
利用互斥对象实现线程同步
主要函数:
- CreateMutex:创建互斥对象
- WaitForSingleObject:请求对象的使用权
- ReleaseMutex:释放互斥对象的所有权
示例代码如下:
#include<windows.h>
#include<iostream.h>
DWORD WINAPI Fun1Proc(LPVOID lpParameter); //新线程1的入口函数声明,函数的名称Fun1Proc可任意取
DWORD WINAPI Fun2Proc(LPVOID lpParameter); //新线程1的入口函数声明
int tickets = 100;
HANDLE hMutex;
void main()
{
HANDLE hThread1;
HANDLE hThread2;
//参数2:TRUE表示创建互斥对象的线程获取对象的所有权;3:互斥对象的名称,NULL表示匿名
hMutex = CreateMutex(NULL,FALSE,NULL);
/*参数1:NULL表示使用默认的安全性;2:线程初始栈的大小,0表示默认使用与调用该函数的线程相同的栈空间大小;
3:新线程的入口函数;4:可通过这个参数给新线程传递参数;5:0表示线程创建后立即运行,CREATE_SUSPENDED表示线程
创建后处于暂停状态,需要调用ResumeThread运行;6:用来接收线程ID*/
hThread1 = CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
hThread2 = CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);
/*CloseHandle关闭对新创建的线程的引用,并不是终止新线程。还会递减线程内核对象的使用计数,当使用计数为0时,系统就会释放该线程内核对象,
如果不关闭线程句柄,只有等到进程终止时,系统才会清理这些对象*/
CloseHandle(hThread1);
CloseHandle(hThread2);
cout<<"main thread is running"<<endl;
Sleep(4000);
}
DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
while(1)
{
/*请求共享对象的使用权。参数1:请求的对象的句柄,如果互斥对象处于有信号状态,就返回,否则一直等待;
参数2:等待的时间,如果指定的时间已过,请求的对象仍然无信号,则返回,参数为0表示测试该对象的状态立即返回,参数为INFINITE表示永远等待*/
WaitForSingleObject(hMutex,INFINITE);
if(tickets>0){
Sleep(1);
cout<<"thread1 sell "<<tickets--<<endl;
}else{
break;
}
//释放指定对象的所有权
ReleaseMutex(hMutex);
}
return 0;
}
DWORD WINAPI Fun2Proc(LPVOID lpParameter)
{
while(1)
{
WaitForSingleObject(hMutex,INFINITE);
if(tickets>0){
Sleep(1);
cout<<"thread2 sell "<<tickets--<<endl;
}else{
break;
}
ReleaseMutex(hMutex);
}
return 0;
}
注意点:
- 获取到互斥对象的使用权后,要保护的代码操作完成后,要调用ReleaseMutex释放当前线程对互斥对象的使用权,这个时候操作系统会将该线程ID设置为0,然后设置互斥对象为有信号状态,其他线程才有机会获得互斥对象的使用权。谁拥有互斥对象,谁就要释放它。
- 如果当前线程已经获得了互斥对象的使用权,又调用了WaitForSingleObject请求对象的使用权,因为请求的线程的ID和互斥对象当前所有者的线程ID是相同的,所以可以请求到这个互斥对象。如果多次在同一个线程中请求同一个互斥对象,就要相应地多次调用ReleaseMutex释放互斥对象。
- 如果一个线程获得了互斥对象的使用权,但是没有释放它,那么当线程执行完毕时,操作系统会将互斥对象的线程ID置为0,引用计数置为0,其他线程就可以得到互斥对象的所有权了。
利用事件对象实现线程同步
主要函数:
- CreateEvent:创建事件对象。其中第2个参数指定是人工重置事件对象还是自动重置事件对象。TRUE表示人工,当线程获取到事件对象的所有权后,需要手动调用ResetEvent将事件对象设置为无信号状态;FALSE表示自动,当线程获取到事件对象的所有权后,系统会自动将该对象设置为无信号状态。
- SetEvent:设置事件对象为有信号状态
- ResetEvent:设置事件对象为无信号状态
示例代码如下:
#include<windows.h>
#include<iostream.h>
DWORD WINAPI Fun1Proc(LPVOID lpParameter);
DWORD WINAPI Fun2Proc(LPVOID lpParameter);
int tickets = 100;
HANDLE hEvent;
void main()
{
HANDLE hThread1;
HANDLE hThread2;
//创建事件对象。参数2:TRUE表示是人工重置事件对象,FALSE表示自动重置事件对象;3:指定事件对象的初始状态,TRUE表示有信号,反之;4:事件对象的名字,NULL表示匿名
hEvent = CreateEvent(NULL,FALSE,TRUE,NULL);
hThread1 = CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
hThread2 = CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);
CloseHandle(hThread1);
CloseHandle(hThread2);
cout<<"main thread is running"<<endl;
Sleep(4000);
}
DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
while(1)
{
//申请事件对象的所有权
WaitForSingleObject(hEvent,INFINITE);
if(tickets>0){
Sleep(1);
cout<<"thread1 sell "<<tickets--<<endl;
}else{
break;
}
//设置事件对象为有信号状态
SetEvent(hEvent);
}
return 0;
}
DWORD WINAPI Fun2Proc(LPVOID lpParameter)
{
while(1)
{
WaitForSingleObject(hEvent,INFINITE);
if(tickets>0){
Sleep(1);
cout<<"thread2 sell "<<tickets--<<endl;
}else{
break;
}
SetEvent(hEvent);
}
return 0;
}
注意点:
- 如果使用人工重置事件对象,创建事件对象和设置事件对象为无信号状态,是分两步进行的,所以很可能当线程一刚获取到事件对象时,线程一的时间片终止了,由于此时线程一还没来得及将事件对象设置为无信号状态,所以线程二又能获取到事件对象的所有权。因此线程同步不应该使用人工重置事件对象。
利用关键代码段实现线程同步
主要函数:
- InitializeCriticalSection:初始化关键代码段–(建造一个电话亭)
- EnterCriticalSection:获取临界区对象的所有权–(进入电话亭使用,其他人不允许再进入)
- LeaveCriticalSection:释放临界区对象的所有权–(离开电话亭,其他人可以进入)
- DeleteCriticalSection:释放一个没有被任何线程所拥有的临界区对象的所有资源(拆除电话亭)
示例代码如下:
#include<windows.h>
#include<iostream.h>
DWORD WINAPI Fun1Proc(LPVOID lpParameter);
DWORD WINAPI Fun2Proc(LPVOID lpParameter);
int tickets = 100;
CRITICAL_SECTION g_cs;
void main()
{
HANDLE hThread1;
HANDLE hThread2;
hThread1 = CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
hThread2 = CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);
CloseHandle(hThread1);
CloseHandle(hThread2);
//初始化关键代码段--相当于建造一个电话亭
InitializeCriticalSection(&g_cs);
cout<<"main thread is running"<<endl;
Sleep(4000);
DeleteCriticalSection(&g_cs);
}
DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
while(1)
{
EnterCriticalSection(&g_cs);
if(tickets>0){
Sleep(1);
cout<<"thread1 sell "<<tickets--<<endl;
}else{
break;
}
LeaveCriticalSection(&g_cs);
}
return 0;
}
DWORD WINAPI Fun2Proc(LPVOID lpParameter)
{
while(1)
{
EnterCriticalSection(&g_cs);
if(tickets>0){
Sleep(1);
cout<<"thread2 sell "<<tickets--<<endl;
}else{
break;
}
LeaveCriticalSection(&g_cs);
}
return 0;
}
注意点:
- 如果一个线程获取了临界区对象的使用权限,并且忘记释放了,尽管该线程执行完毕了,其他线程也没有机会获取临界区对象的使用权限了,只能一直等待。直到进程退出,线程也退出了。
线程死锁
在编写多线程同步时,很容易发生线程死锁。哲学家进餐问题可以形象的描述线程死锁现象:有多位哲学家在一起吃饭,每个人只有一根筷子,无法吃饭。大家都希望其他哲学家可以交出筷子让自己先吃饭,最后只能干瞪眼盯着桌上的美食挨饿。
在多线程中,如果线程1拥有临界区对象A,并且等待获取临界区对象B,而线程2拥有临界对象B,并且还在等待获取临界对象A,就造成了死锁。