线程同步是指同一进程中的多个线程互相协调工作从而达到一致性。之所以需要线程同步,是因为多个线程同时对一个数据对象进行修改操作时,可能会对数据造成破坏,下面是多个线程同时修改同一数据造成破坏的例子:

 #include <thread>
#include <iostream> void Fun_1(unsigned int &counter);
void Fun_2(unsigned int &counter); int main()
{
unsigned int counter = ;
std::thread thrd_1(Fun_1, counter);
std::thread thrd_2(Fun_2, counter);
thrd_1.join();
thrd_2.join();
system("pause");
return ;
} void Fun_1(unsigned int &counter)
{
while (true)
{
++counter;
if (counter < )
{
std::cout << "Function 1 counting " << counter << "...\n";
}
else
{
break;
}
}
} void Fun_2(unsigned int &counter)
{
while (true)
{
++counter;
if (counter < )
{
std::cout << "Function 2 counting " << counter << "...\n";
}
else
{
break;
}
}
}

运行结果如图所示:

显然输出的结果存在问题,变量并没有按顺序递增,所以线程同步是很重要的。在这里记录三种线程同步的方式:

  ①使用C++标准库的thread、mutex头文件:

 #include <thread>
#include <mutex>
#include <iostream> void Fun_1();
void Fun_2(); unsigned int counter = ;
std::mutex mtx; int main()
{
std::thread thrd_1(Fun_1);
std::thread thrd_2(Fun_2);
thrd_1.join();
thrd_2.join();
system("pause");
return ;
} void Fun_1()
{
while (true)
{
std::lock_guard<std::mutex> mtx_locker(mtx);
++counter;
if (counter < )
{
std::cout << "Function 1 counting " << counter << "...\n";
}
else
{
break;
}
}
} void Fun_2()
{
while (true)
{
std::lock_guard<std::mutex> mtx_locker(mtx);
++counter;
if (counter < )
{
std::cout << "Function 2 counting " << counter << "...\n";
}
else
{
break;
}
}
}

  这段代码与前面一段代码唯一的区别就是在两个线程关联的函数中加了一句 std::lock_guard<std::mutex> mtx_locker(mtx); 在C++中,通过构造std::mutex的实例来创建互斥元,可通过调用其成员函数lock()和unlock()来实现加锁和解锁,然后这是不推荐的做法,因为这要求程序员在离开函数的每条代码路径上都调用unlock(),包括由于异常所导致的在内。作为替代,标准库提供了std::lock_guard类模板,实现了互斥元的RAII惯用语法(资源获取即初始化)。该对象在构造时锁定所给的互斥元,析构时解锁该互斥元,从而保证被锁定的互斥元始终被正确解锁。代码运行结果如下图所示,可见得到了正确的结果。

  ②使用windows API的临界区对象:

 //header.h
#ifndef CRTC_SEC_H
#define CRTC_SEC_H #include "windows.h" class RAII_CrtcSec
{
private:
CRITICAL_SECTION crtc_sec;
public:
RAII_CrtcSec() { ::InitializeCriticalSection(&crtc_sec); }
~RAII_CrtcSec() { ::DeleteCriticalSection(&crtc_sec); }
RAII_CrtcSec(const RAII_CrtcSec &) = delete;
RAII_CrtcSec & operator=(const RAII_CrtcSec &) = delete;
//
void Lock() { ::EnterCriticalSection(&crtc_sec); }
void Unlock() { ::LeaveCriticalSection(&crtc_sec); }
}; #endif
 //main.cpp
