C++实现线程同步的几种方式
线程同步是指同一进程中的多个线程互相协调工作从而达到一致性。之所以需要线程同步,是因为多个线程同时对一个数据对象进行修改操作时,可能会对数据造成破坏,下面是多个线程同时修改同一数据造成破坏的例子:
#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++实现线程同步的几种方式的更多相关文章
- IOS 多线程,线程同步的三种方式
本文主要是讲述 IOS 多线程,线程同步的三种方式,更多IOS技术知识,请登陆疯狂软件教育官网. 一般情况下我们使用线程,在多个线程共同访问同一块资源.为保护线程资源的安全和线程访问的正确性. 在IO ...
- Java线程同步的四种方式详解(建议收藏)
Java线程同步属于Java多线程与并发编程的核心点,需要重点掌握,下面我就来详解Java线程同步的4种主要的实现方式@mikechen 目录 什么是线程同步 线程同步的几种方式 1.使用sync ...
- C++线程同步的四种方式(Windows)
为什么要进行线程同步? 在程序中使用多线程时,一般很少有多个线程能在其生命期内进行完全独立的操作.更多的情况是一些线程进行某些处理操作,而其他的线程必须对其处理结果进行了解.正常情况下对这种处理结果的 ...
- C++ 线程同步的四种方式
程之间通信的两个基本问题是互斥和同步. (1)线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒. (2)线程互 ...
- windows线程同步的几种方式
以下为main函数的测试代码 具体线程同步的实现代码请下载:https://github.com/kingsunc/ThreadSynchro #include <stdio.h> #in ...
- Linux学习笔记21——线程同步的两种方式
一 用信号量同步 1 信号量函数的名字都以sem_开头,线程中使用的基本信号量函数有4个 2 创建信号量 #include<semaphore.h> int sem_init(sem_t ...
- 【Linux】多线程同步的四种方式
背景问题:在特定的应用场景下,多线程不进行同步会造成什么问题? 通过多线程模拟多窗口售票为例: #include <iostream> #include<pthread.h> ...
- java笔记--关于线程同步(7种同步方式)
关于线程同步(7种方式) --如果朋友您想转载本文章请注明转载地址"http://www.cnblogs.com/XHJT/p/3897440.html"谢谢-- 为何要使用同步? ...
- iOS中保证线程安全的几种方式与性能对比
来源:景铭巴巴 链接:http://www.jianshu.com/p/938d68ed832c 一.前言 前段时间看了几个开源项目,发现他们保持线程同步的方式各不相同,有@synchronized. ...
随机推荐
- 索引原理-btree索引与hash索引的区别
btree索引与hash索引的区别,之前不清楚,mark一下. Hash索引结构的特殊性,其检索效率非常高,索引的检索可以一次定位,不像B-Tree索引需要从根节点到枝节点,最后才能访问到页节点这样多 ...
- springboot部署在云服务器上
1.window云服务器上 在本地的SpringBoot的根目录下 mvn clean package 打包jar 在云服务上安装jdk 将jar拷贝到云服务器上 在jar包所在的相应的位置,执行ja ...
- LA 3211 飞机调度(2—SAT)
https://vjudge.net/problem/UVALive-3211 题意: 有n架飞机需要着陆,每架飞机都可以选择“早着陆”和“晚着陆”两种方式之一,且必须选择一种,第i架飞机的早着陆时间 ...
- select * from table_name where 1=1的
我们先来看看这个语句的结果:select * from table where 1=1,其中where 1=1,由于1=1永远是成立的,返回TRUE,条件为真:所以,这条语句,就相当于select * ...
- linux环境变量 export命令详解
由host $ export DVSDK="${HOME}/ti-dvsdk_dm368-evm_xx_xx_xx_xx"引发的问题 1.${HOME}:首先, HOME 是个变量 ...
- 用Chrome查看微信访问需要OAuth2.0网页授权的页面
在PC浏览器打开某网站页面提示页面错误,是因为进行了OAuth2.0网页授权 有以下限制, 1.必须在微信打开 2.微信页面授权 其中第一步比较容易实现,修改下ua(user-agent),让其携带“ ...
- javascript闭包和立即执行函数的作用
一.闭包——closure 先看一个闭包的例子.我们想实现一个计数器,最简单的方法就是定义一个全局变量,计数的时候将其加1.但是全局变量有风险,哪里都有可能不小心改掉它.那局部变量呢, 它只在函数内部 ...
- 理解OpenID和OAuth的区别
在项目开发中,我们经常说授权认证,经常把他们放到一起去描述,那两者在本质上是有区别的,OpenID和OAuth就是我们说的认证和授权. OpenID:Authentication 认证 OAuth : ...
- python脚本9_打印斐波那契数列
#打印斐波那契数列 f0 = 0 f1 = 1 for n in range(2,101): fn = f1 + f0 if fn <= 100: print(fn) f0 = f1 f1 = ...
- Repodata is over 2 weeks old. Install yum-cron? Or run: yum makecache
1.事件描述:CentOS7下使用tree命令,发现该命令没有被安装,在安装的过程中发现yum报错 1 2 3 4 5 [root@openstack-01 ~]# tree -d bash: tre ...