C/C++ _beginthreadex 多线程操作 - 线程同步

8 篇文章 2 订阅

上一篇博客讲到了如何创建线程并使用线程,这一篇将讲解线程同步的操作!



一、线程同步 ----- 互斥对象

(内核级别)

互斥对象(mutex)属于内核对象,它能够确保线程拥有对单个资源的互斥访问权。

互斥对象包含一个使用数量,一个线程ID和一个计数器。其中线程ID用于标识系统中的哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数。

创建互斥对象:调用函数CreateMutex。调用成功,该函数返回所创建的互斥对象的句柄。

请求互斥对象所有权:调用函数WaitForSingleObject函数。线程必须主动请求共享对象的所有权才能获得所有权。

释放指定互斥对象的所有权:调用ReleaseMutex函数。线程访问共享资源结束后,线程要主动释放对互斥对象的所有权,使该对象处于已通知状态。

简单来讲,互斥对象就是,同一个内存变量,当线程一在操作他时,线程二就无法进行操作,得等到线程一不操作它了,线程二才可以进行操作它!

HANDLE WINAPI CreateMutexW (
    _In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes,   //指向安全属性
    _In_ BOOL bInitialOwner,   //初始化互斥对象的所有者  TRUE 立即拥有互斥体
    _In_opt_ LPCWSTR lpName    //指向互斥对象名的指针
);

前提是得要用WaitForMultipleObjects进行阻塞才可以!

用法:

// 全局变量句柄,用于互斥
HANDLE hMutex;


// 创建互斥量
hMutex = CreateMutex(NULL, FALSE, NULL);

	/* 创建线程的操作:省略一万行代码 */

// 阻塞多个线程句柄,直到子线程运行完毕,主线程才会往下走
WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);

// 关闭互斥量
CloseHandle(hMutex);

CreateMutex:参数一默认为NULL即可;参数二根据具体需求写TRUE或者NULL;参数三也写NULL吧,我都有点蒙圈了。

整体使用框架如上,create后记得要close。

提一个需求,创建n个线程,然后循环n次,奇数次对一个全局变量做加一操作,偶数次对一个全局变量做减一操作。

如果不使用CreateMutex进行互斥,线程就会同时访问到同一个全局变量,那么数据就乱了!

测试代码:

#include <stdio.h>
#include <Windows.h>
#include <process.h>

#define NUM_THREAD		50		// 五十个线程

unsigned WINAPI threadInc(void *arg);	// 线程操作方法,执行全局变量加一操作
unsigned WINAPI threadDes(void *arg);	// 线程操作方法,执行全局变量减一操作

long long num = 0;	// 全局变量

// 全局句柄,用于互斥
HANDLE hMutex;

int main(void) {
	// 线程句柄
	HANDLE tHandles[NUM_THREAD];

	// 创建互斥量
	hMutex = CreateMutex(NULL, FALSE, NULL);

	// 创建五十个线程
	printf("sizeof long long :%d\n", sizeof(long long));
	for (int i = 0; i < NUM_THREAD; i++) {
		if (i % 2) {
			tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadInc, NULL, 0, NULL);

		} else {
			tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0, NULL);
		}
	}

	// 阻塞多个线程句柄,直到子线程运行完毕,主线程才会往下走
	WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);

	// 关闭互斥量
	CloseHandle(hMutex);

	printf("result:%lld\n", num);
	return 0;
}


// 对全局变量加一操作
unsigned WINAPI threadInc(void *arg) {

	// 等待一个内核对象变为已通知状态
	WaitForSingleObject(hMutex, INFINITE);

	for (int i = 0; i < 500000; i++) {
		num += 1;
	}

	// 释放
	ReleaseMutex(hMutex);

	return 0;
}


// 对全局变量减一操作
unsigned WINAPI threadDes(void *arg) {
	WaitForSingleObject(hMutex, INFINITE);

	for (int i = 0; i < 500000; i++) {
		num -= 1;
	}

	ReleaseMutex(hMutex);

	return 0;
}

在这里插入图片描述


二、线程同步 ----- 事件对象

(内核级别)

事件对象也属于内核对象,它包含以下三个成员:
● 使用计数;
● 用于指明该事件是一个自动重置的事件还是一个人工重置的事件的布尔值;
● 用于指明该事件处于已通知状态还是未通知状态的布尔值。

