第一节:【Window】创建线程的3种方式
第二节:【Window】线程同步概述
第三节:【Window】线程同步方式1——临界区(关键代码段)
第四节:【Window】线程同步方式2——互斥量
第五节:【Window】线程同步方式3——事件
第六节:【Window】线程同步方式4——信号量
文章目录
1 临界区(关键代码段)
1.1 简介
临界区,也称为关键代码段,工作在用户方式下。它是指一个小代码段,在代码能够执行前,它必须独占对某些资源的访问权。
临界区(Critical Section)是一段独占对某些共享资源访问的代码,在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式
操作共享资源的目的。
每个进程中访问临界资源的那段代码称为临界区(Critical Section)(临界资源是一次仅允许一个进程使用的共享资源)。每次只准许一个进程进入临界区,进入后不允许其他进程进入。不论是硬件临界资源,还是软件临界资源,多个进程必须互斥地对它进行访问。
多个进程中涉及到同一个临界资源的临界区称为相关临界区。.
临界资源是一次仅允许一个进程使用的共享资源。各进程采取互斥的方式,实现共享的资源称作临界资源。
属于临界资源的硬件:打印机,磁带机等;
软件:消息队列,变量,数组,缓冲区等。
诸进程间采取互斥方式,实现对这种资源的共享。
1.2 程序调度法则
进程进入临界区的调度原则是:
- 如果有若干进程要求进入空闲的临界区,一次仅允许一个进程进入。
- 任何时候,处于临界区内的进程不可多于一个。如已有进程进入自己的临界区,则其它所有试图进入临界区的进程必须等待。
- 进入临界区的进程要在
有限时间内退出
,以便其它进程能及时进入自己的临界区。 - 如果进程不能进入自己的临界区,则应让出CPU,避免进程出现“
忙等
”现象。
1.3 线程同步问题
临界区在使用时以CRITICAL_SECTION结构
对象保护共享资源,并分别用EnterCriticalSection()
和LeaveCriticalSection()
函数去标识和释放一个临界区。
所用到的CRITICAL_SECTION结构
对象必须经过InitializeCriticalSection()
的初始化后才能使用,而且必须确保所有线程中的任何试图访问此共享资源的代码都处在此临界区的保护之下。否则临界区将不会起到应有的作用,共享资源依然有被破坏的可能。
2 临界区操作原语
2.1 定义全局的锁CRITICAL_SECTION
- 说明
CRITICAL_SECTION不是针对于资源的,而是针对于不同线程间的代码段的,我们能够用它来进行所谓资源的“锁定”,其实是因为我们在任何访问共享资源的地方都加入了EnterCriticalSection和 LeaveCriticalSection语句,使得同一时间只能够有一个线程的代码段访问到该共享资源而已(其它想访问该资源的代码段不得不等待)。 - 声明
CRITICAL_SECTION cs;//可以理解为锁定一个资源
2)InitializeCriticalSection
- 说明
InitializeCriticalSection函数用来初始化一个临界资源对象。“临界区”CCriticalSection 是临界资源对象指针,该函数无返回值。单进程的各个线程可以使用临界资源对象来解决同步互斥问题
,该对象不能保证哪个线程能够获得到临界资源对象,该系统能公平的对待每一个线程。 - 声明
VOID WINAPI InitializeCriticalSection(
LPCRITICAL_SECTION lpCriticalSection //临界资源对象指针。
);
3)EnterCriticalSection和LeaveCriticalSection
- 说明
多个线程操作相同的数据时,一般是需要按顺序访问的,否则会引导数据错乱,无法控制数据,变成随机变量。为解决这个问题,就需要引入互斥变量,让每个线程都按顺序地访问变量。这样就需要使用EnterCriticalSection和LeaveCriticalSection
函数。
是多线程中用来确保同一时刻只有一个线程操作被保护的数据的操作函数。
- 声明
加锁————接下来的代码处理过程中不允许其他线程进行操作,除非遇到LeaveCriticalSection。
VOID WINAPI EnterCriticalSection(
_Inout_ LPCRITICAL_SECTION lpCriticalSection //临界资源对象指针。
);
解锁————EnterCriticalSection之间代码资源已经释放了,其他线程可以进行操作。
VOID WINAPI LeaveCriticalSection(
_Inout_ LPCRITICAL_SECTION lpCriticalSection //临界资源对象指针。
);
4) DeleteCriticalSection
- 说明
删除临界区,先前必须已在InitializeCriticalSection函数中将该对象初始化。 - 声明
// 此函数不返回值
VOID WINAPI DeleteCriticalSection(
_Inout_ LPCRITICAL_SECTION lpCriticalSection //临界资源对象指针。
);
3 应用和举例
#include <Windows.h>
#include <iostream>
using namespace std;
typedef struct _STRUCT_DATA_
{
int id; //用于标识出票id
int tickets;//门票数
}_DATA, * _pDATA; //共享资源
CRITICAL_SECTION g_cs;//定义全局的锁,可以理解为锁定一个资源
DWORD WINAPI Fun1(LPVOID lpParam);
DWORD WINAPI Fun2(LPVOID lpParam);
void main()
{
HANDLE hThread1;
HANDLE hThread2;
_DATA stru_data;
stru_data.id = 0;//出票ID初始为0
stru_data.tickets = 100;//门票总共100张
hThread1 = CreateThread(
NULL, //为NULL则表示返回的句柄不能被子进程继承
0, //设置初始栈的大小,以字节为单位,如果为0,那么默认将使用与调用该函数的线程相同的栈空间大小。
Fun1, //指向线程函数的指针
&stru_data, //向线程函数传递的参数,是一个指向结构的指针
0, //控制线程创建的标志,0:表示创建后立即激活
NULL //保存新线程的id,是指向线程id的指针,如果为空,线程id不被返回
); //函数成功,返回线程句柄,否则返回NULL
hThread2 = CreateThread(NULL, 0, Fun2, &stru_data, 0, NULL);
if (hThread1 != NULL)CloseHandle(hThread1);
if (hThread1 != NULL)CloseHandle(hThread2);
InitializeCriticalSection(&g_cs);//初始化结构CRITICAL_SECTION
Sleep(4000);
DeleteCriticalSection(&g_cs);//删除临界区
}
DWORD WINAPI Fun1(LPVOID lpParam)
{
_pDATA data = (_pDATA)lpParam;
while (TRUE)
{
EnterCriticalSection(&g_cs);//进入临界区,g_cs对data进行了锁定操作,data处于g_cs的保护之中,接下来的代码处理过程中不允许其他线程进行操作,除非遇到LeaveCriticalSection
//判断当前的票数是否全部售出
//是:休息1毫秒,让出CPU;然后当前取票ID+1,票数-1;然后离开临界区,打开锁
//不是:离开临界区,运行其他线程进入临界区;线程结束;
if (data->tickets > 0)
{
Sleep(1);
cout << "fun1: " << data->id++ << endl;
cout << "thread 1:sell ticket: " << data->tickets-- << endl;
LeaveCriticalSection(&g_cs);//解锁,到EnterCriticalSection之间代码资源已经释放了,其他线程可以进行操作
}
else {
LeaveCriticalSection(&g_cs);
break;
}
}
return 0;
}
DWORD WINAPI Fun2(LPVOID lpParam)
{
_pDATA data = (_pDATA)lpParam;
while (TRUE)
{
EnterCriticalSection(&g_cs);//加锁
if (data->tickets > 0)
{
Sleep(1);
cout << "fun2: " << data->id++ << endl;
cout << "thread 2:sell ticket: " << data->tickets-- << endl;
LeaveCriticalSection(&g_cs);//解锁
}
else {
LeaveCriticalSection(&g_cs);//解锁
break;
}
}
return 0;
}
为了让g_cs
发挥作用,必须在访问共享资源_DATA, * _pDATA
的任何一个地方都加上 EnterCriticalSection(&g_cs)和LeaveCriticalSection(&g_cs)
语句。
① 某个线程运行,先遇到EnterCriticalSection(&g_cs)
后,发现此时的g_cs
未上锁,因此可以执行if的代码,并锁住临界资源,此线程会暂停1毫秒。
② 于是另一个线程运行,它遇到的第一个语句是EnterCriticalSection(&g_cs)
,这个语句将对g_cs
变量进行访问。如果这个时候①中线程在操作_pDATA
,g_cs
变量中包含的值将告诉此线程,已有其它线程占用了g_cs
。因此,此线程的 EnterCriticalSection(&g_cs)
语句将不会返回,而处于挂起等待状态。直到①线程执行了 LeaveCriticalSection(&g_cs)
,此线程的EnterCriticalSection(&g_cs)
语句才会返回, 并且继续执行下面的操作。
③ 等到①线程醒过来后,开始if中的后续操作,并解锁LeaveCriticalSection(&g_cs)
。
④ 后续某个线程再次遇到语句EnterCriticalSection(&g_cs)
,可以执行并上锁。
…
与此往复,直到退出循环。
这个过程实际上是通过限制有且只有一个函数进入
CriticalSection
变量来实现代码段同步的。简单地说,对于同一个CRITICAL_SECTION
,当一个线程执行了EnterCriticalSection而没有执行LeaveCriticalSection
的时候,其它任何一个线程都无法完全执行EnterCriticalSection
,而不得不处于等待状态。
#include "windows.h"
#include "iostream"
using namespace std;
DWORD WINAPI FunProc1(LPVOID lpParameter);
DWORD WINAPI FunProc2(LPVOID lpParameter);
int ticket = 100;
CRITICAL_SECTION g_cs; //定义临界区
void main()
{
HANDLE hThread[2];
InitializeCriticalSection(&g_cs); //必须先初始化临界区
hThread[0] = CreateThread(NULL, 0, FunProc1, NULL, 0, NULL);
hThread[1] = CreateThread(NULL, 0, FunProc2, NULL, 0, NULL);
//等待线程返回
WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
DeleteCriticalSection(&g_cs); //删除临界区
}
DWORD WINAPI FunProc1(LPVOID lpParameter)
{
while (TRUE)
{
EnterCriticalSection(&g_cs);//进入临界区(申请钥匙,得到钥匙)
if (ticket > 0)
{
Sleep(1);
cout << "ticket 1:" << ticket-- << endl;
LeaveCriticalSection(&g_cs); //离开(放弃钥匙,不再拥有)
}
else
{
LeaveCriticalSection(&g_cs); //离开
break;
}
}
return 0;
}
DWORD WINAPI FunProc2(LPVOID lpParameter)
{
while (TRUE)
{
EnterCriticalSection(&g_cs);
if (ticket > 0)
{
Sleep(1);
cout << "ticket 2:" << ticket-- << endl;
LeaveCriticalSection(&g_cs);
}
else
{
LeaveCriticalSection(&g_cs);
break;
}
}
return 0;
}
再次强调一次,没有任何资源被“锁定”,CRITICAL_SECTION不是针对于资源的,而是针对于不同线程间的代码段的!我们能够用它来进行所谓资源的“锁定”,其实是因为我们在任何访问共享资源的地方都加入了EnterCriticalSection和 LeaveCriticalSection语句,使得同一时间只能够有一个线程的代码段访问到该共享资源而已(其它想访问该资源的代码段不得不等待)。
4 临界区存在的几个问题(重要)
在使用临界区时,一般不允许其运行时间过长,只要进入临界区的线程还没有离开,其他所有试图进入此临界区的线程都会被挂起而进入到等待状态,并会在一定程度上影响程序的运行性能。尤其需要注意的是不要将等待用户输入或是其他一些外界干预的操作包含到临界区。如果进入了临界区却一直没有释放,同样也会引起其他线程的长时间等待。换句话说,在执行了EnterCriticalSection()语句进入临界区后无论发生什么,必须确保与之匹配的LeaveCriticalSection()都能够被执行到。可以通过添加结构化异常处理代码来确保LeaveCriticalSection()语句的执行
。虽然临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。
-
临界区的退出,不会检测是否是已经进入的线程,也就是说,我可以在A线程中调用进入临界区函数,在B线程调用退出临界区的函数,同样是成功;
-
测试临界区的时候,如果没有调用进入临界区的函数,直接退出的话,系统没有进行判断,但是计数发现了改变,此时此临界区就再也用不了了,因为结构中的数据已经乱掉了。
-
使用
EnterCriticalSection与LeaveCriticlSection
时应注意:若在同一个线程中第一次LeaveCriticlSection
与第二次EnterCriticalSection
执行间隔较短(如一个循环内的最后一行与第一行),可能导致其他线程无法进入临界区。此时可在LeaveCriticlSection后适当延时。UINT SecondThread(LPVOID lParam) { for (int i = 0; i < 10; i++) { EnterCriticalSection(&cs);//加锁 nValue++; cout <<nValue << endl; LeaveCriticalSection(&cs);//解锁 Sleep(100);//若去掉此句可能导致其他线程无法进入临界区 } return 0; }