线程同步

  • 同一进程或者同一线程可以生成许多不同的子线程来完成规定的任务,但是多个线程同时运行的情况下可能需要对某个资源进行读写访问,比如以下这个情况:创建两个线程对同一资源进行访问,最后打印出这个资源,运气好一点的情况下得到的值应该为 2,运气不好的情况下就会变为 1
#include <Windows.h>
#include <iostream>
#include <process.h>
using namespace std; DWORD WINAPI ThreadCommunication();
unsigned __stdcall ThreadFun1(void* pvParam);
unsigned __stdcall ThreadFun2(void* pvParam);
// 共享资源
int Statistics = 0; int main(int argc, char *argv[])
{
ThreadCommunication();
return 0;
} DWORD WINAPI ThreadCommunication()
{
// 创建线程
HANDLE myThread1 = (HANDLE)_beginthreadex(NULL, NULL, &ThreadFun1, NULL, CREATE_SUSPENDED, NULL); ResumeThread(myThread1);
HANDLE myThread2 = (HANDLE)_beginthreadex(NULL, NULL, &ThreadFun2, NULL, CREATE_SUSPENDED, NULL); ResumeThread(myThread2); // 等待线程退出
HANDLE Threads[2] = { 0 }; Threads[0] = myThread1; Threads[1] = myThread2;
WaitForMultipleObjects(2, Threads, TRUE, INFINITE); // 关闭线程句柄
CloseHandle(myThread1); CloseHandle(myThread2); // 最后打印计数
cout << "Statistics: " << Statistics << endl;
return TRUE;
} // 线程一
unsigned __stdcall ThreadFun1(void* pvParam)
{
// 做一些事情,完成之后计数加一
Statistics++;
return TRUE;
} // 线程二
unsigned __stdcall ThreadFun2(void* pvParam)
{
// 做一些事情,完成之后计数加一
Statistics++;
return TRUE;
}
  • 这个就是运气不好的情况,原因就是多线程运行时对同一资源的访问是竞争性的,线程 1 访问资源的时候线程 2 可能被堵塞。那么怎么让线程进行对同一资源有顺序的访问呢,微软并没有提供这样的方法可以让第一个线程先运行或者当第二个线程先运行(Windows系统并不是实时操作系统,对线程按照算法公平的原则进行调度,是随机的),但是微软提供了一些方法可以让线程同步运行,保证资源的按顺序访问

  • 线程同步逻辑图

0x01 原子访问

  • 原子访问是解决线程冲突的第一种方法,原子访问顾名思义就是按原子的方式访问资源并且保证访问资源不冲突。那么上面的代码可以改为这样:
// 线程一
unsigned __stdcall ThreadFun1(void* pvParam)
{
// 做一些事情,完成之后计数加一
InterlockedExchangeAdd(&Statistics, 1);
return TRUE;
} // 线程二
unsigned __stdcall ThreadFun2(void* pvParam)
{
// 做一些事情,完成之后计数加一
InterlockedExchangeAdd(&Statistics, 1);
return TRUE;
}
  • InterlockedExchangeAdd 的第一个参数可以传入一个 long 型变量,第二个参数传入想要加的值(只可以是整数,需要减的话传负数即可),该函数是以原子方式操作,所以解决了访问冲突的问题;如果嫌麻烦可以使用 InterlockedIncrement 函数直接将变量加一
  • 但这些 Interlock 函数存在一个缺点,只能加减固定的整数,所以不够灵活(总不能在使用函数之前花时间做运算吧),所以出现了升级版函数 InterlockedExchange,这个函数可以直接把变量以原子方式改为我们想要的值,当然了这个是 32 位的,如果需要 64 位的可以使用 InterlockedExchange64 这个函数
// 线程一
unsigned __stdcall ThreadFun1(void* pvParam)
{
// 做一些事情, 之后将 Statistics 的值变为 1
InterlockedExchange(&Statistics, 1);
return TRUE;
} // 线程二
unsigned __stdcall ThreadFun2(void* pvParam)
{
// 做一些事情, 之后将 Statistics 的值变为 2
InterlockedExchange(&Statistics, 2);
return TRUE;
}
  • 除了以原子方式加减或改变一个变量,微软还给出了比较方便的比较函数
