Win32中临界区和互斥体的机制是为了解决不同线程或进程访问同一共享资源产生错误的问题。
1. 临界区Critical Section
临界区的产生是为了解决一个进程内多个线程访问共享资源的问题。例如,由于同一个进程中不同线程拥有各自的堆栈,但对全局变量的访问是公共的,若访问全局变量时对全局变量进行写操作,则可能产生意料之外的错误。考虑以下模拟售票的C代码
#include <stdio.h>
#include <windows.h>
int tickets = 10; //总票数存储为全局变量
//模拟售票的线程函数
DWORD WINAPI FirstThreadProc(LPVOID lpParameter){
While (tickets > 0){//判断是否有余票
printf("There are %d tickets left", tickets);
tickets--;
printf("sell one ticket, %d left", tickets);
}
return 0;
}
//与第一个Thread的函数功能相同
DWORD WINAPI SecondThreadProc(LPVOID lpParameter){
While (tickets > 0){
printf("There are %d tickets left", tickets);
tickets--;
printf("sell one ticket, %d left", tickets);
}
return 0;
}
int main(){
//创建句柄数组
HANDLE ThreadHandles[2];
//开始第一个线程
ThreadHandles[0] = CreatThread(
NULL,
0,
FirstThreadProc,
NULL,
0,
NULL
);
//开始第二个线程
ThreadHandles[0] = CreatThread(
NULL,
0,
SecondThreadProc,
NULL,
0,
NULL
);
//等待两个线程执行完毕
WaitForMultipleObjects(2,ThreadHandles,TRUE,INFINITE);
return 0;
}
运行此代码将发现,可能出现同一张票“卖重”,及剩余票数为负的情况,如下图所示。
产生这种情况的原因是由于计算机的多道程序机制,即CPU在执行这两个线程时并不是分别执行一次,而是在上下文切换中在不同的时钟周期内执行,从而会导致意料之外的错误,此处描述其中的一种错误的产生过程:
假设某一时刻票仅剩一张,此时FirstThread线程在CPU中开始运行。判断有票有剩余后,恰巧此刻该线程被系统挂起;此时另一个线程SecondThread开始在CPU中执行,在一次while循环中始终没有被挂起,卖出一张票,从而剩余票数为0;此时被挂起的FirstThread被系统恢复执行,由于已执行过的代码判断剩余票数大于0(尽管此时实际剩余票数为0),因此进行售票操作,卖出一张后剩余票数为-1。
从这个简单的例子中可见,进程内不同线程在对全局变量进行写操作时可能产生意料之外的错误。通过令牌或锁的方式可避免这种情况的出现。在Win32中临界区是保证同一时间只有一个线程访问公共资源的简单方式,临界区包括两个操作原语
EnterCriticalSection() //进入临界区
LeaveCriticalSection() //离开临界区
将对共享资源进行访问的代码放在临界区操作原语之间,将对公共资源的访问和修改的代码定义为临界资源,每次只允许一个线程进入临界区,从而可实现对公共资源的加锁。对上述代码做如下修改
DWORD WINAPI FirstThreadProc(LPVOID lpParameter){
EnterCriticalSection(); //进入临界区
While (tickets > 0){
printf("There are %d tickets left", tickets);
tickets--;
printf("sell one ticket, %d left", tickets);
}
LeaveCriticalSection(); //离开临界区
return 0;
}
DWORD WINAPI FirstThreadProc(LPVOID lpParameter){
EnterCriticalSection(); //进入临界区
While (tickets > 0){
printf("There are %d tickets left", tickets);
tickets--;
printf("sell one ticket, %d left", tickets);
}
LeaveCriticalSection(); //离开临界区
return 0;
需要注意的是,临界区只产生用户空间的锁,而不产生内核的锁,因此只能作用于同一进程的不同线程之间,又称为线程锁。
2.互斥体Mutex
类似于同一进程中不同线程对共享资源的访问,在不同进程的不同线程之间也经常需要访问共享资源,如对文件的读写。因此,在不同进程之间实现对共享资源加锁也是非常必要的。在Win32中不同进程的线程之间共享资源的加锁通过互斥体实现。互斥体的定义如下
HANDLE CreateMutexA(
LPSECURITY_ATTRIBUTES lpMutexAttributes, //互斥体安全属性
BOOL bInitialOwner, //互斥体的起始拥有者
LPCSTR lpName //创建的互斥体的名字
);
互斥体的API详细说明见以下URL
https://docs.microsoft.com/zh-cn/windows/win32/api/synchapi/nf-synchapi-createmutexa
以下代码演示了在两个进程中使用互斥体的情景。其中一个程序代码如下
#include<windows.h>
#include<stdio.h>
int main(){
//创建互斥体内核对象,并获取句柄
HANDLE hdMutex = CreateMutex(NULL,FALSE,"mutex1");
//获取互斥体运行令牌
WaitForSingleObject(hdMutex,INFINITE);
for (int i=0;i<10;i++){
Sleep(500);
printf("A进程的X线程: %d\n", i);
}
//释放互斥体令牌
ReleaseMutex(hdMutex);
return 0;
}
另一个程序代码如下
#include<windows.h>
#include<stdio.h>
int main(){
//调用CreateMutex创建名字已存在的互斥体,返回值为已创建的互斥体句柄。
HANDLE hdMutex = CreateMutex(NULL,FALSE,"mutex1");
/*
此时若调用GetLastError()则得到ERROR_ALREADY_EXISTS
DWORD dwRtn = GetLastError();
*/
//获取互斥体运行令牌
WaitForSingleObject(hdMutex,INFINITE);
for (int i=0;i<10;i++){
Sleep(500);
printf("A进程的X线程: %d\n", i);
}
//释放互斥体令牌
ReleaseMutex(hdMutex);
return 0;
}
同时运行连个程序,发现同一时间只有一个进程可以运行,另一个进程处于阻塞状态直到第一个进程完成执行。
3.临界区和互斥体的区别
- 临界区(线程锁)只能用于一个进程内不同线程的控制
- 互斥体可设定等待超时时间,但线程锁不能
- 线程意外终止时,互斥体可避免死锁
- 互斥体创建内核对象,因而效率低于线程锁