临界区
线程安全问题
每个线程都有自己的栈,而局部变量是存储在栈中的,这就意味着每个线程都有一份自己的局部变量,如果线程仅仅使用"局部变量"那么就不存在线程安全问题。
反之如果多个线程共有一个全局变量呢?那么在什么情况下会有问题呢?那就是当多线程共用一个全局变量并对其进行修改时则存在安全问题,如果仅仅是读的话没有问题。
那如果多个线程共用一个全局变量呢?
多线程不访问全局变量就没有线程安全问题。
#include <windows.h>
int countNumber = 10;
DWORD WINAPI ThreadProc(LPVOID lpParameter) {
while (countNumber > 0) {
printf("Sell num: %d\n", countNumber);
// 售出-1
countNumber--;
printf("Count: %d\n", countNumber);
}
return 0;
}
int main(int argc, char* argv[])
{
HANDLE hThread;
hThread = CreateThread(NULL, NULL, ThreadProc, NULL, 0, NULL);
HANDLE hThread1;
hThread1 = CreateThread(NULL, NULL, ThreadProc, NULL, 0, NULL);
CloseHandle(hThread);
getchar();
return 0;
}
如图,我们运行了代码,发现会出现重复售卖,并且到最后总数竟变成了-1:
出现这样的问题其本质原因是什么呢?因为多线程在执行的时候是同步进行的,并不是按照顺序来,所以就都会窒息,自然就会出现这种情况。
解决问题
想要解决线程问题,就需要引伸出一个概念:临界资源,临界资源表示对该资源的访问一次只能有一个线程;访问临界资源的那一段程序,我们称之为临界区
那么我们如何实现临界区呢?第一,我们可以自己来写,但是这需要一定门槛,先不过多的去了解;第二,可以使用Windows提供的API来实现
临界资源
临界资源指的是一次只允许一个线程使用的资源,对它访问的代码称为临界区
解决方法临界区
首先会有一个令牌,假设线程1获取了这个令牌,那么这时候令牌则只为线程1所有,然后线程1会执行代码去访问全局变量,最后归还令牌;如果其他线程想要去访问这个全局变量就需要获取这个令牌,但当令牌已经被取走时则无法访问。
假设你自己来实现临界区,可能在判断令牌有没有被拿走的时候就又会出现问题,所以自己实现临界区还是有一定的门槛的。
把获取令牌称为原子操作
临界区实现之线程锁
-
创建全局变量
CRITICAL_SECTION cs; -
初始化全局变量
InitializeCriticalSection(&cs); -
实现临界区
EnterCriticalSection(&cs);
//使用临界资源:就是对全局变量读写的代码
LeaveCriticalSection(&cs);
代码
#include <windows.h>
CRITICAL_SECTION cs; // 创建全局变量
int countNumber = 10;
DWORD WINAPI ThreadProc(LPVOID lpParameter) {
while (1) {
EnterCriticalSection(&cs); // 构建临界区,获取令牌
if (countNumber > 0) {
printf("Thread: %d\n", *((int*)lpParameter));
printf("Sell num: %d\n", countNumber);
// 售出-1countNumber--;
printf("Count: %d\n", countNumber);
} else {
LeaveCriticalSection(&cs); // 离开临临界区,归还令牌
break;
}
LeaveCriticalSection(&cs); // 离开临临界区,归还令牌
}
return 0;
}
int main(int argc, char* argv[])
{
InitializeCriticalSection(&cs); // 使用之前进行初始化
int a = 1;
HANDLE hThread;
hThread = CreateThread(NULL, NULL, ThreadProc, (LPVOID)&a, 0, NULL);
int b = 2;
HANDLE hThread1;
hThread1 = CreateThread(NULL, NULL, ThreadProc, (LPVOID)&b, 0, NULL);
CloseHandle(hThread);
getchar();
return 0;
}
互斥体的使用
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes, // SD 安全属性,包含安全描述符
BOOL bInitialOwner, // initial owner 是否希望互斥体创建出来就有信号,或者说就可以使用,如果希望的话就为FALSE;官方解释为如果该值为TRUE则表示当前进程拥有该互斥体所有权
LPCTSTR lpName // object name 互斥体的名字
);
创建互斥体的函数为CreateMutex,该函数的语法格式如下:
互斥体(能够放在内核中的令牌)
内核级临界资源怎么办?
上一章中我们了解了使用线程锁来解决多个线程共用一个全局变量的线程安全问题;那么假设A进程的B线程和C进程的D线程,同时使用的是内核级的临界资源(内核对象:线程、文件、进程...)该怎么让这个访问是安全的?使用线程锁的方式明显不行,因为线程锁仅能控制同进程中的多线程。
互斥体的使用
创建互斥体的函数为CreateMutex,该函数的语法格式如下:
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes, // SD 安全属性,包含安全描述符
BOOL bInitialOwner, // initial owner 是否希望互斥体创建出来就有信号,或者说就可以
使用,如果希望的话就为FALSE;官方解释为如果该值为TRUE则表示当前进程拥有该互斥体所有权
LPCTSTR lpName // object name 互斥体的名字
);
我们可以模拟一下操作资源然后创建:
#include <windows.h>
int main(int argc, char* argv[])
{
// 创建互斥体
HANDLE cm = CreateMutex(NULL, FALSE, "XYZ");
// 等待互斥体状态发生变化,也就是有信号或为互斥体拥有者,获取令牌
WaitForSingleObject(cm, INFINITE);
// 操作资源
for (int i = 0; i < 5; i++) {
printf("Process: A Thread: B -- %d \n", i);
Sleep(1000);
}
// 释放令牌
ReleaseMutex(cm);
return 0;
}
互斥体与线程锁的区别
- 线程锁只能用于单个进程间的线程控制
- 互斥体可以设定等待超时,但线程锁不能
- 线程意外终结,Mutex可以避免无限等待
- Mutex效率没有线程锁高
互斥体与线程锁的区别
- 线程锁只能用于单个进程间的线程控制
- 互斥体可以设定等待超时,但线程锁不能
- 线程意外终结时,互斥体(Mutex)可以避免无限等待
- (Mutex)效率没有线程锁高
课外扩展-互斥体防止程序多开
CreateMutex函数的返回值MSDN Library的介绍是这样的:如果函数成功,返回值是一个指向mutex对象的句柄;如果命名的mutex对象在函数调用前已经存在,函数返回现有对象的句柄,GetLastError返回ERROR_ALREADY_EXISTS(表示互斥体以及存在);否则,调用者创建mutex对象;如果函数失败,返回值为NULL,要获得扩展的错误信息,请调用GetLastError获取。
所以我们可以利用互斥体来防止程序进行多开:
#include <windows.h>
int main(int argc, char* argv[])
{
// 创建互斥体
HANDLE cm = CreateMutex(NULL, TRUE, "XYZ");
// 判断互斥体是否创建失败
if (cm != NULL) {
// 判断互斥体是否已经存在,如果存在则表示程序被多次打开
if (GetLastError() == ERROR_ALREADY_EXISTS) {
printf("该程序已经开启了,请勿再次开启!");
getchar();
} else {
// 等待互斥体状态发生变化,也就是有信号或为互斥体拥有者,获取令牌
WaitForSingleObject(cm, INFINITE);
// 操作资源
for (int i = 0; i < 5; i++) {
printf("Process: A Thread: B -- %d \n", i);
Sleep(1000);
}
// 释放令牌
ReleaseMutex(cm);
}
} else {
printf("CreateMutex 创建失败! 错误代码: %d\n", GetLastError());
}
return 0;
}