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. ...
随机推荐
- opkg 不能更新和安装openwrt软件的方法
首先,将所有的IPK 放在自己的虚拟HTTP服务器上.2,用Telnet进入路由器,使用VI编辑器,编程Opkg.conf,命令: vi /etc/opkg.conf3,修改文件,将第一行 ...
- 20145324 Java实验四
在IDEA上操作 由于不会创建安卓模拟器失败 选择老师给的插件 成功 实验总结 开始开发安卓,感觉更难了,这次实验完全是看运气拼电脑的实验! 步骤 耗时 百分比 需求分析 10m 17% 设计 20m ...
- 20145331 《Java程序设计》第7周学习总结
20145331 <Java程序设计>第7周学习总结 教材学习内容总结 第十二章 Lambda 1.Lambda定义:一个不用被绑定到一个标识符上,并且可能被调用的函数. 2.在只有Lam ...
- make menuconfig错误的解决办法
如果使用make menuconfig的方式配置内核,又碰巧系统没有安装ncurses库(ubuntu系统默认就没有安装此库),就会出现错误,错误信息大体上如下: *** Unable to find ...
- SVN添加忽略目录
项目:Thinkphp 目录结构: Thinkphp |-- Common |-- Runtime |-- Home 忽略目标: Runtime 文件夹及下面所有文件 首先,需要忽略的目录必须没有加入 ...
- Swift学习笔记 - Swift属性只读
在OC中我们经常用到只读属性,用readonly修饰一下就行了,但在Swift中已经不是这样修饰的了,下面记录一下Swift中只读属性的使用 在OC中的只读: //只读属性 @property(rea ...
- [Network Architecture]Mask R-CNN论文解析(转)
前言 最近有一个idea需要去验证,比较忙,看完Mask R-CNN论文了,最近会去研究Mask R-CNN的代码,论文解析转载网上的两篇博客 技术挖掘者 remanented 文章1 论文题目:Ma ...
- 使用iview--1
在任意一个你想创建项目的路径下 每次输入就输一致的就可以 /*************************安装选项开始****************/ 回车再次回车就如下,输入Y 继续 回车,输 ...
- webstorm自动换行
1.文件 — — 设置 2. 编辑器 — — 编辑器 — — 在编辑窗口使用软换行(勾选)
- cygwin 获取root高级权限
cygwin安装完成后没有passwd文件解决方法