设为首页 收藏本站
查看: 547|回复: 0

[经验分享] Windows线程同步详解

[复制链接]

尚未签到

发表于 2018-6-17 12:21:59 | 显示全部楼层 |阅读模式
线程同步问题

  在多线程编程中,极容易产生错误。造成错误的原因:两个或多个线程同时访问了共有的资源(比如全局变量,句柄,对空间等),造成资源在不同线程修改时出现不一致。多个线程对于资源的访问要按照一定的先后顺序,但是未按照预想的顺序来,就会导致程序出现意想不到的错误。
  问题实例:(环境:vs2015 控制台程序)
  

#include<Windows.h>  
#include<stdio.h>
  
int g_nNum = 0;
  
DWORD WINAPI ThreadProc(LPVOID lParam)
  
{
  
for (int i = 0; i < 10000; i++)
  
{
  
g_nNum++;
  
}
  
printf("%d", g_nNum);
  
return 0;
  
}
  
int main()
  
{
  
//创建线程1
  
HANDLE hThread1 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL);
  
//创建线程2
  
HANDLE HThread2 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL);
  
WaitForSingleObject(hThread1, INFINITE); //线程1执行完毕后返回
  
WaitForSingleObject(HThread2, INFINITE); //线程2执行完毕后返回
  
printf("%d\n", g_nNum);
  
return 0;
  
}
  

  第一次执行结果:
  11789
  17876
  17876
  第二次执行结果:
  20000
  15844
  20000
  按照预期,g_nNum在两个线程中应该各自自增10000,而实际上,g_nNum的值确是不确定的。
  首先来看一下自增这个简单的操纵在汇编层的代码:
  

00AE1419 mov eax,dword ptr ds [00AE8134h]  
00AE141E add eax,1
  
00AE1421 mov dword ptr ds:[00AE8134h],eax
  

  两个线程同时执行g_nNum++这个操作,有可能线程1执行了add eax,还没有将将自增的结果写入,线程2又开始执行,当线程1再执行的时候,线程2的执行就相当于已经无用。因为线程的调度是不可控的,所以我们不能预知最后的结果。

解决方案:**
  1.原子操作
  原子操作是一些比较简单的操作,只能对资源进行简单的加减赋值等。当运用原子操作访问某数据时,其他线程不能在此次操作结束前访问此数据,即不允许两个线程同时操作一个数据,当然,也不允许三个。原子操作就像厕所,只允许一个人进入。
  常见的原子操作函数自行百度
  

int g_nNum = 0;  
DWORD WINAPI ThreadProc(LPVOID lParam)
  
{
  
for (int i = 0; i < 10000; i++)
  
{
  
//原子操作中的自增,其他的原子操作函数自行百度
  
InterlockedIncrement((unsigned long*)&g_nNum);
  
}
  
printf("%d", g_nNum);
  
return 0;
  
}
  
int main()
  
{
  
HANDLE hThread1 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL);
  
HANDLE HThread2 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL);
  
WaitForSingleObject(hThread1, INFINITE);
  
WaitForSingleObject(HThread2, INFINITE);
  
printf("%d\n", g_nNum);
  
return 0;
  
}
  

  运行结果
  10000
  20000
  20000


2.临界区
  原子操作仅能够解决单独的数据(整型变量的基本运算)的线程同步问题,大多数时候,我们想要实现的是对一个代码段的保护,于是便引入了临界区这一概念。临界区通过EnterCriticalSection与LeaveCriticalSection这一对函数,通过这个函数对,就可以实现多个代码保护区。在使用临界区前,需要调用InitiaizeCriticalSection初始化一个临界区,使用完后调用DeleteCriticalSection销毁临界区。
  

#include <windows.h>  
CRITICAL_SECTION cs = {};
  
int g_nNum = 0;
  