unsigned __stdcall ThreadFun1(void* pvParam)
{
// 做一些事情
InterlockedExchange(&Statistics, 1);
return TRUE;
} // 线程二
unsigned __stdcall ThreadFun2(void* pvParam)
{
// 做一些事情
InterlockedCompareExchange(&Statistics, Statistics + 1, 1);
return TRUE;
}
  • 上面代码的意义是如果线程 1 先运行并且将 Statistics 的值设置为 1 的话,线程 2 就将现有的 Statistics 的值加 1

0x02 关键段和旋转锁

  • 纵观单用原子的方式解决线程同步确实有很大的局限性,第一:对资源变量只能以整数的方式进行操作;第二:只能更改变量,不能做到更改其他资源,比如 IO 读写等。所以出现了解决线程同步的第二种方法关键段和旋转锁
  • 关键段顾名思义就是在这个代码段中,代码访问的所有资源都是按原子方式进行的,防止线程之间的访问冲突,实现关键段的方式也很简单:
#include <Windows.h>
#include <iostream>
#include <process.h>
using namespace std; DWORD WINAPI ThreadCommunication();
unsigned __stdcall ThreadFun1(void* pvParam);
unsigned __stdcall ThreadFun2(void* pvParam); // 共享资源
int Statistics = 0; // 创建供关键段使用的结构体
CRITICAL_SECTION Critical; int main(int argc, char *argv[])
{
ThreadCommunication();
return 0;
} DWORD WINAPI ThreadCommunication()
{
// 初始化结构体
InitializeCriticalSection(&Critical); // 创建线程
HANDLE myThread1 = (HANDLE)_beginthreadex(NULL, NULL, &ThreadFun1, NULL, CREATE_SUSPENDED, NULL); ResumeThread(myThread1);
HANDLE myThread2 = (HANDLE)_beginthreadex(NULL, NULL, &ThreadFun2, NULL, CREATE_SUSPENDED, NULL); ResumeThread(myThread2); // 等待线程退出
HANDLE Threads[2] = { 0 }; Threads[0] = myThread1; Threads[1] = myThread2;
WaitForMultipleObjects(2, Threads, TRUE, INFINITE); // 关闭线程句柄
CloseHandle(myThread1); CloseHandle(myThread2); // 清除关键段结构体
DeleteCriticalSection(&Critical); // 最后打印计数
cout << "Statistics: " << Statistics << endl;
return TRUE;
} // 线程一
unsigned __stdcall ThreadFun1(void* pvParam)
{
// 关键段开始
EnterCriticalSection(&Critical); // 做一些事情
for (size_t i = 0; i < 1000; i++)
{
Statistics++;
} // 关键段结束
LeaveCriticalSection(&Critical);
return TRUE;
} // 线程二
unsigned __stdcall ThreadFun2(void* pvParam)
{
// 关键段开始
EnterCriticalSection(&Critical); // 做一些事情
for (size_t i = 0; i < 1000; i++)
{
Statistics++;
} // 关键段结束
LeaveCriticalSection(&Critical);
return TRUE;
}
  • 需要注意的是在将结构体传入 EnterCriticalSection 或者 LeaveCriticalSection 函数之前必须使用 InitializeCriticalSection 函数对 CRITICAL_SECTION 结构体进行初始化。共享变量访问结束之后记得使用 DeleteCriticalSection 函数清除 CRITICAL_SECTION 结构体

注:任何线程都可以使用 LeaveCriticalSection 和 EnterCriticalSection 函数开辟关键段来防止对共享资源的访问冲突。但这里有一个问题就是如果一个线程抢到了对某一个共享资源的 CPU 访问时间,那么其它线程在想访问这一个共享资源是就会被阻塞,也就是会一直等待,所以为了节省宝贵的 CPU 时间,微软给出了 TryEnterCriticalSection 函数来解决以上问题,如果共享资源被别的线程访问,那么 TryEnterCriticalSection 函数就会直接返回 FALSE 而不是等待,假如共享资源并没有被其他线程访问,那么 TryEnterCriticalSection 函数就会返回 TRUE 并且更新 CRITICAL_SECTION 的成员变量,当然之后需要调用 LeaveCriticalSection 函数来释放CRIRICAL_SECTION 结构体变量

  • 旋转锁是基于原子访问的技术,简单来说就是利用更为复杂的原子访问实现旋转锁:

