【Windows线程开发】Windows线程同步技术

news2025/1/31 3:15:40

我们在上一章节中讲解了关于Windows的线程基础,相信大家已经对线程有了基本的概念。这一章节中,我们来讲讲线程同步技术,包括加锁技术(原子锁和互斥体)和事件,信号灯。

文章目录

  • 一.原子锁
  • 二.互斥体
  • 三.事件
  • 四.信号灯

一.原子锁

原子锁主要解决的问题是多线程在操作符方面的问题。

  • 相关问题:

    多个线程对同一个数据进行原子操作时,会产生结果丢失,比如++运算符

我们来写一段代码看看多线程在操作同一个数据的时候出现的问题:

#include <stdio.h>
#include <windows.h>

DWORD WINAPI ThreadProc1(LPVOID lpParameter);
DWORD WINAPI ThreadProc2(LPVOID lpParameter);
int g_value = 0;

int main() {
	DWORD nID = 0;
	HANDLE hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &nID);
	HANDLE hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &nID);
	WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
	printf("%d\n", g_value);
	return 0;
}

DWORD WINAPI ThreadProc1(LPVOID lpParameter) {
	for (int i = 0; i < 100000000; i++) {
		g_value++;
	}
	return 0;
}


DWORD WINAPI ThreadProc2(LPVOID lpParameter) {
	for (int i = 0; i < 100000000; i++) {
		g_value++;
	}
	return 0;
}
  • 代码解释
    我们创建两个线程,同时对全局变量g_value进行自增操作,两个线程分别自增100000000次,那么最后结果就应该是200000000,我们来看看执行结果:
    原子锁相关问题
    我们发现,最后结果并不是200000000,那么是为什么呢?

我们来分析一下错误:
当线程A执行g_value++时,如果线程切换正好是在线程A将结果保存到g_value之前,线程B继续执行g_value++,那么当线程A再次被切换回来之后,会继续上一步的操作,继续将值保存到g_value中,线程B的计算结果被覆盖

通俗点来说,就是线程A计算好了g_value的结果,但是还没有保存到g_value,这时候线程切换到了B线程,线程B完成了计算,并且成功保存,当返回到A线程的时候,A线程会继续上一步的保存操作,那么B线程的计算结果就被覆盖掉了。

那么如何来解决这样的问题呢?那就要用到我们的线程同步技术—原子锁了:

  • 原子锁函数:
    InterlockedIncrement()
    InterlockedDecrement()
    InterlockedCompareExcahnge()
    InterlockedExchange()
    我们在上文中提到,原子锁主要针对的是运算符的问题,每一种运算符都有原子锁函数
    我们来看看使用效果:这里以++运算符为例:
#include <stdio.h>
#include <windows.h>

DWORD WINAPI ThreadProc1(LPVOID lpParameter);
DWORD WINAPI ThreadProc2(LPVOID lpParameter);
DWORD g_value = 0;

int main() {
	DWORD nID = 0;
	HANDLE hThread[2] = { 0 };
	hThread[0] = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &nID);
	hThread[1] = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &nID);
	WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
	printf("%d\n", g_value);
	return 0;
}

DWORD WINAPI ThreadProc1(LPVOID lpParameter) {
	for (int i = 0; i < 100000000; i++) {
		InterlockedIncrement(&g_value);
	}
	return 0;
}


DWORD WINAPI ThreadProc2(LPVOID lpParameter) {
	for (int i = 0; i < 100000000; i++) {
		InterlockedIncrement(&g_value);
	}
	return 0;
}

我们来看看执行效果:
原子锁执行效果2
我们可以发现,当我们使用原子锁的时候,两个线程操作同一个数据,就不会出现结果丢失的问题了。但是我们也不难发现,执行结果慢了很多,这是因为执行过程中多了很多等待事件,这个等待我们在互斥中会讲到。
原子锁的实现:直接对数据所在的内存操作,并且在任何一个瞬间,只能有一个线程访问

二.互斥体

  • 相关问题
    跟原子锁一样,都是解决多线程下资源的共享使用,但是与原子锁不同的是,互斥体解决的是代码资源的共享使用。
  • 互斥体的使用:
      1. 创建互斥体:
        使用CreateMutex函数:
        MSDN官方文档解释
HMODLE CreateMutex(
	LPSECURITY_ATTRIBUTES lpMutexAttributes,    //安全性
	BOOL bInitialOwner,        //初始拥有者
	LPCSTR lpName              //为互斥命名
);

