临界区
线程安全问题
每个线程都有自己的栈,而局部变量是存储在栈中的,这就意味着每个线程都有一份自己的“局部变量”,如果线程仅仅使用“局部变量”那么就不存在线程安全问题。
那如果多个线程共用一个全局变量呢?
全局变量是存储在全局区的,如果多线程执行的代码都去访问这个全局变量,那么用的却同一个全局变量,这就是线程安全问题
那多线程一定存在线程安全问题吗?
未必,访问的不是全部变量的话就没问题
那多线程访问全局变量就一定存在问题吗?
也未必,如果只对全局变量是只读的操作,就没有
所以线程安全问题的前提
- 有全局变量
- 对全局变量不是只读的操作,又写的动作
例子体现问题
#include<stdio.h>
#include<windows.h>
int NumOfTickets = 10;
DWORD WINAPI FirstThread(LPVOID lpParameter)
{
while (NumOfTickets > 0)
{
printf("还有:%d张票\n", NumOfTickets);
NumOfTickets--;
printf("卖出一张,还剩下:%d张票\n", NumOfTickets);
}
return 0;
}
int main()
{
DWORD res1;
DWORD res2;
HANDLE hThread[2];
//创建两个线程,并指向同一个函数
hThread[0] = CreateThread(NULL, 0, FirstThread, NULL, 0, NULL);
hThread[1] = CreateThread(NULL, 0, FirstThread, NULL, 0, NULL);
//等线程结束
WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
//线程执行完
GetExitCodeThread(hThread[0], &res1);
GetExitCodeThread(hThread[1], &res2);
printf("%d %d\n", res1, res2);
return 0;
}
这里的代码是粗略的模拟了一个售票系统,初始有10张票,随着两个线程里票的递减,而打印出票的情况,具体看代码,不难,API都是前面写过的
可以看到这里的票情况十分的混乱,甚至出现了-1票的情况
(如果没出现-1票情况的,可以多运行几下)
为了方便理解这样情况的发生过程,请看如下
//第一个线程X
DWORD WINAPI FirstThread(LPVOID lpParameter)
{
while (NumOfTickets > 0)
{//(1)
printf("还有:%d张票\n", NumOfTickets);
NumOfTickets--;
printf("卖出一张,还剩下:%d张票\n", NumOfTickets);
}
return 0;
}
//全局变量
int NumOfTickets;
//第二个线程Y
DWORD WINAPI FirstThread(LPVOID lpParameter)
{
while (NumOfTickets > 0)
{
printf("还有:%d张票\n", NumOfTickets);
NumOfTickets--; //(2)
printf("卖出一张,还剩下:%d张票\n", NumOfTickets);
}
return 0;
}
这里有两个线程,一份全局变量,这两个线程是交替执行的,可能会在任何点进行线程切换
假如,当票只剩一张的时候,X运行到(1)的位置,发生线程切换,Y运行到(2)位置时发生线程切换,此时,票已经是0,X就会打印“还有0张票”,然后-1,打印出”还剩-1票“
通过这个例子相信大家能够理解为什么会有线程安全问题
解决办法
这里又要提到两个新的概念
如果想让我们的程序变得安全,首先给这个全局变量起个名字,叫临界资源
什么叫临界资源呢?
是指一次只允许一个线程使用的资源
对这个临界资源访问的代码称为临界区
这个临界区可以由自己来写也可以用API来写,目前不涉及自己来写临界区,需要考虑的东西有点多
Windows实现方式:
首先在设置一个全局变量,称为令牌,线程一要想访问临界资源,就要先访问令牌,是否还在,在的话下面的代码就可以对临界资源进行操作,那线程二三,访问的时候发现令牌不在了,那代码就访问不了临界资源,所以这样就避免了线程安全问题
细心的大佬可能发现问题了,那线程一在访问令牌的时候还能来的及改,那线程二就来访问了,那两个线程不就同时拥有令牌了吗?
所以,windows就把这里令牌的判断和修改变成了原子操作(感兴趣的可以去搜一搜,我也不是很了解T_T),所以这就是自己实现需要考虑之一的事情
临界区实现之线程锁
这里实现线程安全的方式有很多,用下面的例子来做实验
1.创建全局变量
CRITICAL_SECTION cs;
CRITICAL_SECTION结构体
typedef RTL_CRITICAL_SECTION CRITICAL_SECTION;
typedef struct _RTL_CRITICAL_SECTION {
PRTL_CRITICAL_SECTION_DEBUG DebugInfo;
//
// The following three fields control entering and exiting the critical
// section for the resource
//
LONG LockCount;
LONG RecursionCount;
HANDLE OwningThread; // from the thread's ClientId->UniqueThread
HANDLE LockSemaphore;
ULONG_PTR SpinCount; // force size on 64-bit systems when packed
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;
2.初始化全局变量
InitializeCriticalSection(&cs);
3.实现临界区
//进入临界区
EnterCriticalSection(&cs);
//离开临界区
LeaveCriticalSection(&cs);
目前就用知道怎么使用和大概的函数意思就行了
代码真正实现
#include<stdio.h>
#include<windows.h>
int NumOfTickets = 10;
CRITICAL_SECTION cs; //要注意必须是全局变量
DWORD WINAPI FirstThread(LPVOID lpParameter)
{
EnterCriticalSection(&cs); //进入临界区
while (NumOfTickets > 0)
{
printf("还有:%d张票\n", NumOfTickets);
NumOfTickets--;
printf("卖出一张,还剩下:%d张票\n", NumOfTickets);
}
LeaveCriticalSection(&cs); //离开临界区
return 0;
}
int main()
{
DWORD res1;
DWORD res2;
HANDLE hThread[2];
InitializeCriticalSection(&cs); //初始化
//创建两个线程,并指向同一个函数
hThread[0] = CreateThread(NULL, 0, FirstThread, NULL, 0, NULL);
hThread[1] = CreateThread(NULL, 0, FirstThread, NULL, 0, NULL);
//等线程结束
WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
//线程执行完
GetExitCodeThread(hThread[0], &res1);
GetExitCodeThread(hThread[1], &res2);
printf("%d %d\n", res1, res2);
return 0;
}
运行结果
可以看到成功实现,没有混乱的输出和-1票的结果
注意线程锁应该要包含所有与临界资源有关的代码,如判断和改写等
假如把线程锁写到循环里面就会出现-1票的结果
总结
- 了解了什么是线程安全问题
- 两个新的概念:临界资源和临界区
- 线程安全问题的其中一个解决方法