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

C++拾遗--多线程:C语言多线程的引入 C++拾遗--多线程:C语言多线程的引入 前言 多线程是编程中的一个重要内容。多核时代使多线程成为一种可能,显然,一件事情多个人干,效率一定会提升。下面来看下C语言中是如何使用多线程的。 正文 1.CreateThread 先来看一个实例 阅读详情

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



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

(内核级别)

互斥对象(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;
}

一直卡在这…

在这里插入图片描述


六、总结

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

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

[Win32] 多线程和线程同步 多线程:一个进程创建时,默认情况下系统会为它创建一个主线程,(如果使用Native API创建的线程就没有主线程,是空的,必须自己创建主线程),应用程序可以自己创建线程,还有以前写过的一篇“DLL注入技术”,就是远程在其他进程中创建线程,然后让远程线程load我们的dll。 系统是如何实现多线程的?其实,对于单CPU单核心的设备上,在一个确定的时刻,只能执行内存中的一个指令。所谓的“多任务抢占式 阅读详情

相关推荐

windows多线程基础(5):使用_beginthreadex创建线程(C/C++ Runtime Library考虑)

标准CRT(C Runtime Library)问世于1970年,没有考虑到将CRT运行于多线程应用程序的问题。 在多线程环境中存在问题的C/C++运行期库变量和函数包括:error, _doserrno, strtok, strerror, asctime, gmtime,malloc等等。这些函数中都存在全局变量或者要访问全局变量(对于malloc来说,进程的堆也算是全全局的了),所

知其然,也要知其所以然 1378

c++多线程之_beginthreadex

创建一个线程,具体用法查看代码 ,特别注意: 如果在代码中有使用标准C运行库中的函数时,尽量使用_beginthreadex()来代替CreateThread() 因为_beginthreadex在内部调用了CreateThread,在调用之前_beginthreadex做了很多的工作,从而使得它比CreateThread更安全。 例子一 #include <windows.h&g...

sinat_31608641的博客 5503

多线程编程在C++中的使用:_beginthread函数

注意,在实际的多线程编程中,还需要考虑线程的安全性、死锁等问题,并采取相应的措施进行处理。在C++中,可以使用互斥量(mutex)来实现线程之间的同步,并使用全局变量或线程局部存储(Thread Local Storage,TLS)来实现数据的共享。头文件,然后定义了线程函数,使用_beginthread函数创建线程,并在主函数中等待线程结束。在使用_beginthread函数创建线程之前,我们需要定义一个线程函数。一旦我们定义了线程函数,就可以使用_beginthread函数来创建线程。

CodeGu的博客 533

秒杀多线程第二篇 多线程第一次亲密接触 CreateThread与_beginthreadex本质区别

本文将带领你与多线程作第一次亲密接触,并深入分析CreateThread与_beginthreadex的本质区别,相信阅读本文后你能轻松的使用多线程并能流畅准确的回答CreateThread与_beginthreadex到底有什么区别,在实际的编程中到底应该使用CreateThread还是_beginthreadex? 使用多线程其实是非常容易的,下面这个程序的主线程会创建了一个子线程并等待...

huobengluantiao8 3585

使用beginthreadex函数实现C++多线程

需要注意的是,在访问g_count变量时,我们使用了EnterCriticalSection和LeaveCriticalSection函数来保证线程安全,同时需要先调用InitializeCriticalSection函数初始化临界区,最后使用DeleteCriticalSection函数删除临界区。在上面的代码中,我们定义了一个名为g_count的全局变量作为计数器,然后使用beginthreadex函数创建了5个线程,并让它们分别执行ThreadFunc函数。

CodeByte的博客 275

windows C++多线程(四):_beginthread的使用

windows创建线程一般都使用CreateThread,该函数与CRT有冲突,尽量不要使用该函数,应该用_beginthread,结束线程用_endthread,这两个函数可以成套的使用。 使用这两个函数需要包含的头文件#include <process.h> _beginthread声明如下 ACRTIMP uintptr_t __cde...

琅嬛福地 3616

windows多线程详解

在一个牛人的博客上看到了这篇文章,所以就转过来了,地址是http://blog.youkuaiyun.com/morewindows/article/details/7421759 本文将带领你与多线程作第一次亲密接触,并深入分析CreateThread与_beginthreadex的本质区别,相信阅读本文后你能轻松的使用多线程并能流畅准确的回答CreateThread与_beginthreadex到底有什

