大名鼎鼎的线程同步!
(一)线程同步之互斥对象:
互斥对象(mutex)属于内核对象,它能够确保线程拥有对单个资源的互斥访问权,即线程1访问一个变量时,其余线程无法对这个变量进行访问,直至线程1结束它的访问。
互斥对象包含一个使用数量、一个线程id和一个计数器。其中线程id用于标识系统中哪个线程拥有互斥对象,在创建线程的函数中最后一个参数即为线程id;计数器用于指明该线程拥有互斥对象的次数。
创建互斥对象需调用CreatMutex函数,创建成功则返回所创建的互斥对象的句柄,创建失败返回NULL。
请求互斥对象所有权,调用WaitForSingleObject函数,线程必须主动请求共享对象的所有权才能获得所有权。
释放指定互斥对象的所有权,调用ReleaseMutex函数,线程访问共享资源结束后要主动释放互斥对象的所有权,使该对象处于已通知状态。
CreatMutex(
//参数一:安全属性 一般为NULL
//参数二:一般为TRUE,代表立即拥有互斥体
//参数三:指向互斥对象名的指针,也可以写为NULL
)
举例如下,num为奇数时让其-1,为偶数时让其+1。如果没有互斥对象,结果很难为零,因为很有可能在自减线程对num没有访问完成的时候,自增线程已访问多次,而添加互斥对象后则可保证在同一时间内仅有一个线程在访问num。
long long num=0;
HANDLE hMutex;//定义一个互斥量的句柄
unsigned threadInc(void* arg) //num+1的函数
{
WaitForSingleObject(hMutex,INFINITE);
for(int i=0;i<500;i++) num+=1;
ReleaseMutex(hMutex);
return num;
}
unsigned threadDel(void* arg) //num-1的函数
{
WaitForSingleObject(hMutex,INFINITE);
for(int i=0;i<500;i++) num-=1;
ReleaseMutex(hMutex);
return num;
}
int main()
{
hMutex=CreateMutex(NULL,FALSE,NULL);
for(int i=0;i<;i++)
{
if(i%2==0)
tHandles[i]=(HANDLE)_beginthreadex(NULL,0,threadInc,NULL,0,NULL);
else
tHandles[i]=(HANDLE)_beginthreadex(NULL,0,threadDel,NULL,0,NULL);
}
}
(二)线程同步之事件对象:
互斥对象可理解为两个线程对同一变量的操作但这两个线程之间并没有什么实际性的关联,各干各的,而事件对象则是多个线程同步执行并且每个线程的执行结果都会对最终结果产生影响,比如卖票 好几个窗口不可能各卖各的,它们的卖票结果是实时都同步着的,因为不可能同一号码的票卖给多个人。
可比喻为互斥对象是自己一个人去上厕所,你进厕所之后就把门锁上不让别人进来就可以了,而事件对象则是好几个人一起去上厕所,它们同时都想上厕所,但要一个接一个的上,出来一个之后就告诉下一个“你可以去了”,然后就这样都上完了之后在一起离开
CreateEvent(
//参数一:安全属性 一般为NULL
//参数二:False:事件对象控制一次后将立即重置(暂停); True:可手动暂停
//参数三:False:对象建立后控制为暂停状态; True:可运行状态
//参数四:对象名称,NULL为无名的事件对象
)
第二个参数为TRUE时,可随时"启动运行"(SetEvent)和"暂停运行"(ResetEvent)。
(三)线程同步之信号量:
信号量相比前两种,算是比较抽象的了,但应用的也不是很多。
使用信号量要先关注内核对象的状态:
- 触发状态(有信号状态),表示有可用资源
- 未触发状态(无信号状态),表示没有可用资源
简单通俗的讲一下信号量的工作原理,以停车场为例:
- 假设停车场只有三个车位,开始时三个车位都是空的,此时来了五辆车,门卫只能允许前三辆不受阻碍的进入,剩下的车则需要排队等待,并且后面如果再来车也要排队等待。
- 此时有一辆车驶出停车场,门卫看到后便放了一辆车进来。如此往复
- 另一种情况,虽然有三个车位,但有一个不对外开放,所以此时的门卫就必须要保证同一时间内最多有两辆车停在停车场里,其余的都要在外排队等候,直至里面有对外开放且空余的车位。
在这一过程中,每辆车就相当于一个个的线程,而信号量扮演着门卫的角色,即信号量在限制可活动的线程数。
信号量的组成:
- 计数器:该内核对象被使用的次数
- 最大资源数量:标识信号量可以控制的最大资源数量,即停车场里的三个停车位
- 当前资源数量:表示当前可用资源数量,注意:并不是剩下的资源数量,因为只有开放的资源才能被线程申请,即停车场有三个车位,但只有两个对外开放
信号量的规则:
- 如果当前资源计数大于0,那么信号量处于有信号状态
- 如果当前资源计数等于0,那么信号量处于无信号状态
- 系统绝不会让当前资源计数变为负数
- 当前资源计数绝不会大于最大资源数
信号量与互斥量不同之处在于:同一时刻,互斥量只允许有一个线程访问资源,而信号量可允许多个,但信号量也需要限制同一时刻的最大线程数目。
1、创建信号量
CreateSemaphoreW(
//参数一:安全属性,一般为NULL
//参数二:初始化时,有多少可用资源。若为0表示无可用资源
//参数三:能够处理的最大资源数量
//参数四:信号量名称,一般为NULL
)
2、增加信号量
ReleaseSemaphoreW(
//参数一:信号量的句柄
//参数二:要增加几个
//参数三:当前信号量的资源计数原始值
)
(四)线程同步之关键代码段:
前面几种方式都是内核态的线程同步,而关键代码段是用户态的线程同步方式。
关键代码段,也称为临界区,工作在用户方式下,是一个小代码段,在代码执行前它必须独占对某些资源的访问权。通常把多线程中访问同一资源的那部分代码作为关键代码段。
——初始化关键代码段:调用InitializeCriticalSection函数初始化一个关键代码段
InitializeCriticalSection(
//参数:一个指向CRITICAL_SECTION结构体的指针
)
在调用InitializeCriticalSection函数前,需要构造一个CRITICAL_SECTION结构体类型的对象,然后将该对象的地址传给InitializeCriticalSection函数。
——进入关键代码段:调用EnterCriticalSection函数
EnterCriticalSection(
//参数:一个指向CRITICAL_SECTION结构体的指针
)
调用EnterCriticalSection函数,以获得指定临界区对象的所有权,该函数等待指定的临界区对象的所有权,如果所有权赋予了调用线程,则该函数就返回,否则该函数一直等待,从而导致线程等待
——退出关键代码段:调用LeaveCriticalSection函数
LeaveCriticalSection(
//参数:一个指向CRITICAL_SECTION结构体的指针
)
线程使用完临界区的资源后,需要调用LeaveCriticalSection函数释放指定的临界区对象的所有权,之后,其他想要获得该临界区对象所有权的线程就可以获得该所有权了,从而进入关键代码段,访问保护的资源
——删除临界区:调用DeleteCriticalSection函数
DeleteCriticalSection(
//参数:一个指向CRITICAL_SECTION结构体的指针
)
当临界区不再需要时,可以调用DeleteCriticalSection函数释放该对象,该函数将释放一个没有被任何线程所拥有的临界区对象的所有资源
(五)几种线程同步方式的比较:
windows线程同步的方式主要有四种:互斥对象、事件对象、信号量以及关键代码段。
互斥对象、事件对象、信号量属于内核对象,利用内核对象进行线程同步时,速度较慢,但利用这些内核对象可以实现多个进程间的线程同步。
关键代码段在用户态下工作,同步的速度比较快,但在使用的时候很容易进入死锁状态,因为在等待进入关键代码段时无法设定超时值。
通常在编写多线程程序并需要实现线程同步时,首选关键代码段,由于它使用比较简单,在MFC程序中使用的话,可以在类的构造函数中调用InitializeCriticalSection函数,在析构函数中调用DeleteCriticalSection函数,在所需要保护的代码前调用EnterCriticalSection函数,访问完所需保护的资源后调用LeaveCriticalSection函数。
但使用关键代码段也要注意:
——调用EnterCriticalSection函数就要相对应的调用LeaveCriticalSection函数,否则其他等待该临界区对象所有权的线程将无法执行
——如果访问关键代码段时,使用了多个临界区对象,就要注意防止死锁的发生。另外如果需要在多个进程间实现线程同步的话,可以使用另外三种同步方式