《TCP/IP网络编程》学习笔记 | Chapter 20:Windows 中的线程同步

《TCP/IP网络编程》学习笔记 | Chapter 20:Windows 中的线程同步

  • 《TCP/IP网络编程》学习笔记 | Chapter 20:Windows 中的线程同步
    • 用户模式和内核模式
      • 用户模式同步
      • 内核模式同步
    • 基于 CRITICAL_SECTION 的同步
    • 内核模式的同步方法
      • 基于互斥量对象的同步
      • 基于信号量对象的同步
      • 基于事件对象的同步

《TCP/IP网络编程》学习笔记 | Chapter 20:Windows 中的线程同步

用户模式和内核模式

Windows操作系统的运行方式是“双模式操作”(Dual-mode Operation):

  • 用户模式(User mode):运行应用程序的基本模式,禁止访问物理设备,而且会限制访问的内存区域。
  • 内核模式(Kernal mode):操作系统运行时的模式,不仅不会限制访问的内存区域,而且访问的硬件设备也不会受限。

实际上,在应用程序运行过程中,Windows操作系统不会一直停留在用户模式,而是在用户模式和内核模式之间切换。

例如,可以在Windows中创建线程。虽然创建线程的请求是由应用程序的函数调用完成,但实际创建线程的是操作系统。因此,创建线程的过程中无法避免向内核模式的转换。

定义这2种模式主要是为了提高安全性。应用程序的运行时错误会破坏操作系统及各种资源。特别是C/C++可以进行指针运算,很容易发生这类问题。例如,因为错误的指针运算覆盖了操作系统中存有重要数据的内存区域,这很可能引起操作系统崩溃。但实际上各位从未经历过这类事件,因为用户模式会保护与操作系统有关的内存区域。因此,即使遇到错误的指针运算也仅停止应用程序的运行,而不会影响操作系统。

总之,像线程这种伴随着内核对象创建的资源创建过程中,都要默认经历如下模式转换过程:用户模式→内核模式→用户模式。

从用户模式切换到内核模式是为了创建资源,从内核模式再次切换到用户模式是为了执行应用程序的剩余部分。不仅是资源的创建,与内核对象有关的所有事务都在内核模式下进行。

模式切换对系统而言其实也是一种负担,频繁的模式切换会影响性能。

用户模式同步

用户模式同步是用户模式下进行的同步,即无需操作系统的帮助而在应用程序级别进行的同步。

用户模式同步的最大优点是——速度快。无需切换到内核模式,仅考虑这一点也比经历内核模式切换的其他方法要快。而且使用方法相对简单,因此,适当运用用户模式同步并无坏处。

但因为这种同步方法不会借助操作系统的力量,其功能上存在一定局限性。稍后将介绍属于用户模式同步的、基于“CRITICAL_SECTION”的同步方法。

内核模式同步

下面给出内核模式同步的优点。

  • 比用户模式同步提供的功能更多。
  • 可以指定超时,防止产生死锁。

因为都是通过操作系统的帮助完成同步的,所以提供更多功能。特别是在内核模式同步中,可以跨越进程进行线程同步。因为内核对象并不属于某一进程,而是操作系统拥有并管理的。

与此同时,由于无法避免用户模式和内核模式之间的切换,所以性能上会受到一定影响。

基于 CRITICAL_SECTION 的同步

基于 CRITICAL_SECTION 的同步中将创建并运用“CRITICAL_SECTION对象”,但这并非内核对象。与其他同步对象相同,它是进入临界区的一把“钥匙”。因此,为了进入临界区,需要得到 CRITICAL_SECTION 对象这把“钥匙”。相反,离开时应上交 CRITICAL_SECTION 对象。

下面介绍 CRITICAL_SECTION 对象的初始化及销毁相关函数。

#include<windows.h>void InitilizerCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

参数:

  • IpCriticalSection:InitilizerCriticalSection 函数中传入需要初始化的 CRITICAL_SECTION 对象的地址值,DeleteCriticalSection 函数中传入需要解除的 CRITICAL_SECTION 对象的地址值。

上述函数的参数类型 LPCRITICAL_SECTION 是 CRITICAL_SECTION 指针类型。另外 DeleteCriticalSection 函数并不销毁CRITICAL_SECTION 对象。该函数的作用是销毁 CRITICAL_SECTION 对象相关的资源。

