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. ...
随机推荐
- SQL Server 2016 特性和安装方法
SQL Server 2016 特性: 全程加密技术(Always Encrypted),动态数据屏蔽(Dynamic Data Masking),JSON支持,多TempDB数据库文件,PolyBa ...
- Vue数据绑定失效
首先,我们得明白Vue数据响应的原理: 以对象为例:当把一个JavaScript对象传给Vue实例的data选项时,Vue将遍历此对象所有的属性,并使用Object.defineProperty把这些 ...
- tomcat结合httpd和nginx
httpd结合tomcat: 前提:httpd版本2.4以上,编译安装 httpd:192.168.223.136 tomcat:192.168.223.146 tomcat简单创建一个额外的weba ...
- 20144303 《Java程序设计》第八周学习总结
20144303 <Java程序设计>第八周学习总结 教材学习内容总结 第十五章 1.日志API简介: java.util.logging包提供了日志功能相关类与接口,不必额外配置日志组件 ...
- 轻谈Normalize.css
Normalize.css 是 * ? Normalize.css只是一个很小的CSS文件,但它在默认的HTML元素样式上提供了跨浏览器的高度一致性.相比于传统的CSS reset , Normali ...
- [翻译]理解CSS模块方法
在这个前端发展日新月异的世界,能够找到有所影响的概念相当困难,而将其准确无误的传达,让人们愿意尝试,更是难上加难. 拿CSS来看,在我们写CSS时,工具侧最大的变化,也就是CSS处理器的使用,如:可能 ...
- RabbitMQ 安装使用教程
环境 CentOS7 + Python3.5 yum -y install epel-release erlang socat cd /usr/local/src wget http://www.ra ...
- Tornado异步(2)
Tornado异步 因为epoll主要是用来解决网络IO的并发问题,所以Tornado的异步编程也主要体现在网络IO的异步上,即异步Web请求. 1. tornado.httpclient.Async ...
- AVL树 - 学习笔记
2017-08-29 14:35:55 writer:pprp AVL树就是带有平衡条件的二叉查找树.每个节点的左子树和右子树高度相差最多为1的二叉查找树 空树的高度定为-1 对树的修正称为旋转 对内 ...
- 【深入理解JVM】:Java对象的创建、内存布局、访问定位
对象的创建 一个简单的创建对象语句Clazz instance = new Clazz();包含的主要过程包括了类加载检查.对象分配内存.并发处理.内存空间初始化.对象设置.执行ini方法等. 主要流 ...