long res = FALSE; // 线程一
unsigned __stdcall ThreadFun1(void* pvParam)
{
// 使用旋转锁
while (InterlockedExchange(&res, TRUE) == TRUE)
Sleep(0);
// 做一些事情
InterlockedExchange(&res, FALSE);
return TRUE;
} // 线程二
unsigned __stdcall ThreadFun2(void* pvParam)
{
// 使用旋转锁
while (InterlockedExchange(&res, TRUE) == TRUE)
Sleep(0);
// 做一些事情
InterlockedExchange(&res, FALSE);
return TRUE;
}
  • 如果线程 1 先运行,那么 InterlockedExchange 会将 res 的值变为 TRUE,并且返回值为 TRUE,之后线程 1 就可以做它的工作了和访问共享的资源了;如果这时线程 2 运行,使用 InterlockedExchange 修改 res 为 TRUE 就会返回 FALSE(因为 res 已经为 TRUE)

注:关键段的使用需要注意死锁这个问题

0x03 读写锁和条件变量

  • 读写锁类似于关键段,也是在保护对共享资源的访问,具体实现如下
#include <Windows.h>
#include <iostream>
#include <process.h>
using namespace std; DWORD WINAPI ThreadCommunication();
unsigned __stdcall ThreadFun1(void* pvParam);
unsigned __stdcall ThreadFun2(void* pvParam); // 共享资源
int Statistics = 0; // 读写锁结构体
SRWLOCK Srwlock; int main(int argc, char *argv[])
{
ThreadCommunication();
return 0;
} DWORD WINAPI ThreadCommunication()
{
// 使用 InitalizeSRWLock 函数初始化结构体
InitializeSRWLock(&Srwlock); // 创建线程
HANDLE myThread1 = (HANDLE)_beginthreadex(NULL, NULL, &ThreadFun1, NULL, CREATE_SUSPENDED, NULL); ResumeThread(myThread1);
HANDLE myThread2 = (HANDLE)_beginthreadex(NULL, NULL, &ThreadFun2, NULL, CREATE_SUSPENDED, NULL); ResumeThread(myThread2); // 等待线程退出
HANDLE Threads[2] = { 0 }; Threads[0] = myThread1; Threads[1] = myThread2;
WaitForMultipleObjects(2, Threads, TRUE, INFINITE); // 关闭线程句柄
CloseHandle(myThread1); CloseHandle(myThread2); // 最后打印计数
cout << "Statistics: " << Statistics << endl;
return TRUE;
} // 线程一
unsigned __stdcall ThreadFun1(void* pvParam)
{
// 写入这线程
AcquireSRWLockExclusive(&Srwlock); // 做一些事情
Statistics++; ReleaseSRWLockExclusive(&Srwlock);
return TRUE;
} // 线程二
unsigned __stdcall ThreadFun2(void* pvParam)
{
// 读取者线程
AcquireSRWLockShared(&Srwlock); // 做一些事情
Statistics++; ReleaseSRWLockShared(&Srwlock);
return TRUE;
}
  • 与关键段不同的是,读写锁并没有提供函数来取消等待,而关键段的 TryEnterCriticalSection 函数则可以;但是读写锁也有便于控制线程访问资源的好处,比如控制一些线程只读共享资源,另一些线程更新共享资源,增加了程序的可伸缩性;权衡利弊还是优先使用关键段
  • 对于线程同步的最后一种方式就是条件变量了,想象这样一种场景:某一个线程的功能是载入用于输入的一个文件,如果用户已经输入了这个文件的路径,那么载入它;如果用户还没有输入,那么线程进入等待状态,直到用户进行了输入。那么就利用条件变量实现它吧
