一、同步问题概述

如果多个线程同时对同一个变量(内存区域)进行读写,就会由于线程切换(cpu时间片分配)导致结果与预期不相符,如两个线程A和B同时执行变量自增运算,由于A从内存取数据到cpu后线程切换到了B,B取完数据,cpu运算完然后将结果写回内存后线程才切换到了A,A继续从中断的地方执行,即cpu运算和写回内存,这使得A和B写回内存的结果都是相同的,即都是变量自增一次的结果,而不是进行了两次自增运算的结果,这就是同步问题

如图:

#include <iostream>
#include<Windows.h>
unsigned int g_val = 0;
DWORD threadfun(LPVOID lpParameter)
{for (int i = 0; i < 1000000; i++){g_val++;//InterlockedIncrement(&g_val);}std::cout << "值:" << g_val <<"线程id:" << GetCurrentThreadId()<<std::endl;return 0;
}
int main()
{CreateThread(NULL, 0, threadfun, NULL, 0, NULL);CreateThread(NULL, 0, threadfun, NULL, 0, NULL);system("pause");
}

运行结果:

二、用户态下解决方案

1.原子(互锁)操作

原子操作是指不会被线程调度打断的操作,在操作中不会发生线程切换,windows提供的原子操作api有如下:

以自增运算为例:

LONG __cdecl InterlockedIncrement(__inout  LONG volatile* Addend    //要进行自增运算的变量的指针
);

演示:

#include <iostream>
#include<Windows.h>
unsigned int g_val = 0;
DWORD threadfun(LPVOID lpParameter)
{for (int i = 0; i < 1000000; i++){InterlockedIncrement(&g_val);}std::cout << "值:" << g_val <<"线程id:" << GetCurrentThreadId()<<std::endl;return 0;
}
int main()
{CreateThread(NULL, 0, threadfun, NULL, 0, NULL);CreateThread(NULL, 0, threadfun, NULL, 0, NULL);system("pause");
}

运行结果:

多次运行可以看到第一个线程的值总是在变化,这是因为线程在执行这些原子操作时也会不断切换,所以第一个线程的值总是不固定的,但是总是大于1000000,并且最终变量值为2000000,相当于执行了两遍for循环

2.临界区

如果我们的操作不是自增运算,而是如下:

#include <iostream>
#include<Windows.h>
#include<vector>std::vector<int> vec;
RTL_CRITICAL_SECTION  lock;
DWORD threadfun(LPVOID lpParameter)
{for (int i = 0; i < 100000; i++){vec.push_back(i);   //末尾添加元素i}std::cout<<"数量:"<<vec.size()<<"线程id:" << GetCurrentThreadId()<<std::endl;return 0;
}
int main()
{CreateThread(NULL, 0, threadfun, NULL, 0, NULL);CreateThread(NULL, 0, threadfun, NULL, 0, NULL);system("pause");
}

编译后会发现无法运行,而且中断位置不固定,像这样

原子操作api只用于简单运算,当操作较为复杂的时候我们可以采用临界区,临界区相当于一把锁,把代码锁起来,只能让当前线程访问,其他线程会阻塞,相关api:

进入临界区(上锁):

void WINAPI EnterCriticalSection(__inout  LPCRITICAL_SECTION lpCriticalSection
);

参数是一个结构体指针,指向如下结构体

typedef struct _RTL_CRITICAL_SECTION {PRTL_CRITICAL_SECTION_DEBUG DebugInfo;////  The following three fields control entering and exiting the critical//  section for the resource//LONG LockCount;LONG RecursionCount;HANDLE OwningThread;        // from the thread's ClientId->UniqueThreadHANDLE LockSemaphore;ULONG_PTR SpinCount;        // force size on 64-bit systems when packed
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;

该结构体成员不需要我们填写,由api自己完成

出临界区(开锁):

void WINAPI LeaveCriticalSection(__inout  LPCRITICAL_SECTION lpCriticalSection
);

临界区初始化:

void WINAPI InitializeCriticalSection(__out  LPCRITICAL_SECTION lpCriticalSection
);

临界区反初始化

void WINAPI DeleteCriticalSection(__inout  LPCRITICAL_SECTION lpCriticalSection
);

示例:

#include <iostream>
#include<Windows.h>
#include<vector>std::vector<int> vec;
RTL_CRITICAL_SECTION  lock;
DWORD threadfun(LPVOID lpParameter)
{for (int i = 0; i < 1000; i++){EnterCriticalSection(&lock);    //临界区开始vec.push_back(i);   //末尾添加元素iLeaveCriticalSection(&lock);    //临界区结束}std::cout<<"数量:"<<vec.size()<<"线程id:" << GetCurrentThreadId()<<std::endl;return 0;
}
int main()
{InitializeCriticalSection(&lock);   //初始化临界区CreateThread(NULL, 0, threadfun, NULL, 0, NULL);CreateThread(NULL, 0, threadfun, NULL, 0, NULL);system("pause");DeleteCriticalSection(&lock);   //释放临界区
}

三、内核同步对象

由Windows内核提供,0环和3环都可以使用

1.互斥体

相当于内核版的临界区,可以跨进程使用

创建或打开互斥体:

HANDLE WINAPI CreateMutex(__in_opt  LPSECURITY_ATTRIBUTES lpMutexAttributes,    //安全属性__in      BOOL bInitialOwner,    //是否对当前进程上锁__in_opt  LPCTSTR lpName    //对象名称,如果要跨进程使用可以填
);
函数成功返回互斥对象句柄

注意如果第二个参数为TRUE并且调用者创建了互斥锁,则调用线程获得该互斥体的初始所有权

补充:虽然每个进程都有自己独立的资源和空间,但有些时候我们只需要程序在系统上只保存一份进程实例,这就是进程互斥问题,CreateMutex函数的第三个参数可以指定互斥对象的名称,程序每次运行时通过判断系统是否存在同名互斥对象来判断程序是否重复运行,若存在同名互斥对象,GetLastError()函数返回ERROR_ALREADY_EXISTS

给互斥体上锁(也可用于等待线程或进程结束):

DWORD WINAPI WaitForSingleObject(__in  HANDLE hHandle,    //互斥对象句柄__in  DWORD dwMilliseconds    //超时时间
);

如果互斥体未锁上,则该函数将上锁,否则将处于阻塞状态,第二个参数表示超时时间,如果过了这个时间,锁仍然未开,则函数将退出,但是如果这个时间内锁开了,函数也会退出,可以根据返回值来判断是哪种情况退出:

WAIT_OBJECT_0
0x00000000L

锁开了

WAIT_TIMEOUT
0x00000102L

锁未开,超时了

WAIT_ABANDONED
0x00000080L

判断使用互斥体的线程是否结束

开锁(释放线程对互斥对象的控制权):

BOOL WINAPI ReleaseMutex(__in  HANDLE hMutex    //句柄
);

示例:

#include <iostream>
#include<Windows.h>
#include<vector>HANDLE mutex = NULL;
int g_val = 0;
DWORD threadfun(LPVOID lpParameter)
{for (int i = 0; i < 1000000; i++){WaitForSingleObject(mutex, INFINITE);   //加锁,一直等待g_val++;ReleaseMutex(mutex);    //开锁}std::cout<<"结果:"<<g_val<<"线程id:" << GetCurrentThreadId()<<std::endl;return 0;
}
int main()
{mutex = CreateMutex(NULL, FALSE,TEXT("test"));if (mutex){if (GetLastError() == ERROR_ALREADY_EXISTS){std::cout << "进程已存在!" << std::endl;}}CreateThread(NULL, 0, threadfun, NULL, 0, NULL);CreateThread(NULL, 0, threadfun, NULL, 0, NULL);system("pause");
}

临界区和互斥体的区别:

对于互斥体来说,当上锁的线程结束了并且没有开锁,锁会自动开,这种开锁称为线程遗弃,而对于临界区,当上锁的线程结束了并且没有开锁,锁不会自动开,其他线程依然处于阻塞状态

2.事件

创建事件内核对象:

HANDLE WINAPI CreateEvent(__in_opt  LPSECURITY_ATTRIBUTES lpEventAttributes,__in      BOOL bManualReset,__in      BOOL bInitialState,__in_opt  LPCTSTR lpName
);

第二个参数标识是否自动上锁,还是需要自己调api上锁

第三个参数表示初始状态时要不要上锁

上锁/等待:

DWORD WINAPI WaitForSingleObject(__in  HANDLE hHandle,__in  DWORD dwMilliseconds
);

开锁(设置事件对象为已触发):

BOOL WINAPI SetEvent(__in  HANDLE hEvent    //事件句柄
);

上锁(设置事件对象为未触发):

BOOL WINAPI ResetEvent(__in  HANDLE hEvent
);

示例:

#include <iostream>
#include<Windows.h>
#include<vector>HANDLE hevent = NULL;
int g_val = 0;
HANDLE hd1 = NULL, hd2 = NULL;
DWORD threadfun(LPVOID lpParameter)
{for (int i = 0; i < 100000; i++){WaitForSingleObject(hevent, INFINITE);g_val++;SetEvent(hevent);}std::cout<<"结果:"<<g_val<<"线程id:" << GetCurrentThreadId()<<std::endl;return 0;
}
int main()
{hevent = CreateEvent(NULL, FALSE, TRUE, NULL);hd1=CreateThread(NULL, 0, threadfun, NULL, 0, NULL);hd2=CreateThread(NULL, 0, threadfun, NULL, 0, NULL);WaitForSingleObject(hd1, INFINITE);    //暂停等待线程运行WaitForSingleObject(hd2, INFINITE);CloseHandle(hevent);    //关闭句柄
}

3.信号量

信号量与前面的临界区,互斥体等等的都不同,临界区,互斥体是独占cpu资源,而信号量则是用于限制同时运行的线程的个数,信号量内核对象中有一个最大资源计数和一个当前资源计数,最大资源计数表示信号量可以控制的最大资源数目,当前资源计数表示信号量当前可用资源的数量

创建信号量:

HANDLE WINAPI CreateSemaphore(__in_opt  LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,__in      LONG lInitialCount,    //初始信号个数__in      LONG lMaximumCount,    //最大个数__in_opt  LPCTSTR lpName
);

增加信号个数:

BOOL WINAPI ReleaseSemaphore(__in       HANDLE hSemaphore,__in       LONG lReleaseCount,    //增加个数    __out_opt  LPLONG lpPreviousCount    //传出参数,原来的个数
);

如果当前资源计数大于0,那么信号量处于触发状态,如果当前资源计数等于0,那么信号量处于未触发状态。当一个线程访问资源时,当前资源计数会减一,但是当前资源计数一定大于等于0并且小于最大资源计数

使用:

#include <iostream>
#include<Windows.h>
#include<vector>HANDLE sem = NULL;
int g_val = 0;
HANDLE hd[4] = {};
DWORD threadfun(LPVOID lpParameter)
{for (int i = 0; i < 100000000000000; i++){WaitForSingleObject(sem, INFINITE);std::cout << "线程" << GetCurrentThreadId() << "正在执行" << std::endl;g_val++;Sleep(10000);std::cout << "线程" << GetCurrentThreadId() << "执行完毕" << std::endl;ReleaseSemaphore(sem, 1, NULL);     //资源计数+1,让线程可以切换}return 0;
}
int main()
{sem = CreateSemaphore(NULL, 0, 5, NULL);for (int i = 0; i < 4; i++){hd[i] = CreateThread(NULL, 0, threadfun, NULL, 0, NULL);}ReleaseSemaphore(sem, 1, NULL);WaitForMultipleObjects(4, hd, TRUE, INFINITE);
}

运行结果:

补充:

对线程同步来说,每个内核对象都有两种状态,要么处于触发/有信号(signaled)状态,要么处于未触发/无信号(nonsignaled)状态,每种对象都有相应规则在这两个状态中切换,如进程内核对象创建时总是处于未触发状态,进程终止时进程内核对象会被操作系统设置为触发状态。对于线程来说,如果线程正在等待的对象处于未触发状态,这时线程是不可调度的,当对象处于触发状态时,线程才会变为可调度的,上面所说的WaitForSingleObject函数就是一个等待函数,会让一个线程自愿进入等待状态,直到指定的内核对象被触发为止或者超时。

所以说,这里说的锁实际上就是线程在等待对象被触发而被”锁住",而开锁就是让对象被触发

等待多个对象可以调用如下api:

DWORD WINAPI WaitForMultipleObjects(__in  DWORD nCount,    //句柄个数__in  const HANDLE* lpHandles,    //数组首地址__in  BOOL bWaitAll,    //true表示等待所有线程都变为已触发,false表示只要有一个线程变为已触发就结束__in  DWORD dwMilliseconds
);

Windows程序设计学习笔记——线程(二)同步相关推荐

  1. Windows异常学习笔记(二)—— 内核异常处理流程用户异常的分发

    Windows异常学习笔记(二)-- 内核异常处理流程&用户异常分发 用户层与内核层异常 内核异常 分析 KiDispatchException 分析 RtlDispatchException ...

  2. Windows APC学习笔记(二)—— 挂入过程执行过程

    Windows APC学习笔记(二)-- 挂入过程&执行过程 基础知识 挂入过程 KeInitializeApc ApcStateIndex KiInsertQueueApc Alertabl ...

  3. Windows系统调用学习笔记(二)—— 3环进0环

    Windows系统调用学习笔记(二)-- 3环进0环 要点回顾 基本概念 _KUSER_SHARED_DATA 0x7FFE0300 实验:判断CPU是否支持快速调用 第一步:修改EAX=1 第二步: ...

  4. Windows程序设计学习笔记(1):一个简单的windows程序

    <Windows程序设计>(第五版)(美Charles Petzold著) 1 #include<windows.h> 2 3 LRESULT CALLBACK WndProc ...

  5. MFC Windows程序设计学习笔记--文件和串行化

    文件IO主要为了 支持文档的存储和加载. 多数MFC程序用CArchive对象实现磁盘文档的存储和加载. 1.CFile: m_hFile 保存着与CFile相关联的文件的句柄. m_strFileN ...

  6. JavaScript高级程序设计学习笔记(三)

    分享一下第五章(引用类型)的笔记,内容比较多,我拆成了两部分,今天这部分是关于Object.Array.Date和RegExp类型的. 以下的笔记是书上一些我以前学习的时候,没有太重视的js基础知识, ...

  7. Windows事件等待学习笔记(二)—— 线程等待与唤醒

    Windows事件等待学习笔记(二)-- 线程等待与唤醒 要点回顾 等待与唤醒机制 可等待对象 可等待对象的差异 线程与等待对象 一个线程等待一个对象 实验 第一步:编译并运行以下代码 第二步:在Wi ...

  8. Windows消息机制学习笔记(二)—— 窗口与线程

    Windows消息机制学习笔记(二)-- 窗口与线程 要点回顾 消息从哪里来? 实验一:Spy++捕获消息 实验二:消息捕获 消息到哪里去? 窗口在哪? 实验:分析CreateWindowExW 窗口 ...

  9. Windows进程与线程学习笔记(二)—— 线程结构体

    Windows进程与线程学习笔记(二)-- 线程结构体 线程结构体 ETHREAD +0x000 Tcb : _KTHREAD 练习 线程结构体 ETHREAD 描述: 每个windows线程在0环都 ...

最新文章

  1. 一个JavaBean和DTO转换的优秀案例
  2. 【数据分析】近10年学术论文的数据分析!
  3. 双绞线传输距离_光纤传输有哪些特点 光纤传输原理介绍【图文】
  4. 10.1-控制单元CU的组合逻辑设计
  5. 用css和jquery实现标签页效果(一)
  6. 微信小程序API之setInterval
  7. 微信小程序在组件中关闭小程序
  8. [使用心得]maven2之m2eclipse使用手册之二m2eclipse功能介绍
  9. 远程连接另一台电脑,如何用被远程的电脑听歌
  10. 作业帮基于 Flink 的实时计算平台实践
  11. 计算机中数据存储--ASCII码
  12. 通信原理几种调制方式
  13. ogg怎么转mp3格式,ogg转mp3方法
  14. webmax函数高级教程整理集
  15. 联想移动裁员为求自保 摩托罗拉品牌逐渐消退
  16. tools-centos-基本配置
  17. 2018 mysql 笔试题_2018秋招数据库笔试面试题汇总
  18. Excel数值、文本相互转换
  19. laravel 构建后台package Voyager 使用笔记
  20. 按可比价格计算的意义

热门文章

  1. Python【2019年蓝桥杯省赛C++填空】
  2. Js抽奖动画Demo
  3. openlayers6【十六】vue overlay类实现gif动态图标效果详解
  4. php dynamic library,PHP Startup: Unable to load dynamic library 问题。
  5. 如何把学到的知识系统化?思维导图帮你知识管理
  6. 合格前端系列第十一弹-初探 Nuxt.js 秘密花园
  7. Java面向字符的 输入流
  8. Could not load compiled classes for settings file
  9. QUIC Design Documentand Specification Rationale(四)(即时翻译,会有多处错误)
  10. 两台电脑用蓝牙传文件出现“系统资源不足,电脑之间互相传递单个大文件,例如单个文件50g,100g