接下来介绍获取(拥有)及释放 CRITICAL_SECTION 对象的函数,可以简单理解为获取和释放“钥匙”的函数。

#include<windows.h>void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
void LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

参数:

  • IpCriticalSection:获取(拥有)及释放 CRITICAL_SECTION 对象的地址值。

与 Linux 部分中介绍过的互斥量类似,相信大部分人仅靠这些函数介绍也能写出示例程序。

示例程序:

#include <stdio.h>
#include <stdlib.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;
CRITICAL_SECTION cs;int main(int argc, char *argv[])
{HANDLE tHandles[NUM_THREAD];int i;InitializeCriticalSection(&cs);for (i = 0; i < NUM_THREAD; i++){if (i % 2)tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadInc, NULL, 0, NULL);elsetHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0, NULL);}WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);DeleteCriticalSection(&cs);printf("result: %lld \n", num);system("pause");return 0;
}unsigned WINAPI threadInc(void *arg)
{int i;EnterCriticalSection(&cs);for (i = 0; i < 50000000; i++)num += 1;LeaveCriticalSection(&cs);return 0;
}unsigned WINAPI threadDes(void *arg)
{int i;EnterCriticalSection(&cs);for (i = 0; i < 50000000; i++)num -= 1;LeaveCriticalSection(&cs);return 0;
}

运行结果:

在这里插入图片描述

程序将整个循环纳入临界区,可以减少运行时间。

内核模式的同步方法

基于互斥量对象的同步

基于互斥量(Mutual Exclusion)对象的同步方法与基于 CRITICAL_SECTION 对象的同步方法类似,因此,互斥量对象同样可以理解为“钥匙”。

首先介绍创建互斥量对象的函数:

#include<windows.h>HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes,BOOL bInitialOwner,LPCTSTR lpName
);

参数:

  • lpMutexAttributes:传递安全相关的配置信息,使用默认安全设置时可以传递 NULL。
  • blnitialOwner:如果为 TRUE,则创建出的互斥量对象属于调用该函数的线程,同时进入 non-signaled 状态;如果为 FALSE,则创建出的互斥量对象不属于任何线程,此时状态为 signaled。
  • IpName:用于命名互斥量对象。传入 NULL 创建无名的互斥量对象。

成功时返回创建的互斥量对象句柄,失败时返回 NULL。

从上述参数说明中可以看到,如果互斥量对象不属于任何拥有者,则将进入 signaled 状态。利用该特点进行同步。

另外,互斥量属于内核对象,所以通过如下函数销毁:

#include<windows.h>BOOL CloseHandle(HANDLE hObject);

参数:

  • hObject:要销毁的内核对象的句柄。

成功时返回 TRUE,失败时返回 FALSE。

上述函数是销毁内核对象的函数,所以同样可以销毁即将介绍的信号量及事件。下面介绍获取和释放互斥量的函数,但我认为只需介绍释放的函数,因为获取是通过各位熟悉的 WaitForSingleObject 函数完成的。

#include<windows.h>BOOL ReleaseMutex(HANDLE hMutex);

参数:

  • hMutex:需要释放的互斥量对象句柄。

成功时返回 TRUE,失败时返回 FALSE。

接下来分析获取和释放互斥量的过程。互斥量被某一线程获取时(拥有时)为 non-signaled 状态,释放时(未拥有时)进入 signaled 状态。因此,可以使用 WaitForSingleObject 函数验证互斥量是否已分配。该函数的调用结果有如下 2 种。

  • 调用后进入阻塞状态:互压量对象已被其他线程获取,现处于 non-signaled 状态。
  • 调用后直接返回:其他线程未占用互斥量对象,现处于 signaled 状态。

互斥量在 WaitForSingleObject 函数返回时自动进入 non-signaled 状态,因为它是第 19 章介绍过的"auto-reset"模式的内核对象。结果,WaitForSingleObject 函数为申请互斥量时调用的函数。因此,基于互斥量的临界区保护代码如下:

WaitForsingleobject(hMutex, INFINITE);
// 临界区的开始
// ......
// 临界区的结束
ReleaseMutex(hMutex);