#include <Windows.h>
#include <iostream>
#include <process.h>
using namespace std; DWORD WINAPI ThreadCommunication();
unsigned __stdcall ThreadFun1(void* pvParam);
unsigned __stdcall ThreadFun2(void* pvParam); // 读写锁结构体
SRWLOCK Srwlock; int main(int argc, char *argv[])
{
ThreadCommunication();
return 0;
} DWORD WINAPI ThreadCommunication()
{
// 使用 InitalizeSRWLock 函数初始化结构体
InitializeSRWLock(&Srwlock); // 创建线程
HANDLE myThread1 = (HANDLE)_beginthreadex(NULL, NULL, &ThreadFun1, NULL, CREATE_SUSPENDED, NULL); ResumeThread(myThread1);
HANDLE myThread2 = (HANDLE)_beginthreadex(NULL, NULL, &ThreadFun2, NULL, CREATE_SUSPENDED, NULL); ResumeThread(myThread2); // 等待线程退出
HANDLE Threads[2] = { 0 }; Threads[0] = myThread1; Threads[1] = myThread2;
WaitForMultipleObjects(2, Threads, TRUE, INFINITE); // 关闭线程句柄
CloseHandle(myThread1); CloseHandle(myThread2); return TRUE;
} // 模拟判断用户是否输入
BOOL IsFit = FALSE; // 条件变量结构体
CONDITION_VARIABLE Condition; // 线程一
unsigned __stdcall ThreadFun1(void* pvParam)
{
AcquireSRWLockExclusive(&Srwlock); // 模拟用户输入
Sleep(300); // 输入完成之后将 IsFit 变为 TRUE,并且唤醒等待线程
IsFit = TRUE;
WakeConditionVariable(&Condition); ReleaseSRWLockExclusive(&Srwlock);
return TRUE;
} // 线程二
unsigned __stdcall ThreadFun2(void* pvParam)
{
AcquireSRWLockExclusive(&Srwlock); // 如果用户还没有输入则进入等待状态,等待用户输入
if (IsFit == FALSE)
{
cout << "[-] 等待用户输入: " << endl;
SleepConditionVariableSRW(&Condition, &Srwlock, INFINITE, 0);
}
cout << "[*] 用户输入完毕 " << endl; // 根据用户输入载入文件... ReleaseSRWLockExclusive(&Srwlock);
return TRUE;
}
  • 如果线程二执行前,用户已经输入并且变量 IsFit 的值已经变为 TRUE,那么没有问题,按照流程载入文件即可;如果线程二执行前用户还没有输入,那么该线程就会进入等待状态直到用于输入完成之后才开始往下执行载入文件的操作

用户下的线程用户研究总结到此完毕,如有错误欢迎指正

参考资料:Windows 核心编程