事件对象有两种类型:人工重置的事件对象和自动重置的事件对象。这两种事件对象的区别在于当人工重置的事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程;而当一个自动重置的事件对象得到通知时,等待该事件对象的线程中只有一个线程变为可调度线程。

  1. 创建事件对象
    调用CreateEvent函数创建或打开一个命名的或匿名的事件对象。
  2. 设置事件对象状态
    调用SetEvent函数把指定的事件对象设置为有信号状态。
  3. 重置事件对象状态
    调用ResetEvent函数把指定的事件对象设置为无信号状态。
  4. 请求事件对象
    线程通过调用WaitForSingleObject函数请求事件对象。

创建事件对象的函数原型如下:

HANDLE CreateEvent(   
	LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全属性,写NULL   
	BOOL bManualReset,   // 复位方式  TRUE 必须用ResetEvent手动复原  FALSE 自动还原为无信号状态
	BOOL bInitialState,   // 初始状态   TRUE 初始状态为有信号状态  FALSE 无信号状态
	LPCTSTR lpName     //对象名称  写NULL  无名的事件对象 
);

第二个参数:就是设置信号状态的,就是它可以自动设置回无信号状态,或者手动设置!

第三个参数:就是说,返回值的这个句柄,它里面是有两种状态,有信号状态和无信号状态,当他是有信号这个状态时,WaitForSingleObject才不会卡住,否则会一直卡在有WaitForSingleObject的地方!

需求:
两个线程,一个线程统计字符数组中包含字符‘A’的个数;另一个线程统计字符数组不包含字符‘A’的个数;

测试代码

两个线程是同时执行的!

#include <stdio.h>
#include <Windows.h>
#include <process.h>

#define STR_LEN		100

unsigned WINAPI NumberOfA(void *arg);		// 统计字符A个数
unsigned WINAPI NumberOfOthers(void *arg);	// 统计其它字符个数

static char str[STR_LEN];
static HANDLE hEvent;


int main(void) {

	HANDLE hThread1, hThread2;
	printf("请输入一个字符串:");
	scanf_s("%s", str, sizeof(str) / sizeof(str[0]));

	// 创建事件对象,需要手动设置与释放信号,初始状态为无信号状态
	hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

	hThread1 = (HANDLE)_beginthreadex(NULL, 0, NumberOfA, NULL, 0, NULL);
	hThread2 = (HANDLE)_beginthreadex(NULL, 0, NumberOfOthers, NULL, 0, NULL);

	// 设置事件对象为有信号状态
	SetEvent(hEvent);

	// 等待信号
	WaitForSingleObject(hThread1, INFINITE);
	WaitForSingleObject(hThread2, INFINITE);

	// 直到2个线程执行完之后,再把事件设置为无信号状态(手动释放)
	ResetEvent(hEvent);
	CloseHandle(hEvent);

	return 0;
}


// 下面两个方法同时执行,访问同一个全局变量,因为没有对他做修改操作,所以没有关系
unsigned WINAPI NumberOfA(void *arg) {
	int cnt = 0;

	// 一直等待时间信号,当有事件信号后就可以执行了,否则一直卡在这里
	WaitForSingleObject(hEvent, INFINITE);

	for (int i = 0; str[i] != 0; i++) {
		if (str[i] == 'A') {
			cnt++;
		}
	}

	printf("Num of A: %d \n", cnt);

	return 0;
}


unsigned WINAPI NumberOfOthers(void *arg) {
	int cnt = 0;

	// 一直等待时间信号,当有事件信号后就可以执行了,否则一直卡在这里
	WaitForSingleObject(hEvent, INFINITE);

	for (int i = 0; str[i] != 0; i++) {
		if (str[i] != 'A') {
			cnt++;
		}
	}

	printf("Num of others: %d \n", cnt);

	return 0;
}

在这里插入图片描述

再举一个例子

模拟一个售票的流程,100张票,分为两个窗口(线程)出售!

// 创建事件对象,自动还原无信号状态,初始状态为无信号状态
hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);

让信号状态自动恢复无信号状态!
上面那个例子是手动恢复的!

测试代码