参数bInitialOwner介绍:
如果此值为 TRUE ,并且调用方创建了互斥体,则调用线程获取互斥体对象的初始所有权。 否则,调用线程不会获取互斥体的所有权。

互斥体特性介绍:

  1. 在任何一个时间点上,只能由一个线程拥有互斥体
  2. 当前任何一个线程不拥有互斥体是,互斥体句柄有信号
  3. 谁先等候互斥体,谁先获取
    1. 等候互斥体:
      上一篇介绍过了,使用等候句柄函数。WaitFor...
    1. 释放互斥体
BOOL ReleaseMutex(
	HANDLE hMutex           //handle of Mutex
);
    1. 关闭互斥体
      使用CloseHandle函数
      我们来看看使用互斥体来解决我们在多线程中遇到的问题:
#include <stdio.h>
#include <windows.h>

DWORD WINAPI ThreadProc1(LPVOID lpParameter);
DWORD WINAPI ThreadProc2(LPVOID lpParameter);
DWORD g_value = 0;
HANDLE hMutex = NULL; //用于接收互斥体句柄

int main() {
	DWORD nID = 0;
	HANDLE hThread[2] = { 0 };
	//hMutex = CreateMutex(NULL, FALSE, NULL);
	hThread[0] = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &nID);
	hThread[1] = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &nID);
	WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
	printf("%d\n", g_value);
	CloseHandle(hMutex);
	return 0;
}

DWORD WINAPI ThreadProc1(LPVOID lpParameter) {
	char a[] = "********";
	
	while (1) {
		//WaitForSingleObject(hMutex, INFINITE);
		for (int i = 0; i < strlen(a); i++) {
			printf("%c", a[i]);
			Sleep(125);
		}
		printf("\n");
		//ReleaseMutex(hMutex);
	}
	return 0;
}


DWORD WINAPI ThreadProc2(LPVOID lpParameter) {
	char b[] = "--------";
	while (1) {
		//WaitForSingleObject(hMutex, INFINITE);
		for (int i = 0; i < strlen(b); i++) {
			printf("%c", b[i]);
			Sleep(125);
		}
		//ReleaseMutex(hMutex);
		printf("\n");
	}
	return 0;
}

我们来看看不适用互斥体技术的时候的输出:
不适用互斥体技术
我们再来看看使用了互斥体之后:

#include <stdio.h>
#include <windows.h>

DWORD WINAPI ThreadProc1(LPVOID lpParameter);
DWORD WINAPI ThreadProc2(LPVOID lpParameter);
DWORD g_value = 0;
HANDLE hMutex = NULL; //用于接收互斥体句柄

int main() {
	DWORD nID = 0;
	HANDLE hThread[2] = { 0 };
	hMutex = CreateMutex(NULL, FALSE, NULL);
	hThread[0] = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &nID);
	hThread[1] = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &nID);
	WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
	printf("%d\n", g_value);
	CloseHandle(hMutex);
	return 0;
}

DWORD WINAPI ThreadProc1(LPVOID lpParameter) {
	char a[] = "********";
	
	while (1) {
		WaitForSingleObject(hMutex, INFINITE);
		for (int i = 0; i < strlen(a); i++) {
			printf("%c", a[i]);
			Sleep(125);
		}
		printf("\n");
		ReleaseMutex(hMutex);
	}
	return 0;
}


DWORD WINAPI ThreadProc2(LPVOID lpParameter) {
	char b[] = "--------";
	while (1) {
		WaitForSingleObject(hMutex, INFINITE);
		for (int i = 0; i < strlen(b); i++) {
			printf("%c", b[i]);
			Sleep(125);
		}
		ReleaseMutex(hMutex);
		printf("\n");
	}
	return 0;
}

使用互斥体
我们可以发现使用互斥体之后,对代码段进行了枷锁。
我们来大致讲解一下互斥体的实现吧:

我们在主进程中创建了互斥体,并且互斥体不归之进程所有,两个线程谁先等待互斥体句柄,谁就拥有了互斥体,那么当线程跳转到另一个线程之后,发现被锁定在了另一个线程,那么线程就会被阻塞,直到线程再次跳转到另一个线程,执行完之后,互斥体被释放,这时候跳转到这个线程,在这个线程中再进行加锁,这个线程执行完之后,再锁定到另一个线程,这样就实现了加锁技术。

三.事件