WaitForSingleObject 函数使互斥量进入 non-signaled 状态,限制访问临界区,所以相当于临界区的门禁系统。相反,ReleaseMutex 函数使互斥量重新进入 signaled 状态,所以相当于临界区的出口。

示例程序:

#include <stdio.h>
#include <stdlib.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(int argc, char *argv[])
{HANDLE tHandles[NUM_THREAD];int i;hMutex = CreateMutex(NULL, FALSE, NULL);for (i = 0; i < NUM_THREAD; i++){if (i % 2)tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadInc, NULL, 0, NULL);elsetHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0, NULL);}WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);CloseHandle(hMutex);printf("result: %lld \n", num);system("pause");return 0;
}unsigned WINAPI threadInc(void *arg)
{int i;WaitForSingleObject(hMutex, INFINITE);for (i = 0; i < 50000000; i++)num += 1;ReleaseMutex(hMutex);return 0;
}unsigned WINAPI threadDes(void *arg)
{int i;WaitForSingleObject(hMutex, INFINITE);for (i = 0; i < 50000000; i++)num -= 1;ReleaseMutex(hMutex);return 0;
}

运行结果:

在这里插入图片描述

基于信号量对象的同步

Windows 中基于信号量对象的同步也与 Linux 下的信号量类似,二者都是利用名为“信号量值”的整数值完成同步的,而且该值都不能小于 0。当然,Windows 的信号量值注册于内核对象。

下面介绍创建信号量对象的函数,其销毁同样是利用 CloseHandle 函数进行的。

#include <windows.h>HANDLE Createsemaphore(LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,LONG lInitialCount,LONG lMaximumCount,LPCTSTR lpName
);

参数:

  • IpSemaphoreAttributes:安全配置信息,采用默认安全设置时传递 NULL。
  • lInitialCount:指定信号量的初始值,应大于 0 小于 lMaximumCount。
  • IMaximumCount:信号量的最大值。该值为 1 时,信号量变为只能表示 0 和 1 的二进制信号量。
  • lpName:用于命名信号量对象,传递 NULL 时创建无名的信号量对象。

成功时返回创建的信号量对象的句柄,失败时返回 NULL。

向 lInitialCount 参数传递 0 时,创建 non-signaled 状态的信号量对象。而向 IMaximumCount 传入 3 时,信号量最大值为 3,因此可以实现 3 个线程同时访问临界区时的同步。

可以利用“信量值为 0 时进入 non-signaled 状态,大于 0 时进入 signaled 状态”的特性进行同步。

下面介绍释放信号量对象的函数:

#include <windows.h>BOOL ReleaseSemaphore(HANDLE hSemaphore,LONG lReleaseCount,LPLONG lpPreviouscount
);

参数:

  • Semaphore:传递需要释放的信号量对象.
  • IReleaseCount:释放意味着信号量值的增加,通过该参数可以指定增加的值。超过最大值则不增加,返回 FALSE。
  • IpPreviousCount:用于保存修改之前值的变量地址,不需要时可传递 NULL。

成功时返回 TRUE,失败时返回 FALSE。

信号量对象的值大于 0 时成为 signaled 状态,为 0 时成为 non-signaled 状态。因此,调用 WaitForSingleObject 函数时,信号量大于 0 的情况才会返回,返回的同时将信量值减 1。可以通过如下程序结构保护临界区。

WaitForSingleObject(hSemaphore, INFINITE);
// 临界区的开始
// ......
// 临界区的结束
ReleaseSemaphore(hSemaphore, 1, NULL);

示例程序:

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <process.h>unsigned WINAPI read(void *arg);
unsigned WINAPI accu(void *arg);static HANDLE sem_one;
static HANDLE sem_two;
static int num;int main(int argc, char const *argv[])
{HANDLE hThread1, hThread2;sem_one = CreateSemaphore(NULL, 0, 1, NULL);sem_two = 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(sem_one);CloseHandle(sem_two);system("pause");return 0;
}unsigned WINAPI read(void *arg)
{int i;for (i = 0; i < 5; i++){fputs("Input num: ", stdout);WaitForSingleObject(sem_two, INFINITE);scanf("%d", &num);ReleaseSemaphore(sem_one, 1, NULL);}return 0;
}unsigned WINAPI accu(void *arg)
{int sum = 0, i;for (i = 0; i < 5; i++){WaitForSingleObject(sem_one, INFINITE);sum += num;ReleaseSemaphore(sem_two, 1, NULL);}printf("Result: %d \n", sum);return 0;
}