线程一执行后,线程二就不会执行;线程二执行后,线程一就不会执行。
原因
在main函数中SetEvent(hEvent);后,只能有一个线程句柄去接收使用,所以呢,两个线程,要么是线程A执行完后再SetEvent(hEvent);,然后线程B在执行,执行完后再SetEvent(hEvent);,线程A接收到再执行…;反过来也是一样的道理!

#include <stdio.h>
#include <Windows.h>
#include <process.h>

int iTickets = 100;
HANDLE hEvent;

unsigned int WINAPI SellTicketA(void *);
unsigned int WINAPI SellTicketB(void *);

int main(void) {

	HANDLE hThreadA, hThreadB;

	// 创建线程
	hThreadA = (HANDLE)_beginthreadex(NULL, 0, SellTicketA, NULL, 0, 0);
	hThreadB = (HANDLE)_beginthreadex(NULL, 0, SellTicketB, NULL, 0, 0);

	// 创建事件对象,自动还原无信号状态,初始状态为无信号状态
	hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);

	// 设置事件对象为有信号状态
	SetEvent(hEvent);

	WaitForSingleObject(hThreadA, INFINITE);
	WaitForSingleObject(hThreadB, INFINITE);
	system("pause");
	// 关闭句柄
	CloseHandle(hEvent);
	return 0;
}


unsigned int WINAPI SellTicketA(void *) {

	while (1) {
		// 等待事件信号
		WaitForSingleObject(hEvent, INFINITE);

		if (iTickets > 0) {
			Sleep(10);
			iTickets--;
			printf("A remain %d \n", iTickets);
		
		} else {
			break;
		}

		// 设置事件对象为有信号状态
		SetEvent(hEvent);
	}

	SetEvent(hEvent);

	return 0;
}


unsigned int WINAPI SellTicketB(void *) {

	while (1) {
		WaitForSingleObject(hEvent, INFINITE);

		if (iTickets > 0) {
			Sleep(10);
			iTickets--;
			printf("B remain %d \n", iTickets);
		
		} else {
			break;
		}

		SetEvent(hEvent);
	}

	SetEvent(hEvent);

	return 0;
}

在这里插入图片描述
当票买完后,就退出了。
全程两个线程是同时运行的,都是在访问同一个全局变量!
但是他们不是在同一时刻访问的,线程一访问了,线程二就不可以访问了;线程二访问了,线程一就不可以访问了。


三、线程同步 ----- 信号量

(内核级别)

内核对象的状态:
触发状态(有信号状态),表示有可用资源。
未触发状态(无信号状态),表示没有可用资源

工作原理
以一个停车场是运作为例。假设停车场只有三个车位,一开始三个车位都是空的。这时如果同时来了五辆车,看门人允许其中三辆不受阻碍的进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入一辆,如果又离开两辆,则又可以放入两辆,如此往复。这个停车系统中,每辆车就好比一个线程,看门人就好比一个信号量,看门人限制了可以活动的线程。假如里面依然是三个车位,但是看门人改变了规则,要求每次只能停两辆车,那么一开始进入两辆车,后面得等到有车离开才能有车进入,但是得保证最多停两辆车。对于Semaphore而言,就如同一个看门人,限制了可活动的线程数。

信号量的组成
  ①计数器:该内核对象被使用的次数;
  ②最大资源数量:标识信号量可以控制的最大资源数量(带符号的32位);
  ③当前资源数量:标识当前可用资源的数量(带符号的32位)。即表示当前开放资源的个数(注意不是剩下资源的个数),只有开放的资源才能被线程所申请。但这些开放的资源不一定被线程占用完。比如,当前开放5个资源,而只有3个线程申请,则还有2个资源可被申请,但如果这时总共是7个线程要使用信号量,显然开放的资源5个是不够的。这时还可以再开放2个,直到达到最大资源数量。

信号量的规则如下:
(1)如果当前资源计数大于0,那么信号量处于触发状态(有信号状态),表示有可用资源。
(2)如果当前资源计数等于0,那么信号量属于未触发状态(无信号状态),表示没有可用资源。
(3)系统绝对不会让当前资源计数变为负数
(4)当前资源计数绝对不会大于最大资源计数

在这里插入图片描述