用户模式下的线程同步的分析(Windows核心编程)的更多相关文章

  1. windows核心编程---第七章 用户模式下的线程同步

    用户模式下的线程同步 系统中的线程必须访问系统资源,如堆.串口.文件.窗口以及其他资源.如果一个线程独占了对某个资源的访问,其他线程就无法完成工作.我们也必须限制线程在任何时刻都能访问任何资源.比如在 ...

  2. 《windows核心编程系列》七谈谈用户模式下的线程同步

    用户模式下的线程同步 系统中的线程必须访问系统资源,如堆.串口.文件.窗口以及其他资源.如果一个线程独占了对某个资源的访问,其他线程就无法完成工作.我们也必须限制线程在任何时刻都能访问任何资源.比如在 ...

  3. 【windows核心编程】 第八章 用户模式下的线程同步

    Windows核心编程 第八章 用户模式下的线程同步 1. 线程之间通信发生在以下两种情况: ①    需要让多个线程同时访问一个共享资源,同时不能破坏资源的完整性 ②    一个线程需要通知其他线程 ...

  4. Windows核心编程:第8章 用户模式下的线程同步

    Github https://github.com/gongluck/Windows-Core-Program.git //第8章 用户模式下的线程同步.cpp: 定义应用程序的入口点. // #in ...

  5. 内核模式下的线程同步的分析(Windows核心编程)

    内核模式下的线程同步 内核模式下的线程同步是用户模式下的线程同步的扩展,因为用户模式下的线程同步有一定的局限性.但用户模式下线程同步的好处是速度快,不需要切换到内核模式(需要额外的 CPU 时间).通 ...

  6. 第8章 用户模式下的线程同步(4)_条件变量(Condition Variable)

    8.6 条件变量(Condition Variables)——可利用临界区或SRWLock锁来实现 8.6.1 条件变量的使用 (1)条件变量机制就是为了简化 “生产者-消费者”问题而设计的一种线程同 ...

  7. 第8章 用户模式下的线程同步(1)_Interlocked系列函数

    8.1 原子访问:Interlocked系列函数(Interlock英文为互锁的意思) (1)原子访问的原理 ①原子访问:指的是一线程在访问某个资源的同时,能够保证没有其他线程会在同一时刻访问该资源. ...

  8. 第8章 用户模式下的线程同步(3)_Slim读写锁(SRWLock)

    8.5 Slim读/写锁(SRWLock)——轻量级的读写锁 (1)SRWLock锁的目的 ①允许读者线程同一时刻访问共享资源(因为不存在破坏数据的风险) ②写者线程应独占资源的访问权,任何其他线程( ...

  9. 第8章 用户模式下的线程同步(2)_临界区(CRITICAL_SECTION)

    8.4 关键段(临界区)——内部也是使用Interlocked函数来实现的! 8.4.1 关键段的细节 (1)CRITICAL_SECTION的使用方法 ①CRITICAL_SECTION cs;   ...

随机推荐

  1. sublime text3里 修改TAB键为缩进为四个空格

    1. 菜单栏里点击 Preferences-> Setting里面,右侧小窗口User 2. 在弹出来的文本里,添加如下两行:{ "tab_size": 4, "t ...

  2. android底部导航栏小结

    android自带的有TabHost,但好像无法满足要求, 本文只记录使用 TabLayout + Fragment  和 android 自带的 BottomNavigationView + Fra ...

  3. C#控制鼠标自动连续点(DEMO)

    ---------------------------界面---------------------------------------------------- ------------------ ...

  4. Canal高可用架构部署

    一.前言 canal 是阿里的一款开源项目,纯 Java 开发.基于数据库增量日志解析,提供增量数据订阅&消费,目前主要支持了 MySQL(也支持 mariaDB). canal 模拟 mys ...

  5. MySQL Order BY 排序过程

    MySQL 在进行 Order By 操作排序时,通常有两种排序方式: 全字段排序 Row_id 排序 MySQL 中每个线程在执行排序时,都会被分配一块区域 - sort buffer,它的大小通过 ...

  6. 当红开发语言Go,真的是未来的技术主流吗?

    摘要:文将详细介绍 Golang 的语言特点以及它的优缺点和适用场景,带着上述几个疑问,为读者分析 Go 语言的各个方面,以帮助初入 IT 行业的程序员以及对 Go 感兴趣的开发者进一步了解这个热门语 ...

  7. 【Makefile】2-Makefile的介绍及原理

    目录 前言 概念 Chapter 2:介绍 2.1 makefile的规则 2.3 make 是如何工作的 ** 2.5 让 make 自动推导 2.8 Makefile 里面有什么 2.9 Make ...

  8. 前端vue性能优化

    一:代码层次优化 1.1.v-if 和 v-show 区分使用场景 v-if 是 真正 的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建:也是惰性的:如果在初始渲染时 ...

  9. Linux 服务器性能测试报告-sysbench命令实践

    Linux 服务器性能测试报告 我们使用linux 工具sysbench 来测试linux服务器性能,目前在Centos上进行操作 Install sysbench yum -y install sy ...

  10. [Fundamental of Power Electronics]-PART II-8. 变换器传递函数-8.5 交流传递函数以及阻抗的测量/8.6 本章小结

    8.5 交流传递函数以及阻抗的测量 测量原型变换器和变换器系统的传递函数是非常好的工程实践过程.这样的实践可以验证系统是否被正确地建模和设计.此外,通过测量单个电路元件的端阻抗来表征其特性也是非常有用 ...