前两个技术都属于加锁技术,即两个线程互斥的时候使用,那么线程也会有协调工作的时候,这时候就需要用到我们的事件和信号量了。

  • 相关问题:
    多线程协调工作的时候的通知问题

  • 事件的使用

    • 创建事件:
      使用CreatEvent函数,MSDN官方解释
HANDLE CreateEvent(
	LPSECURITY_ATTRIBUTES lpEventAttributes, //安全属性
	BOOL                  bManualReset,   //事件重置/复位方式
	BOOL                  bInitialState, //事件初始状态
	LPCSTR                lpName  //为事件命名
);
  • 参数解释:
  • bManualReset为事件重置/复位方式,如果该参数被设置为TRUE那么就需要我们来手动重置事件对象,如果该参数被设置为FALSE,那么操纵系统会帮我们完成事件的重置和复位。
  • bInitialState:该参数指定了当创建事件后,该事件句柄是否处于有消息状态
  • 等候事件:
    WaitFor......函数
  • 触发事件(使事件句柄处于有消息状态)
BOOLSetEvent(
	HANDLE hEvent
);
  • 复位事件(将事件句柄设置为无消息状态)
BOOL ResetEvent(
	HANDLE hEvent
);
  • 关闭事件
    CloseHandle函数
    我们来看看事件的使用:
#include <stdio.h>
#include <windows.h>

DWORD WINAPI ThreadProc1(LPVOID lpParameter);
DWORD WINAPI ThreadProc2(LPVOID lpParameter);
DWORD g_value = 0;
HANDLE hEvent = NULL; //用于接收事件句柄

int main() {
	DWORD nID = 0;
	HANDLE hThread[2] = { 0 };
	hEvent = CreateEvent(NULL,FALSE,0,NULL);
	hThread[0] = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &nID);
	hThread[1] = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &nID);
	WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
	printf("%d\n", g_value);
	CloseHandle(hEvent);
	return 0;
}

DWORD WINAPI ThreadProc1(LPVOID lpParameter) {
	char a[] = "********";
	while (1) {
		WaitForSingleObject(hEvent, INFINITE);
		for (int i = 0; i < strlen(a); i++) {
			printf("%c", a[i]);
		}
		printf("\n");
		ResetEvent(hEvent);
	}
	return 0;
}


DWORD WINAPI ThreadProc2(LPVOID lpParameter) {
	while (1) {
		Sleep(1000);
		SetEvent(hEvent);
	}
	return 0;
}

这是一个很典型的相互协调工作的双线程,我们在A线程中没有设定时间间隔,但是在B线程中设定了事件间隔,我们能够很明显地感受到输出是有时间间隔的:
事件处理

四.信号灯

  • 相关问题:
    类似于事件,解决线程之间通知的相关问题,但提供一个计数器,可以设置次数。
  • 创建信号灯:
    使用CreateSemaphore函数:
    MSDN官方解释
HANDLE CreateSemaphore(
	LPSECURITY_ATTRIBUIES lpSemaphoreAttributes,         //安全属性
	LONG lInitialCount,        //初始化信号灯数量
	LONG lMaximumCount,        //信号灯的最大值
	LPSTSTR lpName             //为信号灯命名
);创建成功返回信号量句柄
  • 等候信号量
    使用WaitFor...函数
    注意: 等候每通过一次,信号灯的信号减一,知道为0阻塞
  • 给信号灯指定计数值
    使用ReleaseSemaphore()函数
    MSDN官方解释
BOOL ReleaseSemaphore(
	HANDLE hSeamephore,           //信号灯句柄
	LONG lReleaseSemaphore,       //信号灯将增加的量
	LPONG lpPreviousCount         //指向一个变量的指针,用于记录信号灯的上一个计数
);
  • 关闭信号灯:
    使用CloseHandle()函数
    我们来看看信号灯的使用实例:
#include <stdio.h>
#include <windows.h>

DWORD WINAPI ThreadProc1(LPVOID lpParameter);
DWORD WINAPI ThreadProc2(LPVOID lpParameter);
HANDLE hSemaphore = NULL; //用于接收事件句柄

int main() {
	DWORD nID = 0;
	HANDLE hThread[2] = { 0 };
	hSemaphore = CreateSemaphore(NULL, 3, 10, NULL);
	hThread[0] = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &nID);
	while (getchar()=='\n') {
		ReleaseSemaphore(hSemaphore, 5, NULL);
	}
	CloseHandle(hSemaphore);
	return 0;
}

	DWORD WINAPI ThreadProc1(LPVOID lpParameter) {
		char a[] = "********";
		while (1) {
			WaitForSingleObject(hSemaphore, INFINITE);
			for (int i = 0; i < strlen(a); i++) {
				printf("%c", a[i]);
			}
			printf("\n");
		}
		return 0;
	}