信号量与互斥量不同的地方是,它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。信号量对象对线程的同步方式与前面几种方法不同,信号允许多个线程同时使用共享资源。

  1. 创建信号量

    HANDLE WINAPI CreateSemaphoreW(
     _In_opt_ LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,  // Null 安全属性
    	_In_ LONG lInitialCount,  //初始化时,共有多少个资源是可以用的。 0:未触发状//态(无信号状态),表示没有可用资源
    
     _In_ LONG lMaximumCount,  //能够处理的最大的资源数量   3
     _In_opt_ LPCWSTR lpName   //NULL 信号量的名称
    );
    

    参数一和参数四都写NULL就可以了!
    参数二:信号资源,写0,说明没有信号资源,写1,说明有一个信号资源。
    参数三:最多可以处理的信号量个数,至少写1吧,不然就没有意义了。

    例:

    HANDLE semOne = CreateSemaphore(NULL, 0, 1, NULL);
    
    /* 参数二:信号资源		参数三:可以处理信号的个数 */
    
    HANDLE semTwo = CreateSemaphore(NULL, 1, 1, NULL);
    

    定义了两个信号量,其中只有一个信号资源,也就是说,semOne 和 semTwo 共享这一个信号资源,谁拥有后就可以处理自己的线程函数了。

    这样即使线程同时运行,但是由于有信号量在阻塞着,所以访问共同的资源的内存(例如全局变量),也不会出现数据错乱。

    当然,信号资源一直被semTwo占用着,怎么才能给semOne使用呢?

  2. 增加信号量
    也可以叫释放信号。就是semTwo 释放掉它那个信号资源,semOne就可以得到这个信号资源,进行处理线程函数了。

    WINAPI ReleaseSemaphore(
    	_In_ HANDLE hSemaphore,   //信号量的句柄
    	_In_ LONG lReleaseCount,   //将lReleaseCount值加到信号量的当前资源计数上面 0-> 1
     	_Out_opt_ LPLONG lpPreviousCount  //当前资源计数的原始值
    );
    

    例:

    // 增加一个信号量(释放一个信号量)
    ReleaseSemaphore(semTwo, 1, NULL);
    

    将semTwo的一个信号量释放出来。

  3. 关闭句柄

    CloseHandle(
    	_In_ _Post_ptr_invalid_ HANDLE hObject
    );
    

    例:

    // 关闭句柄
    CloseHandle(semOne);
    CloseHandle(semTwo);
    

测试代码

需求:
做一个累加的功能,用户依次输入5个整数,一个线程做输入操作,一个线程做累加操作,最后输出累加结果。

线程一执行后,线程二就不会执行;线程二执行后,线程一就不会执行
原因:
只有一个信号资源,两个线程共同使用,那么是线程A先使用,然后释放掉信号资源,线程B再接着使用,再释放信号资源,以此达到线程同步运行但不同不处理数据的效果!

#include <stdio.h>
#include <Windows.h>
#include <process.h>

unsigned WINAPI Read(void *arg);
unsigned WINAPI Accu(void *arg);

static HANDLE semOne;
static HANDLE semTwo;
static int num;

int main(void) {

	HANDLE hThread1, hThread2;

	// semOne:没有资源可用,只能表示0或者1的二进制信号量,无信号
	semOne = CreateSemaphore(NULL, 0, 1, NULL);

	/* 参数二:信号资源		参数三:可以处理信号的个数 */

	// semTwo:有资源可用,有信号状态,有信号
	semTwo = CreateSemaphore(NULL, 1, 1, NULL);

	hThread1 = (HANDLE)_beginthreadex(NULL, 0, Read, NULL, 0, NULL);
	hThread2 = (HANDLE)_beginthreadex(NULL, 0, Accu, NULL, 0, NULL);

	WaitForSingleObject(hThread1, INFINITE);
	WaitForSingleObject(hThread2, INFINITE);

	// 关闭句柄
	CloseHandle(semOne);
	CloseHandle(semTwo);

	system("pause");
	return 0;
}