#include <windows.h>
#include <iostream>
#include "header.h" DWORD WINAPI Fun_1(LPVOID p);
DWORD WINAPI Fun_2(LPVOID p); unsigned int counter = ;
RAII_CrtcSec cs; int main()
{
HANDLE h1, h2;
h1 = CreateThread(nullptr, , Fun_1, nullptr, , );
std::cout << "Thread 1 started...\n";
h2 = CreateThread(nullptr, , Fun_2, nullptr, , );
std::cout << "Thread 2 started...\n";
CloseHandle(h1);
CloseHandle(h2);
//
system("pause");
return ;
} DWORD WINAPI Fun_1(LPVOID p)
{
while (true)
{
cs.Lock();
++counter;
if (counter < )
{
std::cout << "Thread 1 counting " << counter << "...\n";
cs.Unlock();
}
else
{
cs.Unlock();
break;
}
}
return ;
} DWORD WINAPI Fun_2(LPVOID p)
{
while (true)
{
cs.Lock();
++counter;
if (counter < )
{
std::cout << "Thread 2 counting " << counter << "...\n";
cs.Unlock();
}
else
{
cs.Unlock();
break;
}
}
return ;
}

  上面的代码使用了windows提供的API中的临界区对象来实现线程同步。临界区是指一个访问共享资源的代码段,临界区对象则是指当用户使用某个线程访问共享资源时,必须使代码段独占该资源,不允许其他线程访问该资源。在该线程访问完资源后,其他线程才能对资源进行访问。Windows API提供了临界区对象的结构体CRITICAL_SECTION,对该对象的使用可总结为如下几步:

  1.InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection),该函数的作用是初始化临界区,唯一的参数是指向结构体CRITICAL_SECTION的指针变量。

  2.EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection),该函数的作用是使调用该函数的线程进入已经初始化的临界区,并拥有该临界区的所有权。这是一个阻塞函数,如果线程获得临界区的所有权成功,则该函数将返回,调用线程继续执行,否则该函数将一直等待,这样会造成该函数的调用线程也一直等待。如果不想让调用线程等待(非阻塞),则应该使用TryEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection)。

  3.LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection),该函数的作用是使调用该函数的线程离开临界区并释放对该临界区的所有权,以便让其他线程也获得访问该共享资源的机会。一定要在程序不适用临界区时调用该函数释放临界区所有权,否则程序将一直等待造成程序假死。

  4.DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection),该函数的作用是删除程序中已经被初始化的临界区。如果函数调用成功,则程序会将内存中的临界区删除,防止出现内存错误。

  该段代码的运行结果如下图所示:

  ③使用Windows API的事件对象:

 //main.cpp
#include <windows.h>
#include <iostream> DWORD WINAPI Fun_1(LPVOID p);
DWORD WINAPI Fun_2(LPVOID p); HANDLE h_event;
unsigned int counter = ; int main()
{
h_event = CreateEvent(nullptr, true, false, nullptr);
SetEvent(h_event);
HANDLE h1 = CreateThread(nullptr, , Fun_1, nullptr, , nullptr);
std::cout << "Thread 1 started...\n";
HANDLE h2 = CreateThread(nullptr, , Fun_2, nullptr, , nullptr);
std::cout << "Thread 2 started...\n";
CloseHandle(h1);
CloseHandle(h2);
//
system("pause");
return ;
} DWORD WINAPI Fun_1(LPVOID p)
{
while (true)
{
WaitForSingleObject(h_event, INFINITE);
ResetEvent(h_event);
if (counter < )
{
++counter;
std::cout << "Thread 1 counting " << counter << "...\n";
SetEvent(h_event);
}
else
{
SetEvent(h_event);
break;
}
}
return ;
} DWORD WINAPI Fun_2(LPVOID p)
{
while (true)
{
WaitForSingleObject(h_event, INFINITE);
ResetEvent(h_event);
if (counter < )
{
++counter;
std::cout << "Thread 2 counting " << counter << "...\n";
SetEvent(h_event);
}
else
{
SetEvent(h_event);
break;
}
}
return ;
}

  事件对象是一种内核对象,用户在程序中使用内核对象的有无信号状态来实现线程的同步。使用事件对象的步骤可概括如下:

  1.创建事件对象,函数原型为:

 HANDLE WINAPI CreateEvent(
_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,
_In_ BOOL bManualReset,
_In_ BOOL bInitialState,
_In_opt_ LPCTSTR lpName
);

  如果该函数调用成功,则返回新创建的事件对象,否则返回NULL。函数参数的含义如下:

  -lpEventAttributes:表示创建的事件对象的安全属性,若设为NULL则表示该程序使用的是默认安全属性。

  -bManualReset:表示所创建的事件对象是人工重置还是自动重置。若设为true,则表示使用人工重置,在调用线程获得事件对象所有权后用户要显式地调用ResetEvent()将事件对象设置为无信号状态。

  -bInitialState:表示事件对象的初始状态。如果为true,则表示该事件对象初始时为有信号状态,则线程可以使用事件对象。

  -lpName:表示事件对象的名称,若为NULL,则表示创建的是匿名事件对象。

  2.若事件对象初始状态设置为无信号,则需调用SetEvent(HANDLE hEvent)将其设置为有信号状态。ResetEvent(HANDLE hEvent)则用于将事件对象设置为无信号状态。

  3.线程通过调用WaitForSingleObject()主动请求事件对象,该函数原型如下:

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

  该函数将在用户指定的事件对象上等待。如果事件对象处于有信号状态,函数将返回。否则函数将一直等待,直到用户所指定的事件到达。

  该代码的运行结果如下图所示:

  

  ④使用Windows API的互斥对象:

 //main.cpp