心如止水-GISer的成长之路 6万+

Qt 插件开发 plugin 超详细 有源码

Qt 插件化开发框架类似于前后端的微服务的场景,授权哪个微服务则前端可以使用哪个微服务,插件就行硬件插卡一样,可以被随时删除、插入和修改,所以结构很灵活,容易修改,方便软件的升级和维护;插件主要面向接口编程,无需访问.lib文件,热插拔、利于团队开发。即使在程序运行时.dll不存在,也可以正常启动,只是相应插件功能无法正常使用而已;动态库需要访问.lib文件,而且在程序运行时必须保证.lib存在,否则无法正常启动;

q610098308的专栏 6544

多线程编程示例1(结合实例)

1.CreateThread与_beginthreadex #pragma once #include<cstdio> #include<Windows.h> #include<crtdbg.h> #include<process.h> //子线程函数 DWORD WINAPI ThreadFun1(LPVOID ...

160

如何在OBS中添加实时时间戳:Lua脚本配置教程(附效果展示)

本文详细介绍了如何在OBS中通过Lua脚本配置实时时间戳功能,包括环境准备、脚本获取、安装配置及高级定制。适用于直播和视频录制场景,提供精准回溯和风格自定义,支持多平台适配和性能优化,是专业制作的必备技巧。

day7的博客 1039

保姆级教程:手把手配置AutoSar诊断复位(Dcm 0x11服务联动BswM/EcuM)

本文提供AutoSar诊断复位(Dcm 0x11服务联动BswM/EcuM)的保姆级配置教程,详细讲解从诊断服务基础配置到BswM规则引擎搭建、服务端口映射及EcuM复位执行的完整流程。通过Vector配置工具的实际截图和代码片段,帮助开发者高效实现远程复位功能,并分享调试技巧与性能优化实践。

weixin_30756499的博客 543

_ b e g i n t h r e a d e x函数与C r e a t e T h r e a d函数

若要使多线程C和C + +程序能够正确地运行,必须创建一个数据结构,并将它与使用 C / C + +运行期库函数的每个线程关联起来。当你调用C / C + +运行期库时,这些函数必须知道查 看调用线程的数据块,这样就不会对别的线程产生不良影响。 那么系统是否知道在创建新线程时分配该数据块呢?回答是它不知道。系统根本不知道你 得到的应用程序是用C / C + +编写的,也不知道你调用函数的线

autumn20080101的专栏 4682

【雷达成像】基于matlab SAR雷达成像仿真【含Matlab源码 3750期】

SAR雷达成像仿真 完整的代码,方可运行;可提供运行操作视频!适合小白!

订阅付费专栏Matlab(奶茶价版),可赠送奶茶价版付费专栏指定代码1份; 1081

C/C++ _beginthreadex 多线程操作

深入研究_beginthreadex的创建线程的方式,写下心得!

cpp_learner的博客 9180

【ArcGIS遇上Python】ArcGIS10.8 Python代码批量完美实现MODIS NDVI数据格式转换和投影变换

由于论文的需要,将MODIS NDVI数据进行投影变换和格式转换,具体操作可以参照:《ArcGIS10.8完美实现MODIS NDVI数据格式转换和投影变换》,但是该文章中的做法只能一次性实现一景影像的转换,没法批量,虽然ArcGIS中提供了Batch的方法但是需要挨个添加数据,确定输出路径等等,本文就实现以ArcGIS10.8 Python代码批量完美实现MODIS NDVI数据格式转换和投影变换。先来看投影后的效果: 在实现批量投影转换之前,需要两个投影文件:Sinusoidal.prj和MyAl

智慧空间实验室 3814

【OpenClaw从入门到精通】第15篇:国产芯片适配实战——昇腾/龙芯/天数智芯全流程跑通(2026实测版)

2026年国产芯片AI适配迎来爆发期,OpenClaw作为轻量化本地AI智能体,已实现华为昇腾、龙芯、天数智芯等主流国产平台的全流程适配。本文基于实测数据,从背景解析、核心原理、算法优化、实操部署四个维度,详细讲解OpenClaw在香橙派昇腾开发板、龙芯3B6000M、天数彤央TY1200及联想开天X7h的部署流程,同时提供统一工具玄武CLI的使用指南。文中包含完整代码、执行结果、避坑技巧及虚拟实战案例,兼顾新手入门与进阶提升,帮助读者快速掌握国产芯片上的本地AI部署能力,实现数据隐私保护与低成本AI落地。