unsigned WINAPI Read(void *arg) {
	
	for (int i = 0; i < 5; i++) {
		printf("请输入一个整数:\n");

		// 等待内核对象semTwo的信号,如果有信号,继续执行;如果没有信号,等待
		WaitForSingleObject(semTwo, INFINITE);

		scanf_s("%d", &num, sizeof(num));

		// 增加一个信号量(释放一个信号量)
		ReleaseSemaphore(semOne, 1, NULL);
	}

	return 0;
}


unsigned WINAPI Accu(void * arg) {
	int sum = 0;
	for (int i = 0; i < 5; i++) {

		// 等待内核对象semOne的信号,如果有信号,继续执行;如果没有信号,等待
		WaitForSingleObject(semOne, INFINITE);

		sum += num;
		printf("sum = %d \n", sum);

		// 增加一个信号量(释放一个信号量)
		ReleaseSemaphore(semTwo, 1, NULL);
	}

	printf("Result:%d\n", sum);

	return 0;
}

在这里插入图片描述


四、线程同步 ----- 关键代码段

(用户级别):企业级开发中最常用!

线程一执行后,线程二就不会执行;线程二执行后,线程一就不会执行。

关键代码段,也称为临界区,工作在用户方式下。它是指一个小代码段,在代码能够执行前,它必须独占对某些资源的访问权。通常把多线程中访问同一种资源的那部分代码当做关键代码段。

使用关键代码段分为以下几点:

  1. 初始化关键代码段
    调用InitializeCriticalSection函数初始化一个关键代码段。

    函数原型:

    InitializeCriticalSection(
    	_Out_ LPCRITICAL_SECTION lpCriticalSection
    );
    

    该函数只有一个指向CRITICAL_SECTION结构体的指针。在调用InitializeCriticalSection函数之前,首先需要构造一个CRITICAL_SCTION结构体类型的对象,然后将该对象的地址传递给InitializeCriticalSection函数。

    例:

    // 全局变量
    CRITICAL_SECTION g_cs;
    
    
    // 初始化关键代码段
    InitializeCriticalSection(&g_cs);
    
  2. 进入关键代码段

    函数原型:

    VOID WINAPI EnterCriticalSection(
    	_Inout_ LPCRITICAL_SECTION lpCriticalSection
    );
    

    调用EnterCriticalSection函数,以获得指定的临界区对象的所有权,该函数等待指定的临界区对象的所有权,如果该所有权赋予了调用线程,则该函数就返回;否则该函数会一直等待,从而导致线程等待。

    例:

    // 进入临界区
    	EnterCriticalSection(&g_cs);	
    
  3. 退出关键代码段

    函数原型:

    VOID WINAPI LeaveCriticalSection(
    	_Inout_ LPCRITICAL_SECTION lpCriticalSection
    );
    

    线程使用完临界区所保护的资源之后,需要调用LeaveCriticalSection函数,释放指定的临界区对象的所有权。之后,其他想要获得该临界区对象所有权的线程就可以获得该所有权,从而进入关键代码段,访问保护的资源。

    例:

    // 离开临界区
    LeaveCriticalSection(&g_cs);
    
  4. 删除临界区

    函数原型:

    VOID WINAPI LeaveCriticalSection(
    	_Inout_ LPCRITICAL_SECTION lpCriticalSection
    );
    

    当临界区不再需要时,可以调用DeleteCriticalSection函数释放该对象,该函数将释放一个没有被任何线程所拥有的临界区对象的所有资源。

    例:

    // 删除临界区
    DeleteCriticalSection(&g_cs);
    

个人理解:
关键代码段,就是全程都是由程序员在控制着,他也是线程同步的一种,当一个线程执行后,如果里面有关键代码段,在如果里面有某些全局的变量是其它线程也有使用的,那么其它线程就得等待,等待关键代码段执行完毕并执行LeaveCriticalSection离开临界区后,其它线程的关键代码段才可以继续执行!

测试代码

线程一执行后,线程二就不会执行;线程二执行后,线程一就不会执行

需求:
还是那个买票的系统,这里使用关键代码段来实现!

1. 没有使用关键代码段的效果

#include <stdio.h>
#include <Windows.h>
#include <process.h>

int iTickets = 100;
CRITICAL_SECTION g_cs;

unsigned int WINAPI SellTicketA(void *);
unsigned int WINAPI SellTicketB(void *);