#include <windows.h>
#include <iostream> DWORD WINAPI Fun_1(LPVOID p);
DWORD WINAPI Fun_2(LPVOID p); HANDLE h_mutex;
unsigned int counter = ; int main()
{
h_mutex = CreateMutex(nullptr, false, nullptr);
HANDLE h1 = CreateThread(nullptr, , Fun_1, nullptr, , nullptr);
std::cout << "Thread 1 started...\n";
HANDLE h2 = CreateThread(nullptr, , Fun_2, nullptr, , nullptr);
std::cout << "Thread 2 started...\n";
CloseHandle(h1);
CloseHandle(h2);
//
//CloseHandle(h_mutex);
system("pause");
return ;
} DWORD WINAPI Fun_1(LPVOID p)
{
while (true)
{
WaitForSingleObject(h_mutex, INFINITE);
if (counter < )
{
++counter;
std::cout << "Thread 1 counting " << counter << "...\n";
ReleaseMutex(h_mutex);
}
else
{
ReleaseMutex(h_mutex);
break;
}
}
return ;
} DWORD WINAPI Fun_2(LPVOID p)
{
while (true)
{
WaitForSingleObject(h_mutex, INFINITE);
if (counter < )
{
++counter;
std::cout << "Thread 2 counting " << counter << "...\n";
ReleaseMutex(h_mutex);
}
else
{
ReleaseMutex(h_mutex);
break;
}
}
return ;
}

  互斥对象的使用方法和c++标准库的mutex类似,互斥对象使用完后应记得释放。

