本篇文章适合比较熟悉多线程并且想学习线程同步的读者。
最近由于使用多线程,不可避免的要用到线程之间的同步,对一些常用的windows 中同步函数和机制有了一些初步的了解,并且写了一些小例子来验证,当然其中难免有错误和疏漏之处,希望高手能给我这个小鸟指出不足之处,非常感谢。
目录
一 临界区
二 互斥体
三 事件
四 信号量
五 附录
一 临界区
临界区的使用在线程同步中应该算是比较简单,说它简单还是说它同后面讲到的其它方法相比更容易理解。举个简单的例子:比如说有一个全局变量(公共资源)两个线程都会对它进行写操作和读操作,如果我们在这里不加以控制,会产生意想不到的结果。假设线程A正在把全局变量加1然后打印在屏幕上,但是这时切换到线程B,线程B又把全局变量加1然后又切换到线程A,这时候线程A打印的结果就不是程序想要的结果,也就产生了错误。解决的办法就是设置一个区域,让线程A在操纵全局变量的时候进行加锁,线程B如果想操纵这个全局变量就要等待线程A释放这个锁,这个也就是临界区的概念。
使用方法:
CRITICAL_SECTION cs;
InitializeCriticalSection(&cs);
EnterCriticalSection(&cs);
...
LeaveCriticalSection(&cs);
DeleteCriticalSection(&cs);
#include "stdafx.h"
#include <windows.h>
#include <process.h>
#include <iostream>
using namespace std;
/****************************************************************
*在使用临界区的时候要注意,每一个共享资源就有一个CRITICAL_SECTION
*如果要一次访问多个共享变量,各个线程要保证访问的顺序一致,如果不
*一致,很可能发生死锁。例如:
* thread one:
* EnterCriticalSection(&c1)
* EnterCriticalSection(&c2)
* ...
* Leave...
* Leave...
*
* thread two:
* EnterCriticalSection(&c2);
* EnterCriticalSection(&c1);
* ...
* Leave...
* Leave...
*这样的情况就会发生死锁,应该让线程2进入临界区的顺序同线程1相同
****************************************************************/
const int MAX_THREADNUMS = 4; //产生线程数目
CRITICAL_SECTION cs; //临界区
HANDLE event[MAX_THREADNUMS]; //保存createevent的返回handle
int critical_value = 0; //共享资源
UINT WINAPI ThreadFunc(void* arg)
{
int thread = (int)arg;
for (int i = 0; i < 5; i++)
{
EnterCriticalSection(&cs);
cout << "thread " << thread << " ";
critical_value++;
cout << "critical_value = " << critical_value << endl;
LeaveCriticalSection(&cs);
}
SetEvent(event[thread]);
return 1;
}
int main(int argc, char* argv[])
{
cout << "this is a critical_section test program" << endl;
HANDLE hThread;
UINT uThreadID;
DWORD dwWaitRet = 0;
InitializeCriticalSection(&cs);
for (int i = 0; i < MAX_THREADNUMS; i++)
{
event[i] = CreateEvent(NULL, TRUE, FALSE, "");
if (event[i] == NULL)
{
cout << "create event " << i << " failed with code: "
<< GetLastError() << endl;
continue;
}
hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFunc,
(void*)i, 0, &uThreadID);
if (hThread == 0)
{
cout << "begin thread " << i << " failed with code: "
<< GetLastError() << endl;
continue;
}
CloseHandle(hThread);
}
//等待所有线程完成
dwWaitRet = WaitForMultipleObjects(MAX_THREADNUMS, event, TRUE, INFINITE);
switch(dwWaitRet)
{
case WAIT_OBJECT_0:
cout << "all the sub thread has exit!" << endl;
break;
default:
cout << "wait for all the thread failed with code:" << GetLastError() << endl;
break;
}
DeleteCriticalSection(&cs);
for (int k = 0; k < MAX_THREADNUMS; k++)
{
CloseHandle(event[k]);
}
return 0;
}
二 互斥体
windows api中提供了一个互斥体,功能上要比临界区强大。也许你要问,这个东东和临界区有什么区别,为什么强大?它们有以下几点不一致:
1.critical section是局部对象,而mutex是核心对象。因此像waitforsingleobject是不可以等待临界区的。
2.critical section是快速高效的,而mutex同其相比要慢很多
3.critical section使用范围是单一进程中的各个线程,而mutex由于可以有一个名字,因此它是可以应用于不同的进程,当然也可以应用于同一个进程中的不同线程。
4.critical section 无法检测到是否被某一个线程释放,而mutex在某一个线程结束之后会产生一个abandoned的信息。同时mutex只能被拥有它的线程释放。下面举两个应用mutex的例子,一个是程序只能运行一个实例,也就是说同一个程序如果已经运行了,就不能再运行了;另一个是关于非常经典的哲学家吃饭问题的例子。
程序运行单个实例:
#include "stdafx.h"
#include <windows.h>
#include <process.h>
#include <iostream>
using namespace std;
//当输入s或者c时候结束程序
void PrintInfo(HANDLE& h, char t)
{
char c;
while (1)
{
cin >> c;
if (c == t)
{
ReleaseMutex(h);
CloseHandle(h);
break;
}
Sleep(100);
}
}
int main(int argc, char* argv[])
{
//创建mutex,当已经程序发现已经有这个mutex时候,就相当于openmutex
HANDLE hHandle = CreateMutex(NULL, FALSE, "mutex_test");
if (GetLastError() == ERROR_ALREADY_EXISTS)
{
cout << "you had run this program!" << endl;
cout << "input c to close this window" << endl;
PrintInfo(hHandle, 'c');
return 1;
}
cout << "program run!" << endl;
cout << "input s to exit program" <<endl;
PrintInfo(hHandle, 's');
return 1;
}
哲学家吃饭问题:
const int PHILOSOPHERS = 5; //哲学家人数
const int TIME_EATING = 50; //吃饭需要的时间 毫秒
HANDLE event[PHILOSOPHERS]; //主线程同工作线程保持同步的句柄数组
HANDLE mutex[PHILOSOPHERS]; //mutex数组,这里相当于公共资源筷子
CRITICAL_SECTION cs; //控制打印的临界区变量
UINT WINAPI ThreadFunc(void* arg)
{
int num = (int)arg;
DWORD ret = 0;
while (1)
{
ret = WaitForMultipleObjects(2, mutex, TRUE, 1000);
if (ret == WAIT_TIMEOUT)
{
Sleep(100);
continue;
}
EnterCriticalSection(&cs);
cout << "philosopher " << num << " eatting" << endl;
LeaveCriticalSection(&cs);
Sleep(TIME_EATING);
break;
}
//设置时间为有信号
SetEvent(event[num]);
return 1;
}
int main(int argc, char* argv[])
{
HANDLE hThread;
InitializeCriticalSection(&cs);
//循环建立线程
for (int i = 0; i < PHILOSOPHERS; i++)
{
mutex[i] = CreateMutex(NULL, FALSE, "");
event[i] = CreateEvent(NULL, TRUE, FALSE, "");
hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFunc, (void*)i, 0, NULL);
if (hThread == 0)
{
cout << "create thread " << i << "failed with code: "
<< GetLastError() << endl;
DeleteCriticalSection(&cs);
return -1;
}
CloseHandle(hThread);
}
//等待所有的哲学家吃饭结束
DWORD ret = WaitForMultipleObjects(PHILOSOPHERS, event, TRUE, INFINITE);
if (ret == WAIT_OBJECT_0)
{
cout << "all the philosophers had a dinner!" << endl;
}
else
{
cout << "WaitForMultipleObjects failed with code: " << GetLastError() << endl;
}
DeleteCriticalSection(&cs);
for (int j = 0; j < PHILOSOPHERS; j++)
{
CloseHandle(mutex[j]);
}
return 1;
}
三 事件
事件对象的特点是它可以应用在重叠I/O(overlapped I/0)上,比如说socket编程中有两种模型,一种是重叠I/0,一种是完成端口都是可以使用事件同步。它也是核心对象,因此可以被waitforsingleobje这些函数等待;事件可以有名字,因此可以被其他进程开启。我在前几个例子当中其实已经使用到event了,在这里就不多说了,可以参考前一个例子。
四 信号量
semaphore的概念理解起来可能要比mutex还难, 我先简单说一下创建信号量的函数,因为我在开始使用的时候没有很快弄清楚,可能现在还有理解不对的地方,如果有错误还是请大侠多多指教。
CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // SD
LONG lInitialCount, // initial count
LONG lMaximumCount, // maximum count
LPCTSTR lpName // object name
)
第一个参数是安全性,可以使用默认的安全性选项NULL;第二个和第三个参数是两个long型的数值,它们表示什么含义呢?lMaxinumCount表示信号量的最大值,必须要大于零。比如是5就表示可以有5个进程或者线程使用,如果第六个进程或者线程想使用的话就必须进入等待队列等待有进程或者线程释放资源。lInitalCount表示信号量的初始值,应该大于或者等于零小于等于lMaximumCount。如果lInitialCount = 0 && lMaximumCount == 5,那么就表示当前资源已经全部被使用,如果再有进程或者线程想使用的话,信号量就会变成-1,该进程或者线程进入等待队列,直到有进程或者线程执行ReleaseMutex;如果lInitialCount = 5 && lMaximumCount == 5,那么就表示现在信号量可以被进程或者线程使用5次,再之后就要进行等待;如果InitialCount = 2 && MaximumCount == 5这样的用法不太常见,表示还可以调用两次CreateSemaphore或者OpenSemaphore,再调用的话就要进入等待状态。最后一个参数表示这个信号量的名字,这样就可以跨进程的时候通过这个名字OpenSemaphore。说了这么多了,不知道说明白没有
看个例子,popo现在好像在本机只能运行三个实例,我们在前面说的mutex可以让程序只是运行一个实例,下面我通过信号量机制让程序像popo一样运行三个实例。
#include "stdafx.h"
#include <windows.h>
#include <iostream>
using namespace std;
const int MAX_RUNNUM = 3; //最多运行实例个数
void PrintInfo()
{
char c;
cout << "run program" << endl;
cout << "input s to exit program!" << endl;
while (1)
{
cin >> c;
if (c == 's')
{
break;
}
Sleep(10);
}
}
int main(int argc, char* argv[])
{
HANDLE hSe = CreateSemaphore(NULL, MAX_RUNNUM, MAX_RUNNUM, "semaphore_test");
DWORD ret = 0;
if (hSe == NULL)
{
cout << "createsemaphore failed with code: " << GetLastError() << endl;
return -1;
}
ret = WaitForSingleObject(hSe, 1000);
if (ret == WAIT_TIMEOUT)
{
cout << "you have runned " << MAX_RUNNUM << " program!" << endl;
ret = WaitForSingleObject(hSe, INFINITE);
}
PrintInfo();
ReleaseSemaphore(hSe, 1, NULL);
CloseHandle(hSe);
return 0;
}
附录:
核心对象
Change notification
Console input
Event
Job
Mutex
Process
Semaphore
Thread
Waitable timer