运行结果:

在这里插入图片描述

在循环内部构件临界区,起到尽可能缩小临界区的作用,尽量提高程序性能。

基于事件对象的同步

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

力扣45.跳跃游戏

45. 跳跃游戏 II - 力扣&#xff08;LeetCode&#xff09; 代码区&#xff1a; #include<vector> class Solution {public:int jump(vector<int>& nums) {int ans[10005] ;memset(ans,1e4,sizeof(ans));ans[0]0;for(int i0;i<nums.size();i){for(int j1;j…

深入理解 Collections.emptyList():优雅处理空列表的利器!!!

&#x1f680; 深入理解 Collections.emptyList()&#xff1a;优雅处理空列表的利器&#xff01;&#x1f527; 大家好&#xff01;&#x1f44b; 今天我们来聊聊 Java 中一个非常实用但容易被忽视的小工具——Collections.emptyList()。&#x1f389; 如果你经常需要返回一个…

SpringBoot教程(十四) SpringBoot之集成Redis

SpringBoot教程&#xff08;十四&#xff09; | SpringBoot之集成Redis 一、Redis集成简介二、集成步骤 2.1 添加依赖2.2 添加配置2.3 项目中使用之简单使用 &#xff08;举例讲解&#xff09;2.4 项目中使用之工具类封装 &#xff08;正式用这个&#xff09;2.5 序列化 &…

VC6.0图文安装教程

VC6.0图文安装教程 ​ 1、首先&#xff0c;右击安装包&#xff0c;以管理员身份运行 2、点击下一步 ​​​​ 3、点击下一步 4、选择安装路径&#xff0c;点击下一步 5、点击下一步 6、点击安装 7、安装ing 8、点击完成 至此&#xff0c;安装完成&#xff01;

用户说 | 零基础用通义灵码 AI 程序员开发个人笔记网站

作者&#xff1a;宋镇江&#xff0c;安阳幼儿师范高等专科学校数字媒体技术专业教师 通义灵码是一款基于通义大模型的智能编码辅助工具&#xff0c;支持自然语言生成代码、单元测试生成、代码注释生成等功能&#xff0c;兼容多种主流IDE和编程语言。对于零基础用户&#xff0c…

试验一 mybatis 入门操作

试验一 mybatis 入门操作 一 实验目的 1.掌握mybatis基础操作&#xff0c;包括如何在maven工程中引入依赖&#xff0c;创建mapper文件&#xff0c;核心配置文件&#xff0c;映射文件&#xff0c;并测试对数据库表基本的的CRUD操作&#xff1b; 2.掌握核心配置文件中几个重要标…

使用Gitee Go流水线部署个人项目到服务器指南

使用Gitee Go流水线部署个人项目到服务器指南 前言&#xff01;&#xff01;&#xff01; 本文解决的问题&#xff1a; 你有一台ECS服务器&#xff0c;你在上面部署了一个Java服务也就是一个jar&#xff0c;你觉着你每次手动本地打包&#xff0c;上传&#xff0c;在通过命令去…

LCCI ESG 中英联合认证国际分析师适合的岗位

LCCI ESG中英联合认证国际分析师领域热门岗位大揭秘&#xff01;&#x1f30d; 大家好&#xff01;今天我们来探讨LCCI ESG中英联合认证国际分析师领域的热门岗位&#xff0c;看看是否有适合你的选择。 1️⃣ LCCI ESG中英联合认证国际分析师报告专员&#xff1a;主要负责编制…

Compose 实践与探索十五 —— 自定义触摸

1、自定义触摸与一维滑动监测 之前我们在讲 Modifier 时讲过如下与手势检测相关的 Modifier&#xff1a; Modifier.clickable { } Modifier.combinedClickable { } Modifier.pointerInput {detectTapGestures { } }这里对以上内容就不再赘述了&#xff0c;直接去讲解更复杂的…

【Linux】Makefile秘籍

> &#x1f343; 本系列为Linux的内容&#xff0c;如果感兴趣&#xff0c;欢迎订阅&#x1f6a9; > &#x1f38a;个人主页:【小编的个人主页】 >小编将在这里分享学习Linux的心路历程✨和知识分享&#x1f50d; >如果本篇文章有问题&#xff0c;还请多多包涵&a…