C++实现线程同步的几种方式的更多相关文章

  1. IOS 多线程,线程同步的三种方式

    本文主要是讲述 IOS 多线程,线程同步的三种方式,更多IOS技术知识,请登陆疯狂软件教育官网. 一般情况下我们使用线程,在多个线程共同访问同一块资源.为保护线程资源的安全和线程访问的正确性. 在IO ...

  2. Java线程同步的四种方式详解(建议收藏)

    ​ Java线程同步属于Java多线程与并发编程的核心点,需要重点掌握,下面我就来详解Java线程同步的4种主要的实现方式@mikechen 目录 什么是线程同步 线程同步的几种方式 1.使用sync ...

  3. C++线程同步的四种方式(Windows)

    为什么要进行线程同步? 在程序中使用多线程时,一般很少有多个线程能在其生命期内进行完全独立的操作.更多的情况是一些线程进行某些处理操作,而其他的线程必须对其处理结果进行了解.正常情况下对这种处理结果的 ...

  4. C++ 线程同步的四种方式

    程之间通信的两个基本问题是互斥和同步. (1)线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒. (2)线程互 ...

  5. windows线程同步的几种方式

    以下为main函数的测试代码 具体线程同步的实现代码请下载:https://github.com/kingsunc/ThreadSynchro #include <stdio.h> #in ...

  6. Linux学习笔记21——线程同步的两种方式

    一  用信号量同步 1 信号量函数的名字都以sem_开头,线程中使用的基本信号量函数有4个 2 创建信号量 #include<semaphore.h> int sem_init(sem_t ...

  7. 【Linux】多线程同步的四种方式

    背景问题:在特定的应用场景下,多线程不进行同步会造成什么问题? 通过多线程模拟多窗口售票为例: #include <iostream> #include<pthread.h> ...

  8. java笔记--关于线程同步(7种同步方式)

    关于线程同步(7种方式) --如果朋友您想转载本文章请注明转载地址"http://www.cnblogs.com/XHJT/p/3897440.html"谢谢-- 为何要使用同步? ...

  9. iOS中保证线程安全的几种方式与性能对比

    来源:景铭巴巴 链接:http://www.jianshu.com/p/938d68ed832c 一.前言 前段时间看了几个开源项目,发现他们保持线程同步的方式各不相同,有@synchronized. ...

随机推荐

  1. 20145313张雪纯 《Java程序设计》第2周学习总结

    20145313张雪纯 <Java程序设计>第2周学习总结 教材学习内容总结 3.1.1类型 整数:分为short整数(2字节).int整数(4字节).long整数(8字节). 字节:by ...

  2. 20145335郝昊《java程序设计》第5周学习总结

    20145335郝昊<Java程序设计>第5周学习总结 教材学习内容总结 第八章 语法与继承架构 使用try.catch 特点: - 使用try.catch语法,JVM会尝试执行try区块 ...

  3. MySQL优化具体

    1. 查询与索引优化分析 在优化MySQL时,通常需要对数据库进行分析,常见的分析手段有慢查询日志,profiling分析,EXPLAIN分析查询,以及show命令查询系统状态及系统变量,通过定位分析 ...

  4. git分支合并脚本

    为什么要写这个脚本 工作中经常会有合并分支的操作,一开始也不以为然,后来发现如果更新频繁的话,根本就停不下来,昨天上午正好有空,就完成了下面的这个初版 可能存在的问题 一般的应用场景都是:从maste ...

  5. poj 2187:Beauty Contest(旋转卡壳)

    Time Limit: 3000MS   Memory Limit: 65536K Total Submissions: 32708   Accepted: 10156 Description Bes ...

  6. 【前端】jQuery选择器$()的实现原理

    今天三七互娱技术面试的时候面试官问了我这个问题,当时一脸懵逼,于是好好总结一下. 当我们使用jquery选择器的时候,$(s).回默认去执行jquery内部封装好的一个init的构造函数每次申明一个j ...

  7. optind变量

    1.这个变量是在什么地方定义的? 答:系统定义的 2.这个变量在什么场景下使用? 答:在解析命令行参数时会用到 3.这个变量存在的意义? 在每调用一次getopt()或getopt_long()类似函 ...

  8. linux下如何使用自己安装的SunJDK替换默认的OpenJDK

    在linux系统中,由于涉及到版权问题,在大部分linux系统的发行版本中,默认都安装了OpenJDK,并且OpenJDK的java命令也已经加入到环境变量中了. 在刚装好的linux系统中,运行ja ...

  9. .net 修改AD域中的密码

    1.通过vs 2013 新建一个web站点(不是空项目),这个会带一下模板, 2.然后新建一个页面UpdatePassWord.aspx aspx页面内容: <%@ Page Title=&qu ...

  10. 【eclipse】Multiple annotations found at this line:——解决方法

    问题截图: 就是eclipse的maven插件太旧了 用新插件新建的maven项目就没有报错 用软件对比了一下这两个pom文件 只有项目名有区别 所以就是插件的问题 一个简单安装离线maven插件的方 ...