int main(void) {

	HANDLE hThreadA, hThreadB;

	// 创建线程
	hThreadA = (HANDLE)_beginthreadex(NULL, 0, SellTicketA, NULL, 0, 0);
	hThreadB = (HANDLE)_beginthreadex(NULL, 0, SellTicketB, NULL, 0, 0);

	// 初始化关键代码段
	//InitializeCriticalSection(&g_cs);

	// 等待线程函数执行结束
	WaitForSingleObject(hThreadA, INFINITE);
	WaitForSingleObject(hThreadB, INFINITE);

	// 删除临界区
	//DeleteCriticalSection(&g_cs);

	system("pause");
	return 0;
}


unsigned int WINAPI SellTicketA(void *) {

	while (1) {
		// 进入临界区
		//EnterCriticalSection(&g_cs);						//	--
															//   |
		if (iTickets > 0) {									//	 |
			Sleep(1);										//	 |
			iTickets--;										//	  >	临界区。在此区域内,数据变量,其它代码将无法做修改。
			printf("A remain %d \n", iTickets);				//	 |
															//	 |
			// 离开临界区									//	 |
			//LeaveCriticalSection(&g_cs);					//	--
												
		} else {
			// 离开临界区
			//LeaveCriticalSection(&g_cs);
			break;
		}
	}

	return 0;
}


unsigned int WINAPI SellTicketB(void *) {

	while (1) {
		// 进入临界区
		//EnterCriticalSection(&g_cs);

		if (iTickets > 0) {
			Sleep(1);
			iTickets--;
			printf("B remain %d \n", iTickets);

			// 离开临界区
			//LeaveCriticalSection(&g_cs);

		} else {
			// 离开临界区
			//LeaveCriticalSection(&g_cs);
			break;
		}

	}

	return 0;
}

在这里插入图片描述

2. 有关键代码段的效果

#include <stdio.h>
#include <Windows.h>
#include <process.h>

int iTickets = 100;

// 关键代码段的结构体类型对象
CRITICAL_SECTION g_cs;

unsigned int WINAPI SellTicketA(void *);
unsigned int WINAPI SellTicketB(void *);

int main(void) {

	HANDLE hThreadA, hThreadB;

	// 创建线程
	hThreadA = (HANDLE)_beginthreadex(NULL, 0, SellTicketA, NULL, 0, 0);
	hThreadB = (HANDLE)_beginthreadex(NULL, 0, SellTicketB, NULL, 0, 0);

	// 初始化关键代码段
	InitializeCriticalSection(&g_cs);

	// 等待线程函数执行结束
	WaitForSingleObject(hThreadA, INFINITE);
	WaitForSingleObject(hThreadB, INFINITE);

	// 删除临界区
	DeleteCriticalSection(&g_cs);

	system("pause");
	return 0;
}


unsigned int WINAPI SellTicketA(void *) {

	while (1) {
		// 进入临界区
		EnterCriticalSection(&g_cs);						//	--
															//   |
		if (iTickets > 0) {									//	 |
			Sleep(1);										//	 |
			iTickets--;										//	  >	临界区。在此区域内,数据变量,其它代码将无法做修改。
			printf("A remain %d \n", iTickets);				//	 |
															//	 |
			// 离开临界区									//	 |
			LeaveCriticalSection(&g_cs);					//	--
												
		} else {
			// 离开临界区
			LeaveCriticalSection(&g_cs);
			break;
		}
	}

	return 0;
}


unsigned int WINAPI SellTicketB(void *) {

	while (1) {
		// 进入临界区
		EnterCriticalSection(&g_cs);

		if (iTickets > 0) {
			Sleep(1);
			iTickets--;
			printf("B remain %d \n", iTickets);

			// 离开临界区
			LeaveCriticalSection(&g_cs);

		} else {
			// 离开临界区
			LeaveCriticalSection(&g_cs);
			break;
		}

	}

	return 0;
}

在这里插入图片描述

会出现某些线下一直连续执行的情况,但这也是正常的,但是他绝对不会出现像上面那种情况!


五、线程同步 ----- 关键代码段 之 线程死锁

死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。

注意:
线程死锁只会出现在关键代码段中,其它多线程同步是不会出现的!

出现的主要原因就是:滥用关键代码段!

例如,在临界区中包含临界区!