我们设置了计数器为3的信号灯,我们发现程序最开始只会输出三行,每当我们按一次回车键,就将信号灯计数值值为5:
信号灯实例
本篇文章的分享就到这里,如果大家发现有错误之处,还请大家指出来,我会非常虚心地学习。希望我们共同进步!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/521261.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringCloud-网关 Gateway

网关Gateway 一、网关初识二、网关的使用1.创建项目并引入依赖2.编写网关配置3.启动服务并测试 三.查看网关路由规则列表四.路由服务的负载均衡五.断言和过滤1.断言Predicate1.1.The Path Route Predicate Factory(路径断言工厂&#xff09;1.2.The After Route Predicate Fact…

【27】linux进阶——rpm软件包的管理

大家好&#xff0c;这里是天亮之前ict&#xff0c;本人网络工程大三在读小学生&#xff0c;拥有锐捷的ie和红帽的ce认证。每天更新一个linux进阶的小知识&#xff0c;希望能提高自己的技术的同时&#xff0c;也可以帮助到大家 另外其它专栏请关注&#xff1a; 锐捷数通实验&…

基于SSM+MySQL的支教管理系统

目录 1、系统介绍 2、系统功能展示 3、运行环境 4、运行效果 5、运行配置 6、下载地址 1、系统介绍 本系统是基于SSM(SpringSpringMVCMyBatis)框架开发的支教管理系统&#xff0c;系统共有超级管理员&#xff0c;系统管理员&#xff0c;支教学校&#xff0c;志愿者这四大…

我的新书上架了!

talk is cheap&#xff0c;show you my book&#xff01; 新书《从0开始学ARM》终于在各大平台上架了&#xff01;&#xff01; 一、关于本书 1. 本书主要内容 ARM体系架构是目前市面上的主流处理器体系架构&#xff0c;在手机芯片和嵌入式芯片领域&#xff0c;ARM体系架构…

前端架构师-week5-命令行交互原理

目录 加餐&#xff1a;命令行交互原理 学习路径 readline 源码分析 如何开发命令行交互列表 实现原理 架构图 本章学习路径和学习目标 readline 的使用方法和实现原理 高能&#xff1a;深入讲解 readline 键盘输入监听实现原理 秀操作&#xff1a;手写 readline 核心实…

MySQL---控制流函数、窗口函数(序号函数、开窗聚合函数、分布函数、前后函数、头尾函数、其他函数)

1. 控制流函数 格式 解释 案例 IF(expr,v1,v2) 如果表达式 expr 成立&#xff0c;返回结果 v1&#xff1b;否则&#xff0c;返回结果 v2。 SELECT IF(1 > 0,正确,错误) ->正确 IFNULL(v1,v2&#xff09; 如果 v1 的值不为 NULL&#xff0c;则返回 v1&#xff…

JVM垃圾收集器(一)

目录 1、如何考虑 GC 2、如何确定一个对象“死去” 3、分代收集理论 4、垃圾回收算法 5、HotSpot的算法实现细节 1、如何考虑 GC 垃圾收集&#xff08;Garbage Collection&#xff0c;GC&#xff09;的历史比Java更久远&#xff0c;1960年诞生于MIT。 GC 需要考虑的三件事…

UNIAPP实战项目笔记72 提交订单到选择支付方式的前后端交互

UNIAPP实战项目笔记72 提交订单到选择支付方式的前后端交互 思路 购物车确认订单,跳转到订单确认界面确认支付后清除购物车对应id的数据 实例截图 清空购物车数据后 代码 前端代码 order.js export default{state:{// 订单号orderNumber:},getters:{},mutations:{initOr…

数字化转型浪潮下,如何选择适合企业的低代码平台

近日&#xff0c;艾瑞咨询发布了《数字新生态&#xff1a;中国低代码厂商发展白皮书》&#xff08;以下简称“报告”&#xff09;&#xff0c;在该报告中&#xff0c;艾瑞咨询对中国当前的低代码市场进行了非常细致的解构&#xff0c;并针对当前企业数字化转型&#xff0c;对低…

电阻传感器工作原理

金属随着温度变化&#xff0c;其电阻值也发生变化。 对于不同金属来说&#xff0c;温度每变化一度&#xff0c;电阻值变化是不同的&#xff0c;而电阻值又可以直接作为输出信号。 电阻共有两种变化类型 正温度系数 温度升高 阻值增加 温度降低 阻值减少 负温度系数 温…

【一起啃书】《机器学习》第七章 贝叶斯分类器

文章目录 第七章 贝叶斯分类器7.1 贝叶斯决策论7.2 极大似然估计7.3 朴素贝叶斯分类器7.4 半朴素贝叶斯分类器7.5 贝叶斯网7.6 EM算法 第七章 贝叶斯分类器 7.1 贝叶斯决策论 对分类任务来说&#xff0c;在所有相关概率都已知的理想情形下&#xff0c;贝叶斯决策论考虑如何基于…

k8s学习-CKS真题-利用AppArmor进行应用行为限制

目录 题目环境搭建解题模拟题参考 题目 Task 在 cluster 的工作节点 node02 上&#xff0c;实施位于 /etc/apparmor.d/nginx_apparmor 的现有 APPArmor 配置文件。 编辑位于 /cks/KSSH00401/nginx-deploy.yaml 的现有清单文件以应用 AppArmor 配置文件。 最后&#xff0c;应用清…

UE4及Airsim安装时遇到的问题及解决办法

UE4及Airsim安装时遇到的问题及解决办法 目录 UE4及Airsim安装时遇到的问题及解决办法前言UE4下载慢解决方法 Airsim编译过程中提示&#xff1a;无法打开包括文件: “Eigen/Dense”: No such file or directory [D:\software\Visual_studio2022\2022Community\AirSim\Air解决办…

C语言——运算符和表达式

所谓表达式就是指由运算符、运算量和标点符号组成的有效序列&#xff0c;其目的是说明一个计算过程。表达式可以独立成语句&#xff1a;表达式; 运算符按功能分为&#xff1a;算术运算、赋值运算、关系运算、逻辑运算、位运算以及其他运算符 1. 算术运算符&#xff1a; - * / %…

项目部署 | Linux安装Git和Maven

知识目录 一、写在前面✨二、安装Git&#x1f495;2.1 yum安装git2.2 新建Git仓库2.3 拉取仓库代码 三、安装Maven&#x1f495;3.1 上传Maven压缩包并解压3.2 配置环境变量3.3 设置本地仓库3.4 设置中央仓库 四、总结撒花&#x1f60a; 一、写在前面✨ 大家好&#xff01;我是…

二叉树的递归遍历与迭代遍历(图示)

文章目录 前言1. 二叉树的递归遍历&#xff08;一入递归深似海&#xff0c;从此offer是路人&#xff09;1.1 [前序遍历](https://leetcode.cn/problems/binary-tree-preorder-traversal/)1.2 [中序遍历](https://leetcode.cn/problems/binary-tree-inorder-traversal/)1.3 [后序…

实验一 Python基础编程

实验一 Python基础编程 只为给原因学习编程的同学提供一个思路&#xff0c;让编程更简单&#xff01;&#xff01;&#xff01; 本博主擅长整理粉丝的私信&#xff01;只要你有需求就可以告诉博主&#xff01;博主可以帮你解决并发表&#xff01; 一、实验学时 2学时 二、实…

docker发布到dockerhub报错denied: requested access to the resource is denied

docker发布到dockerhub报错denied: requested access to the resource is denied 解决方案 修改发布的镜像的REPOSITORY为自己的账户名镜像&#xff0c;比如我的用户名是luobotoutou123。docker tag tomcat02:1.0 luobotoutou123/tomcat02:1 然后发布镜像 到dockerhub远程仓库…

学习杂记 2023.5.13 单词背诵

目录 鼠标上的DPI是什么&#xff1f; 鼠标上的DPI是什么&#xff1f; DPI是英文Dots Per Inch的缩写&#xff0c;意思是每英寸点数。在计算机中&#xff0c;DPI通常用于描述指针设备&#xff08;例如鼠标&#xff09;的精度。在鼠标上&#xff0c;DPI指的是鼠标移动时指针在屏…

[图神经网络]ViG(Vision GNN)网络代码实现

论文解读&#xff1a; [图神经网络]视觉图神经网络ViG(Vision GNN)--论文阅读https://blog.csdn.net/weixin_37878740/article/details/130124772?spm1001.2014.3001.5501代码地址&#xff1a; ViGhttps://github.com/huawei-noah/Efficient-AI-Backbones/tree/master/vig_p…