c++多线程并发学习笔记(1)
共享数据带来的问题:条件竞争
避免恶性条件竞争的方法:
1. 对数据结构采用某种保护机制,确保只有进行修改的线程才能看到修改时的中间状态。从其他访问线程的角度来看,修改不是已经完成了,就是还没开始。
2. 对数据结构的设计进行修改,修改完的结构必须能完成一系列不可分割的变化,也就是保证每个不变量保持稳定的状态,这就是所谓的无锁编程。
3. 使用事务的方式去处理数据结构的更新(这里的"处理"就如同对数据库进行更新一样)。所需的一些数据和读取都存储在事务日志中,然后将之前的操作合为一步,再进行提交。当数据结构被另一个线程修 改后,或处理已经重启的情况下,提交就会无法进行,这称作为“软件事务内存”(software transactional memory (STM))。
使用互斥量来保护共享数据
主要实现方法:当访问共享数据前,将数据锁住,在访问结束后,再将数据解锁。线程库需要保证,当一个线程使用特定互斥量锁住共享数据时,其他的线程想要访问锁住的数据,都必须等到之前那个线程对数据进行解锁后,才能进行访问。这就保证了所有线程都能看到共享数据,并而不破坏不变量。
互斥量不是万能的,在使用时要注意以下问题:
1. 需要编排代码来保护数据的正确性
2. 避免接口间的竞争条件
3. 避免死锁
4. 对数据的保护太多或太少
c++中的互斥量
通过实例化 std::mutex 来创建互斥量实例,需要包含头文件<mutex>
使用方法:通过成员函数lock() 和unlock()来实现上锁和解锁
#include <iostream>
#include <thread>
#include <mutex> using namespace std; class Test
{
std::mutex m;
public:
void add(int& num)
{
m.lock();//上锁
++num;
cout << num << endl;
m.unlock();//解锁
}
}; int main()
{
int num = ;
Test test;
thread t1(&Test::add, &test, std::ref(num));
thread t2(&Test::add, &test, std::ref(num));
t1.join();
t2.join();
}
但是,实践中不推荐直接去调用成员函数,调用成员函数就意味着,必须在每个函数出口都要去调用unlock(),也包括异常的情况。C++标准库为互斥量提供了一个RAII语法的模板类std::lock_guard
,在构造时就能提供已锁的互斥量,并在析构的时候进行解锁,从而保证了一个已锁互斥量能被正确解锁。
对于上个例子而言,可以改造为:
#include <iostream>
#include <thread>
#include <mutex> using namespace std; class Test
{
std::mutex m;
public:
void add(int& num)
{
lock_guard<std::mutex> guard(m); //在构造时lock
++num;
cout << num << endl;
}//在析构时unlock
}; int main()
{
int num = ;
Test test;
thread t1(&Test::add, &test, std::ref(num));
thread t2(&Test::add, &test, std::ref(num));
t1.join();
t2.join();
}
另外要注意:在使用互斥量来保护数据时,要注意检查指针和引用。切勿将受保护数据的指针或引用传递到互斥锁作用域之外,无论是函数返回值,还是存储在外部可见内存,亦或是以参数的形式传递到用户提供的函数中去。只要没有成员函数通过返回值或者输出参数的形式,向其调用者返回指向受保护数据的指针或引用,数据就是安全的。
接口间的竞争
考虑一个std::stack,它有top(), pop(), empty()等方法。即使我们在每个方法调用内部使用互斥量std::mutex 进行保护,由于接口之间的依赖关系,还是会存在竞争。例如:在调用empty()和调用top()之间,可能有来自另一个线程的pop()调用并删除了最后一个元素。这是一个经典的条件竞争,使用互斥量对栈内部数据进行保护,但依旧不能阻止条件竞争的发生,这就是接口固有的问题。
死锁
线程有对锁的竞争:一对线程需要对他们所有的互斥量做一些操作,其中每个线程都有一个互斥量,且等待另一个解锁。这样没有线程能工作,因为他们都在等待对方释放互斥量。这种情况就是死锁,它的最大问题就是由两个或两个以上的互斥量来锁定一个操作。
避免死锁的一般方法:就是让两个互斥量总以相同的顺序上锁:总在互斥量B之前锁住互斥量A,就永远不会死锁。
std::lock
——可以一次性锁住多个(两个以上)的互斥量,并且没有副作用(死锁风险)。因为std::lock
要么将两个锁都锁住,要不一个都不锁。
// 这里的std::lock()需要包含<mutex>头文件
class some_big_object;
void swap(some_big_object& lhs,some_big_object& rhs);
class X
{
private:
some_big_object some_detail;
std::mutex m;
public:
X(some_big_object const& sd):some_detail(sd){} friend void swap(X& lhs, X& rhs)
{
if(&lhs==&rhs)
return;
std::lock(lhs.m,rhs.m); // 同时锁定
std::lock_guard<std::mutex> lock_a(lhs.m,std::adopt_lock); // std::adopt_lock作用是声明互斥量已在本线程锁定,std::lock_guard只是保证互斥量在作用域结束时被释放
std::lock_guard<std::mutex> lock_b(rhs.m,std::adopt_lock);
swap(lhs.some_detail,rhs.some_detail);
}
};
避免死锁的一些方法:
1. 避免嵌套锁
一个线程已获得一个锁时,再别去获取第二个。因为每个线程只持有一个锁,锁上就不会产生死锁。当你需要获取多个锁,使用一个std::lock
来做这件事(对获取锁的操作上锁),避免产生死锁。
2. 避免在持有锁时调用用户提供的代码
因为代码是用户提供的,你没有办法确定用户要做什么;用户程序可能做任何事情,包括获取锁。
3. 使用固定顺序获取锁
当硬性条件要求你获取两个或两个以上的锁,并且不能使用std::lock
单独操作来获取它们;那么最好在每个线程上,用固定的顺序获取它们(锁)。
4. 使用锁的层次结构
std::unique_lock——灵活的锁
互斥锁保证了线程间的同步,但是却将并行操作变成了串行操作,这对性能有很大的影响,所以我们要尽可能的减小锁定的区域,也就是使用细粒度锁。这一点lock_guard
做的不好,不够灵活,lock_guard
只能保证在析构的时候执行解锁操作,lock_guard
本身并没有提供加锁和解锁的接口,但是有些时候会有这种需求。
class LogFile {
std::mutex _mu;
ofstream f;
public:
LogFile() {
f.open("log.txt");
}
~LogFile() {
f.close();
}
void shared_print(string msg, int id) {
{
std::lock_guard<std::mutex> guard(_mu);
//do something 1
}
//do something 2
{
std::lock_guard<std::mutex> guard(_mu);
// do something 3
f << msg << id << endl;
cout << msg << id << endl;
}
} };
上述代码因为有两段代码需要上锁保护,所以使用lock_guard只能用两个局部变量来上锁和解锁,使用一个也可以,但锁的粒度太大,影响效率,这个时候就可以用unique_lock。
unique_lock它提供了lock()
和unlock()
接口,能记录现在处于上锁还是没上锁状态,在析构的时候,会根据当前状态来决定是否要进行解锁(lock_guard
就一定会解锁)。
上面的代码使用unique_lock可以修改为:
class LogFile {
std::mutex _mu;
ofstream f;
public:
LogFile() {
f.open("log.txt");
}
~LogFile() {
f.close();
}
void shared_print(string msg, int id) { std::unique_lock<std::mutex> guard(_mu);
//do something 1
guard.unlock(); //临时解锁 //do something 2 guard.lock(); //继续上锁
// do something 3
f << msg << id << endl;
cout << msg << id << endl;
// 结束时析构guard会临时解锁
// 这句话可要可不要,不写,析构的时候也会自动执行
// guard.ulock();
} };
另外,还可以使用std::defer_lock
设置初始化的时候不进行默认的上锁操作:
std::unique_lock<std::mutex> guard(_mu, std::defer_lock);
unique_lock的灵活是有代价的,因为它内部需要维护锁的状态,所以效率要比lock_guard
低一点,在lock_guard
能解决问题的时候,就是用lock_guard
,反之,使用unique_lock
。
另外,unique_lock
和lock_guard
都不能复制,lock_guard
不能移动,但是unique_lock
可以。
保护共享数据的初始化过程
某些场景下,我们需要代码只被执行一次,比如单例类的初始化,考虑到多线程安全,需要进行加锁控制。C++11中提供的call_once可以很好的满足这种需求。
#include<mutex> template <class Fn, class... Args>
void call_once (once_flag& flag, Fn&& fn, Args&&...args);
第一个参数是std::once_flag的对象(once_flag是不允许修改的,其拷贝构造函数和operator=函数都声明为delete),第二个参数可调用实体,即要求只执行一次的代码,后面可变参数是其参数列表。
call_once保证函数fn只被执行一次,如果有多个线程同时执行函数fn调用,则只有一个活动线程(active call)会执行函数,其他的线程在这个线程执行返回之前会处于”passive execution”(被动执行状态不会直接返回,直到活动线程对fn调用结束才返回。对于所有调用函数fn的并发线程,数据可见性都是同步的(一致的)。还有一个要注意的地方是 once_flag的生命周期,它必须要比使用它的线程的生命周期要长。所以通常定义成全局变量比较好。
一些其他的互斥锁
嵌套锁:std::recursive_mutex
除了可以对同一线程的单个实例上获取多个锁,其他功能与std::mutex
相同。互斥量锁住其他线程前,必须释放拥有的所有锁,所以当调用lock()三次后,也必须调用unlock()三次。
shared_mutex(c++17)/std::shared_timed_mutex(C++ 14)
shared_mutex的适用场景比较特殊:一个或多个读线程同时读取共享资源,且只有一个写线程来修改这个资源,这种情况下才能从shared_mutex获取性能优势。对于不需要去修改数据结构的线程,
可以使用std::shared_lock<std::shared_mutex>
获取访问权。
参考资料:
https://chenxiaowei.gitbook.io/c-concurrency-in-action-second-edition-2019/3.0-chinese/3.2-chinese
https://www.jianshu.com/p/34d219380d90
https://blog.csdn.net/xijiacun/article/details/71023777
c++多线程并发学习笔记(1)的更多相关文章
- c++多线程并发学习笔记(0)
多进程并发:将应用程序分为多个独立的进程,它们在同一时刻运行.如图所示,独立的进程可以通过进程间常规的通信渠道传递讯息(信号.套接字..文件.管道等等). 优点:1.操作系统在进程间提供附附加的保护操 ...
- c++多线程并发学习笔记(2)
等待一个时间或其他条件 在一个线程等待完成任务时,会有很多选择: 1. 它可以持续的检查共享数据标志(用于做保护工作的互斥量),直到另一个线程完成工作时对这个标志进行重设.缺点:资源浪费,开销大 2. ...
- 多线程编程学习笔记——使用异步IO(一)
接上文 多线程编程学习笔记——使用并发集合(一) 接上文 多线程编程学习笔记——使用并发集合(二) 接上文 多线程编程学习笔记——使用并发集合(三) 假设以下场景,如果在客户端运行程序,最的事情之一是 ...
- 多线程编程学习笔记——使用异步IO
接上文 多线程编程学习笔记——使用并发集合(一) 接上文 多线程编程学习笔记——使用并发集合(二) 接上文 多线程编程学习笔记——使用并发集合(三) 假设以下场景,如果在客户端运行程序,最的事情之一是 ...
- Java多线程技术学习笔记(二)
目录: 线程间的通信示例 等待唤醒机制 等待唤醒机制的优化 线程间通信经典问题:多生产者多消费者问题 多生产多消费问题的解决 JDK1.5之后的新加锁方式 多生产多消费问题的新解决办法 sleep和w ...
- 多线程编程学习笔记——async和await(一)
接上文 多线程编程学习笔记——任务并行库(一) 接上文 多线程编程学习笔记——任务并行库(二) 接上文 多线程编程学习笔记——任务并行库(三) 接上文 多线程编程学习笔记——任务并行库(四) 通过前面 ...
- 多线程编程学习笔记——async和await(二)
接上文 多线程编程学习笔记——async和await(一) 三. 对连续的异步任务使用await操作符 本示例学习如何阅读有多个await方法方法时,程序的实际流程是怎么样的,理解await的异步 ...
- 多线程编程学习笔记——async和await(三)
接上文 多线程编程学习笔记——async和await(一) 接上文 多线程编程学习笔记——async和await(二) 五. 处理异步操作中的异常 本示例学习如何在异步函数中处理异常,学习如何对多 ...
- 多线程编程学习笔记——编写一个异步的HTTP服务器和客户端
接上文 多线程编程学习笔记——使用异步IO 二. 编写一个异步的HTTP服务器和客户端 本节展示了如何编写一个简单的异步HTTP服务器. 1.程序代码如下. using System; using ...
随机推荐
- 【NOIP2016提高A组模拟8.17】(雅礼联考day1)总结
考的还ok,暴力分很多,但有点意外的错误. 第一题找规律的题目,推了好久.100分 第二题dp,没想到. 第三题树状数组.比赛上打了个分段,准备拿60分,因为时间不够,没有对拍,其中有分段的20分莫名 ...
- 【leetcode】1214.Two Sum BSTs
题目如下: Given two binary search trees, return True if and only if there is a node in the first tree an ...
- fiddler界面工具栏介绍(二)
工具栏介绍 1.Winconfig,Windows 使用了一种称为“AppContainer”的隔离技术,使得一些进程的流量无法捕获,打开WinConfig后可设置解除隔离. 2.气泡按钮,给sess ...
- 2019春Python程序设计作业1(0319-0325)
判断题 1-1 在Python 3.x中可以使用中文作为变量名. (2分) T F Python变量使用前必须先声明,并且一旦声明就不能再当前作用域内改变其类型.(2分) T ...
- Tomcat的安装、配置常见问题
(1)服务里面没有Tomcat怎么办? ——运行:cmd=>再到Tomcat 8.0/bin目录下运行: service install 即可: ——然后用: net start Tomc ...
- 湖南省第十二届省赛:Parenthesis
Description Bobo has a balanced parenthesis sequence P=p1 p2…pn of length n and q questions. The i-t ...
- python build-in function
目录(?)[-] absx alliterable anyiterable basestring binx boolx callableobject chri classmethodfunction ...
- MySQL_DML操作
DML(Data Manipulation Laguage)指对数据库数据的增(Create)删(Delete)改(Update)操作 1.增加操作 (1)先创建一个表,如图所示: 语法:Insert ...
- logstash之Filter插件
Logstash之所以强悍的主要原因是filter插件:通过过滤器的各种组合可以得到我们想要的结构化数据 1:grok正则表达式 grok**正则表达式是logstash非常重要的一个环节**:可以通 ...
- SpringMvc中@ModelAttribute的运用
/** * 1. 有 @ModelAttribute 标记的方法, 会在每个目标方法执行之前被 SpringMVC 调用! * 2. @ModelAttribute 注解也可以来修饰目标方法 POJO ...