专注于人工智能、软件开发、工控自动化、工厂数字化及智能化等领域,希望和大家共同进步! 1004

_beginThreadex创建多线程解读

_beginThreadex创建多线程解读 一、需要的头文件支持  #include          // for _beginthread() 需要的设置:ProjectàSetting-->C/C++-->User run-time library 选择Debug Multithreaded 或者Multithreaded。即使用: MT或MTD。 源码如下: #include

铭毅天下Elasticsearch 5万+

IEC 60747-8 是由国际电工委员会(IEC)制定的标准,标题为《半导体器件 - 分立器件 - 第8部分:场效应晶体管》

IEC 60747-8 是由国际电工委员会(IEC)制定的标准,标题为《半导体器件 - 分立器件 - 第8部分:场效应晶体管》(Semiconductor devices - Discrete devices - Part 8: Field-effect transistors)。该标准详细规定了场效应晶体管(FETs)的测试方法,涵盖静态和动态特性测试,适用于多种类型的FET,包括结型场效应晶体管(JFETs)、绝缘栅场效应晶体管(MOSFETs,增强型和耗尽型)等。3. 标准其他部分(简要概述)

专注于前沿技术的探索与实践,热衷于从代码逻辑、架构设计到行业应用等多维度深挖技术本质。期待在技术之路上与你共同成长~ 571

C/C++ 实现多线程与线程同步

多线程中的线程同步可以使用,CreateThread,CreateMutex 互斥锁实现线程同步,通过临界区实现线程同步,Semaphore 基于信号实现线程同步,CreateEvent 事件对象的同步,以及线程函数传递单一参数与多个参数的实现方式。其主要是线程函数中调用的参数会与下一个线程函数结构相冲突,解决的办法时在每次进入线程函数时,自己拷贝一份,每个人使用自己的那一份,才可以避免此类事件的发生,同时最好配合线程同步一起使用,如下时线程扫描器的部分代码片段。通过互斥锁,同步执行两个线程函数。

微软技术分享 1万+

FCTA/FCTB 前方交通穿行预警/制动系统技术规范

FCTA&FCTB前方交叉路口预警制动系统是针对十字路口和停车场等"鬼探头"场景设计的驾驶辅助系统。系统在自车速度0-40km/h范围内工作,通过角雷达和摄像头检测侧向来车.

未知艺术家的博客 737

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

_beginthreadex 创建多线程解读

一、解释 (1)如果你正在编写C/C++代码,决不应该调用CreateThread。相反,应该使用VisualC++运行期库函数_beginthreadex,退出也应该使用_endthreadex。如果不使用Microsoft的VisualC++编译器,你的编译器供应商有它自己的CreateThread替代函数。不管这个替代函数是什么,你都必须使用。 (2)因为_beginthreadex和_en...

Keep Moving~ 1507

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

多线程1: CreateThread与_beginthreadex创建多线程

本文将带领你与多线程作第一次亲密接触,并深入分析CreateThread与_beginthreadex的本质区别,相信阅读本文后你能轻松的使用多线程并能流畅准确的回答CreateThread与_beginthreadex到底有什么区别,在实际的编程中到底应该使用CreateThread还是_beginthreadex? 使用多线程其实是非常容易的,下面这个程序的主线程会创建了一个子线程并等待

chlele0105的专栏 1万+

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

使用C/C++运行时库函数操作线程

Visual Studio包含了4个本机C/C++运行时库和2个用来管理MS.NET的C/C++运行时库。所有这些库都支持多线程编程环境:目前已经没有专门为单线程开发设计的C/C++运行时库了。表6-1对这些库进行了描述: Libray Name Description LibCMt.libStatically linke

xiewenqi.cs 3878

C++多线程系列(一)CreateThread和_beginthreadex区别

现在在学习多线程,顺便将搜集到的资料整理下来以供参考和查询。 首先在开始多线程学习的时候遇到的首要问题便是多线程的创建,在查阅资料后有CreateThread和_beginthreadex两种方法,可能不止这两种,以后学习到了再补充。

烟雨博客 3372
上一篇: C/C++ _beginthreadex 多线程操作
下一篇: C/C++ 并行搜索 算法
cpp_learners
博客等级 码龄7年 1526粉丝 274原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

打赏作者

cpp_learners

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

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

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

打赏作者

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

抵扣说明:

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

余额充值