DWORD WINAPI ThreadProc(LPVOID lParam) {
  // 2. 进入临界区
  // cs有个属性LockSemaphore是不是被锁定
  // 当调用EnterCriticalSection表示临界区被锁定,OwningThread就是该线程
  // 其他调用EnterCriticalSection,会检查和锁定时的线程是否是同一个线程
  // 如果不是,调用Enter的线程就阻塞
  // 如果是,就把锁定计数LockCount+1
  // 有几次Enter就得有几次Leave
  // 但是,不是拥有者线程的人不能主动Leave
  EnterCriticalSection(&cs);
  for (int i = 0; i < 100000; i++)
  {
  g_nNum++;
  }
  printf("%d\n", g_nNum);
  // 3. 离开临界区
  // 万一,还没有调用Leave,该线程就崩溃了,或死循环了..
  // 外面等待的人就永远等待
  // 临界区不是内核对象, 不能跨进程同步
  LeaveCriticalSection(&cs);
  return 0;
  
}
  

  
int main()
  
{
  // 1. 初始化临界区
  InitializeCriticalSection(&cs);
  HANDLE hThread1 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL);
  HANDLE hThread2 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL);
  WaitForSingleObject(hThread1, INFINITE);
  WaitForSingleObject(hThread2, INFINITE);
  printf("%d\n", g_nNum);
  // 4. 销毁临界区
  DeleteCriticalSection(&cs);
  return 0;
  
}
  

3.互斥体
  临界区有很多解决不了的问题,因为临界区在一个进程中有效,无法在多进程的情况下进行同步。并且,如果一个线程进入到临界区,结果这个线程由于某些原因奔溃了,即无法执LeaveCriticalSection(),那么其他线程将无法再进入临界区,程序奔溃。而互斥体则可以解决这些问题。
  首先,互斥体是一个内核对象。(因此互斥体拥有内核对象的一切属性)它有两个状态,激发态和非激发态;它有一个概念叫做线程拥有权,与临界区类似;等待函数等待互斥体的副作用,将互斥体的拥有者设置为本线程,然后将互斥体的状态设置为非激发态。
  主要函数:CreateMutex();WaitForSingleObject();ReleaseMutex();函数用法自行百度。
  当一个线程A调用WaitForSingleObject函数时,WaitForSingleObject会立即返回,将并将互斥体设为非激发态,互斥体被锁住,此线程获得拥有权。之后,任何调用WaitForSingleObject的线程无法获得所有权,必须等待互斥体。当线程A调用ReleaseMutex时,互斥体被解锁,此时互斥体又被设置为激发态,并会从等待它的线程中随机选一个,重复前面的过程。互斥体一次只能被一个线程拥有,在WaitXXXX与ReleaseMutex之间的代码被保护起来,这一点与临界区类似,只不过互斥体是一个内核对象,可以进行多进程同步。
  

#include <windows.h>  
#include<stdio.h>
  
HANDLE hMutex = 0;
  
int g_nNum = 0;
  
// 临界区和互斥体比较
  
// 1. 互斥体是个内核对象,可以跨进程同步,临界区不行
  
// 2. 当他们的拥有者线程都崩溃的时候,互斥体可以被系统释放,变为有信号,其他的等待函数可以正常返回
  
// 临界区不行,如果都是假死(死循环,无响应),他们都会死锁
  
// 3. 临界区不是内核对象,所以访问速度比互斥体快
  
DWORD WINAPI ThreadProc(LPVOID lParam) {
  // 等待某个内核对象,有信号就返回,无信号就一直等待
  // 返回时把等待的对象变为无信号状态
  WaitForSingleObject(hMutex, INFINITE);
  for (int i = 0; i < 100000; i++)
  {
  g_nNum++;
  }
  printf("%d\n", g_nNum);
  // 把互斥体变为有信号状态
  ReleaseMutex(hMutex);
  return 0;
  
}
  

  
int main()
  