下面代码就会造成线程死锁。
原因

  1. SellTicketA中,首先执行EnterCriticalSection(&g_csA);开始进入A的临界区,紧接着睡眠一毫秒;
  2. 然后,就在这睡眠的一毫秒中,代码开始执行SellTicketB函数,开始执行EnterCriticalSection(&g_csB); 开始进入B的临界区,紧接着睡眠一毫秒;
  3. 然后,SellTicketA函数中开始执行EnterCriticalSection(&g_csB);,但是SellTicketB函数已经调用了,所以在SellTicketA函数这边会一直等待,等待SellTicketB函数执行DeleteCriticalSection(&g_csB);离开临界区B,他才会继续往下执行!
  4. SellTicketB函数中也是一样的,开始执行EnterCriticalSection(&g_csA);,但是SellTicketA函数已经调用了,所以SellTicketB函数也会一直等待,等待SellTicketA函数执行DeleteCriticalSection(&g_csA);离开临界区A,他才会继续往下执行!
  5. 这就形成了你等我,我等你的局面,导致线程死锁!

解决办法就是,不要交替使用临界区,进入临界区和退出临界区的代码要成对出现!

#include <stdio.h>
#include <Windows.h>
#include <process.h>

int iTickets = 100;

// 关键代码段的结构体类型对象
CRITICAL_SECTION g_csA;
CRITICAL_SECTION g_csB;

unsigned int WINAPI SellTicketA(void *);
unsigned int WINAPI SellTicketB(void *);

int main(void) {

	HANDLE hThreadA, hThreadB;

	// 创建线程
	hThreadA = (HANDLE)_beginthreadex(NULL, 0, SellTicketA, NULL, 0, 0);
	hThreadB = (HANDLE)_beginthreadex(NULL, 0, SellTicketB, NULL, 0, 0);

	// 初始化关键代码段
	InitializeCriticalSection(&g_csA);
	InitializeCriticalSection(&g_csB);

	// 等待线程函数执行结束
	WaitForSingleObject(hThreadA, INFINITE);
	WaitForSingleObject(hThreadB, INFINITE);

	// 删除临界区
	DeleteCriticalSection(&g_csA);
	DeleteCriticalSection(&g_csB);

	system("pause");
	return 0;
}


unsigned int WINAPI SellTicketA(void *) {

	while (1) {
		// 进入临界区
		EnterCriticalSection(&g_csA);		// 开始进入临界区A
		Sleep(1);
		EnterCriticalSection(&g_csB);		// 开始进入临界区B
															
		if (iTickets > 0) {									
			Sleep(1);										
			iTickets--;										
			printf("A remain %d \n", iTickets);				
															
			// 离开临界区									
			LeaveCriticalSection(&g_csB);		// 离开临界区B				
			LeaveCriticalSection(&g_csA);		// 离开临界区A

		} else {
			// 离开临界区
			LeaveCriticalSection(&g_csB);		// 离开临界区B				
			LeaveCriticalSection(&g_csA);		// 离开临界区A
			break;
		}
	}

	return 0;
}


unsigned int WINAPI SellTicketB(void *) {

	while (1) {
		// 进入临界区
		EnterCriticalSection(&g_csB);			// 开始进入临界区B
		Sleep(1);
		EnterCriticalSection(&g_csA);			// 开始进入临界区A

		if (iTickets > 0) {
			Sleep(1);
			iTickets--;
			printf("B remain %d \n", iTickets);

			// 离开临界区
			LeaveCriticalSection(&g_csA);		// 离开临界区A
			LeaveCriticalSection(&g_csB);		// 离开临界区B		

		} else {
			// 离开临界区
			LeaveCriticalSection(&g_csA);		// 离开临界区A
			LeaveCriticalSection(&g_csB);		// 离开临界区B		
			break;
		}

	}

	return 0;
}

一直卡在这…

在这里插入图片描述


六、总结

线程同步就是说线程同步运行,但是如果需要同时访问全局变量,那么,其中一个线程运行后,另一个线程就会停下来等待,等待那个线程执行完相关代码后它再继续执行!

重点学习一下事件对象和关键代码段,因为这两个是项目开发中比较常用的!
但是也要注意线程死锁喔!

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cpp_learners

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值