共享数据带来的问题:条件竞争

避免恶性条件竞争的方法:

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_locklock_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)的更多相关文章

  1. c++多线程并发学习笔记(0)

    多进程并发:将应用程序分为多个独立的进程,它们在同一时刻运行.如图所示,独立的进程可以通过进程间常规的通信渠道传递讯息(信号.套接字..文件.管道等等). 优点:1.操作系统在进程间提供附附加的保护操 ...

  2. c++多线程并发学习笔记(2)

    等待一个时间或其他条件 在一个线程等待完成任务时,会有很多选择: 1. 它可以持续的检查共享数据标志(用于做保护工作的互斥量),直到另一个线程完成工作时对这个标志进行重设.缺点:资源浪费,开销大 2. ...

  3. 多线程编程学习笔记——使用异步IO(一)

    接上文 多线程编程学习笔记——使用并发集合(一) 接上文 多线程编程学习笔记——使用并发集合(二) 接上文 多线程编程学习笔记——使用并发集合(三) 假设以下场景,如果在客户端运行程序,最的事情之一是 ...

  4. 多线程编程学习笔记——使用异步IO

    接上文 多线程编程学习笔记——使用并发集合(一) 接上文 多线程编程学习笔记——使用并发集合(二) 接上文 多线程编程学习笔记——使用并发集合(三) 假设以下场景,如果在客户端运行程序,最的事情之一是 ...

  5. Java多线程技术学习笔记(二)

    目录: 线程间的通信示例 等待唤醒机制 等待唤醒机制的优化 线程间通信经典问题:多生产者多消费者问题 多生产多消费问题的解决 JDK1.5之后的新加锁方式 多生产多消费问题的新解决办法 sleep和w ...

  6. 多线程编程学习笔记——async和await(一)

    接上文 多线程编程学习笔记——任务并行库(一) 接上文 多线程编程学习笔记——任务并行库(二) 接上文 多线程编程学习笔记——任务并行库(三) 接上文 多线程编程学习笔记——任务并行库(四) 通过前面 ...

  7. 多线程编程学习笔记——async和await(二)

    接上文 多线程编程学习笔记——async和await(一) 三.   对连续的异步任务使用await操作符 本示例学习如何阅读有多个await方法方法时,程序的实际流程是怎么样的,理解await的异步 ...

  8. 多线程编程学习笔记——async和await(三)

    接上文 多线程编程学习笔记——async和await(一) 接上文 多线程编程学习笔记——async和await(二) 五.   处理异步操作中的异常 本示例学习如何在异步函数中处理异常,学习如何对多 ...

  9. 多线程编程学习笔记——编写一个异步的HTTP服务器和客户端

    接上文 多线程编程学习笔记——使用异步IO 二.   编写一个异步的HTTP服务器和客户端 本节展示了如何编写一个简单的异步HTTP服务器. 1.程序代码如下. using System; using ...

随机推荐

  1. 【JZOJ2156】【2017.7.10普及】复仇者vsX战警之训练

    题目 月球上反凤凰装甲在凤凰之力附身霍普之前,将凤凰之力打成五份,分别附身在X战警五大战力上面辐射眼.白皇后.钢力士.秘客和纳摩上(好尴尬,汗). 在凤凰五使徒的至高的力量的威胁下,复仇者被迫逃到昆仑 ...

  2. 微信公众号发送告警Python脚本

    调用该脚本,可以向微信公众号发生告警. #!/bin/env python #coding:utf-8 #Author: Hogan #Descript : 微信公众号发送告警脚本 import ur ...

  3. Codeforces Round #303 (Div. 2) E. Paths and Trees Dijkstra堆优化+贪心(!!!)

    E. Paths and Trees time limit per test 3 seconds memory limit per test 256 megabytes input standard ...

  4. [BZOJ4305]数列的GCD:莫比乌斯反演+组合数学

    分析 一开始想的是对恰好\(k\)个位置容斥,结果发现对\(\gcd\)有些无从下手,想了想发现自己又sb了. 考虑对\(\gcd\)进行容斥处理,弱化条件,现在我们要求的是使\(\gcd\)是\(d ...

  5. 3D Computer Grapihcs Using OpenGL - 19 Vertex Array Object(顶点数组对象)

    大部分OpenGL教程都会在一开始就讲解VAO,但是该教程的作者认为这是很不合理的,因为要理解它的作用需要建立在我们此前学过的知识基础上.因此直到教程已经进行了一大半,作者才引入VAO这个概念.在我看 ...

  6. 南京网络赛C

    分段打表大法好!!! 打表40min,A题1s https://nanti.jisuanke.com/t/41300 #include<bits/stdc++.h> #define int ...

  7. 聊聊 Vue 的双向数据绑定,Model 如何改变 View,View 又是如何改变 Model 的

    todo defineProperty() 参考: https://www.cnblogs.com/wangjiachen666/p/9883916.html

  8. OI常用的常数优化小技巧

    注意:本文所介绍的优化并不是算法上的优化,那个就非常复杂了,不同题目有不同的优化.笔者要说的只是一些实用的常数优化小技巧,很简单,虽然效果可能不那么明显,但在对时间复杂度要求十分苛刻的时候,这些小的优 ...

  9. jsp四种属性范围

    在JSP提供了四种属性的保存范围.所谓的属性保存范围,指的就是一个设置的对象,可以在多个页面中保存并可以继续使用.它们分别是:page.request.session.appliction. 1.pa ...

  10. sqlite的系统表sqlite_master

      SQLite数据库中一个特殊的名叫 SQLITE_MASTER 上执行一个SELECT查询以获得所有表的索引.每一个 SQLite 数据库都有一个叫 SQLITE_MASTER 的表, 它定义数据 ...