{
  // 1. 创建一个互斥体
  hMutex = CreateMutex(
  NULL,
  FALSE,// 是否创建时就被当先线程拥有
  NULL);// 互斥体名称
  HANDLE hThread1 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL);
  HANDLE hThread2 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL);
  WaitForSingleObject(hThread1, INFINITE);
  WaitForSingleObject(hThread2, INFINITE);
  printf("%d\n", g_nNum);
  return 0;
  
}
  

4.信号量
  信号量与互斥体类似。不过信号量中引入了信号数量的概念。如果说互斥体是家里厕所,在一个时间点只能一个人使用,那信号量就是公共厕所,可以多个人同时使用,但是仍有上限。这个上限数量即最大信号数量。
  主要函数:CreateSemaphore();OpenSemaphore();ReleaseSemaphore();WaitForSingleObject(); 函数用法自行百度
  当有线程调用了WaitForSingleObject();当前信号量减一,再有线程调用,再减一。为0时,即信号量被锁住,再有线程调用WaitForSingleObject时,将被阻塞。
  

#include <windows.h>  
#include <stdio.h>
  
HANDLE hSemphore;
  
int g_nNum = 0;
  
DWORD WINAPI ThreadProc(LPVOID lParam) {
  WaitForSingleObject(hSemphore, INFINITE);
  for (int i = 0; i < 100000; i++)
  {
  g_nNum++;
  }
  printf("%d\n", g_nNum);
  ReleaseSemaphore(hSemphore,
  1,// 释放的信号个数可以大于1,但是释放后的信号个数+之前的不能大于最大值,否则释放失败
  NULL);
  return 0;
  
}
  

  
int main()
  
{
  hSemphore = CreateSemaphore(
  NULL,
  1,// 初始信号个数
  1,// 最大信号个数,就是允许同时访问保护资源的线程数
  NULL);
  HANDLE hThread1 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL);
  HANDLE hThread2 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL);
  WaitForSingleObject(hThread1, INFINITE);
  WaitForSingleObject(hThread2, INFINITE);
  printf("%d\n", g_nNum);
  return 0;
  
}
  

5.事件
  事件具有较大的权限。可以手动设置事件对象为激发态还是非激发态。创建时间对象的时候,可以设置是自动选择和手动选择。自动选择的事件,等待函数返回时,会自动将其状态设置为非激发态,阻塞其他线程。手动选择的,事件对象状态的控制全靠代码。
  主要函数:CreateEventW();OpenEventA();SetEvent();PulseEvent();
  CloseEvent();RoseEvent();
  

#include <windows.h>  
#include<stdio.h>
  
HANDLE hEvent1, hEvent2;
  
DWORD WINAPI ThreadProcA(LPVOID lParam) {
  for (int i = 0; i < 10; i++){
  WaitForSingleObject(hEvent1, INFINITE);
  printf("A ");
  SetEvent(hEvent2);
  }
  return 0;
  
}
  

  
DWORD WINAPI ThreadProcB(LPVOID lParam) {
  for (int i = 0; i < 10; i++){
  WaitForSingleObject(hEvent2, INFINITE);
  printf("B ");
  SetEvent(hEvent1);
  }
  return 0;
  
}
  

  
int main()
  
{
  // 事件对象,高度自定义的
  hEvent1 = CreateEvent(
  NULL,
  FALSE,// 自动重置
  TRUE,// 有信号
  NULL);
  // hEvent1自动重置  初始有信号  任何人通过setevent变为有信号 resetevent变为无信号
  // hEvent2自动重置  初始无信号
  hEvent2 = CreateEvent(NULL, FALSE, FALSE, NULL);
  HANDLE hThread1 = CreateThread(NULL, NULL, ThreadProcA, NULL, NULL, NULL);
  HANDLE hThread2 = CreateThread(NULL, NULL, ThreadProcB, NULL, NULL, NULL);
  WaitForSingleObject(hThread1, INFINITE);
  WaitForSingleObject(hThread2, INFINITE);
  return 0;
  
}

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.iyunv.com/thread-524930-1-1.html 上篇帖子: Windows2008系统基本设置 下篇帖子: windows查看当前路径
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表