LDAP从入门到实战:环境部署与配置指南(上)

#作者&#xff1a;朱雷 文章目录 一、LDAP 简介1.1. 什么是目录服务1.2. 什么是 LDAP1.3. LDAP的基本模型 二、Ldap环境部署2.1.下载软件包2.2.安装软件2.3.编辑配置文件2.4.启动服务 一、LDAP 简介 1.1. 什么是目录服务 目录是专门为搜索和浏览而设计的专用数据库&#xff…

《C++智能指针:建议使用 make_shared 代替 shared_ptr》

《C 智能指针&#xff1a;长达数十年的血泪史&#xff0c;一步步征服内存泄漏》-CSDN博客 shared_ptr<int> sp1(new int(10)); 这句代码实际存在两个内存开辟&#xff0c;一是开辟我们要托管的内存资源 &#xff0c;二是开辟引用计数的资源&#xff0c;引用技术也是new出…

代码随想录刷题day50|(回溯算法篇)131.分割回文串▲

目录 一、回溯算法基础知识 二、分割回文串思路 2.1 如何切割 2.2 判断回文 2.3 回溯三部曲 2.4 其他问题 三、相关算法题目 四、总结 一、回溯算法基础知识 详见&#xff1a;代码随想录刷题day46|&#xff08;回溯算法篇&#xff09;77.组合-CSDN博客 二、分割回文…

vivo 湖仓架构的性能提升之旅

作者&#xff1a;郭小龙 vivo互联网 大数据高级研发工程师 导读&#xff1a;本文整理自 vivo互联网 大数据高级研发工程师 郭小龙 在 StarRocks 年度峰会上的分享&#xff0c;聚焦 vivo 大数据多维分析面临的挑战、StarRocks 落地方案及应用收益。 在 即席分析 场景&#xff0c…

Springboot的jak安装与配置教程

目录 Windows系统 macOS系统 Linux系统 Windows系统 下载JDK&#xff1a; 访问Oracle官网或其他JDK提供商网站&#xff0c;下载适合Windows系统的JDK版本。网站地址&#xff1a;Oracle 甲骨文中国 | 云应用和云平台点击进入下滑&#xff0c;点击进入下载根据自己的系统选择&…

力扣算法Hot100——128. 最长连续序列

题目要求时间复杂度为O(n)&#xff0c;因此不能使用两次循环匹配。 首先使用 HashSet 去重&#xff0c;并且 HashSet 查找一个数的复杂度为O(1)外循环还是遍历set集合&#xff0c;里面一重循环需要添加判断&#xff0c;这样才不会达到O( n 2 n^2 n2)判断是否进入最长序列查找循…

BlockChain.java

BlockChain 区块链&#xff0c;举个栗子 注意啦&#xff0c;列子里面的hashcode相等&#xff0c;但是字符串是不一样的哦&#xff0c;之前有记录这个问题 String.hashCode()-CSDN博客

visual studio 中导入 benchmark

法一 1.visual studio 中导入 benchmark.lib Shlwapi.lib这两个库 2.预处理宏 BENCHMARK_STATIC_DEFINE vs导入参考 错误提示 没有加入 BENCHMARK STATIC_DEFINE error LNK2001: 无法解析的外部符号 “__declspec(dllimport) int __cdecl benchmark::internal::InitializeS…

java基础之windows电脑基础命令

windows电脑基础命令 windows电脑基础命令快捷键和功能键键盘功能键B:键盘快捷键 DOS命令行的进入方式xp下如何打开DOS控制台&#xff1f;win7下如何打开DOS控制台&#xff1f;win8下如何打开DOS控制台 DOS命令讲解 黑窗口编译文件使用黑窗口运行java程序 windows电脑基础命令 …

Java 第十一章 GUI编程(3)

目录 内部类 内部类定义 内部类的特点 匿名内部类 格式&#xff1a; 内部类的意义 实例 内部类 ● 把类定义在另一个类的内部&#xff0c;该类就被称为内部类。 ● 如果在类 Outer 的内部再定义一个类 Inner&#xff0c;此时类 Inner 就称为内部类 &#xff08;或称为嵌…