关于

好记性不如烂笔头

理解虽然到位,但是时间长了就容易忘。

本文仅总结自己经常忘记的知识点, 详细解释多线程某些原理、概念。

抱着复习的态度总结此文。

本文参考: cppreference

欢迎指正

0.RAII机制

  • A、RAII=Resource Acquisition Is Initialization,由c++之父Bjarne Stroustrup提出:使用局部对象来管理资源的技术称为资源获取即初始化。
  • B、计算中的资源是有限的,内存套接字......比如,,递归就需要注意爆栈的情况。默认栈大小,win:8M, linux:1M, 递归爆栈是栈空间被用光了。
  • C、原理:充分的利用了C++语言局部对象自动销毁的特性来控制资源生命周期

1.lock_guard

  • 1.0 第一个参数为 std::mutex 变量,但是其没有提供lock的成员函数,因为是在构造函数 lock ,析构函数中 unlock
  • 1.1 可以传递2个参数,第二个参数指定为 adopt_lock ,则需要手动 lock
  • 1.2 若传递一个参数,则不需要手动 lock
  • 1.3 传递2个参数情况用法
void proc1(int a)
{
mtx.lock();//手动锁定
// adopt_lock: 当函数结束,g1将释放互斥锁
lock_guard<mutex> g1(mtx, adopt_lock);
......
}
  • 1.4 传递 1个参数 情况用法
void proc2(int a)
{
lock_guard<mutex> g2(mtx);//自动锁定
...
}

2.unique_lock

  • 2.1 std::unique_lock用法丰富,支持std::lock_guard()的原有功能
  • 2.2 std::unique_lock可以手动lock与手动unlock
  • 2.3 std::unique_lock的第二个参数可以是adopt_locktry_to_lockdefer_lock:
    类型 意义
    adopt_lock 需要手动lock,其构造函数不会lock,析构函数unlock
    try_to_lock 尝试去锁定,得保证锁处于unlock的状态,然后尝试现在能不能获得锁;尝试用mutx的lock()去锁定这个mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里,并继续往下执行;
    defer_lock 初始化一个没有加锁的mutex
  • 2.4 defer_lock 用法 注意,下面函数的最后一个lock,没有与之对应的unlock, 是因为析构函数中会自动解锁。
std::mutex mtx;

void thread_func()
{
// 初始化一个不加锁的mutex
std::unique_lock<std::mutex> locker(mtx, defer_lock);
...
// 现在是std::unique_lock接管mtx,不能调用mtx.lock()和mtx.unlock()
locker.lock();
.....
locker.unlock();
....
locker.lock();
}
  • 2.5 try_to_lock用法,注意 如果 加锁成功,函数结束后,locker将自动释放锁。还有,使用try_to_lock,如果失败,线程不会阻塞,将会继续向下执行。
std::mutex mtx;
void proc2()
{
//尝试加锁一次,如果加锁成功,会立即返回,不会阻塞在那里,且不会再次尝试锁操作。
unique_lock<mutex> locker(m,try_to_lock); // 加锁成功,则会获取到互斥锁的拥有权
if(locker.owns_lock())
{
; // do sth
}
// 加锁失败,则不会获取锁的拥有权,
else
{
; // do sth
}
}
  • 2.6 所有权转移, 使用std::move转移std::unique_lock的控制权,一个例子:
std::mutex mtx;
{
std::unique_lock<std::mutex> locker(m,defer_lock);
// 所有权转移,由to_locker来管理互斥量locker, locker已经失去所有权
std::unique_lock<std::mutex> to_locker(std::move(locker));
to_locker.lock();
...
to_locker.unlock();
...
to_locker.lock();
...
}

3.condition_variable

Note :这一章节偏长,自己对条件变量的理解不够深刻,故此加深理解

  • 3.1 注意std::condition_variable类通常与std::mutex类结合使用,std::condition_variable 通常使用 std::unique_lockstd::mutex 来等待

  • 3.2 作用: 同步线程,管理互斥量,控制线程访问共享资源的顺序

  • 3.3 常用函数

    函数名 解释
    wait 被调用的时候,使用 std::unique_lock 锁住当前线程, 当前线程会一直被阻塞,直到另外一个线程在相同的 std::condition_variable 对象上调用了 notify_onenotify_all 函数来唤醒线程
    wait_for 可以执行一个时间段,线程收到唤醒通知或者时间超时之前,该线程都会处于阻塞状态,如果收到唤醒通知或者时间超时,wait_for返回
    wait_until 与wait_for类似,只是wait_until可以指定一个时间点,在当前线程收到通知或者指定的时间点超时之前,该线程都会处于阻塞状态。如果超时或者收到唤醒通知,wait_until返回
    notify_one 唤醒某个等待(wait)线程。如果当前没有等待线程,则该函数什么也不做,如果同时存在多个等待线程,则随机唤醒一个等待的线程
    notify_all 唤醒所有等待(wait)的线程。如果当前没有等待线程,则该函数什么也不做,注意 惊群效应

    惊群效应

    当多个线程在等待同一个事件时,当事件发生后所有线程被唤醒,但只有一个线程可以被执行,其他线程又将被阻塞,进而造成计算机系统严重的上下文切换

  • 3.4 wait: 睡眠当前线程(阻塞操作)。 其实做了两步操作: A、睡眠当前线程等待条件发生,B、释放mutex,这样,其他线程就可以访问互斥对象。当收到 notify_one() 或者 notify_all() 信号,当前线程会重新尝试lock, 如果lock成功,则结束等待,函数wait就会返回,否则,则继续等待。

  • 3.5 为什么需要与 std::unique_lockstd::mutex 一起使用? 考虑下面情况:有两个线程A和B。线程A调用wait()但线程A 还没有进入 等待条件状态的时候,这时线程B调用函数notity_one()唤醒等待条件的线程。 如果不用mutex锁的话,线程B的notify_one()就 丢失了 。如果 加锁,情形:线程B必须等到 mutex 被释放(也就是 线程A的 wait() 释放锁并进入wait状态 ,此时线程B上锁) 的时候才能调用 notify_one(), 这样,notify_one() 就不会丢失

  • 3.6 虚假唤醒(spurious awakenings): 当调用函数 notify_one()notify_all() 唤醒, 处于等待的条件变量会重新进行互斥锁的竞争。没有得到互斥锁的线程就会发生等待转移(wait morphing),从等待信号量的队列中转移到等待互斥锁的队列中,一旦获取到互斥锁的所有权就会接着向下执行,但是此时其他线程已经执行并重置了执行条件,该线程执行就可并引发未定义的错误。

  • 3.7 避免 虚假唤醒,可以用下面的代码避免:

std::unique_lock<std::mutex> lock(_mutex);

// 避免虚假唤醒
while(!pred)
{
cv.wait(lock);
......
}
  • 3.8 唤醒的位置。 线程的唤醒都是在 内核,内核与内核的切换 和 内核与用户空间的切换,这些切换是有代价的。互斥的竞争也在内核中。有 2 种情况: A、先是互斥锁unlock, 再是唤醒notify_one/notify_all(下文简称 先unlock再唤醒); B、先是唤醒notify_one/notify_all,再是互斥锁unlock(下文简称先唤醒再unlock

    情况 结果
    先unlock再唤醒 等待的条件变量所在线程被唤醒后拿到互斥锁的所有权后立即向下执行
    先唤醒再unlock(Linux首推) 是等待条件变量的所在线程被唤醒, 是线程进入互斥锁的竞争队列,等待互斥锁的unlock(两次内核切换)
  • 3.9 wait函数

    • 3.9.1 形式
    序号 形式
    1 void wait( std::unique_lockstd::mutex& lock );
    2 template< class Predicate > void wait( std::unique_lockstd::mutex& lock, Predicate pred );
    • 3.9.2 参数
    参数 释义
    lock 类型为std :: unique_lock 的对象,该对象必须由当前线程锁定
    pred 条件表达式(断言),等同于 函数 bool pred(); true:wait的等待结束,false:继续等待
    • 3.9.3 返回值: 无

    • 3.9.4 用法, 代码来自 这里, but, 自己做了部分修改

// this is from https://en.cppreference.com/w/cpp/thread/condition_variable/wait
#pragma once
#include <iostream>
#include <thread>
#include <condition_variable>
#include <vector> std::condition_variable cv;
std::mutex cv_m; // This mutex is used for three purposes:
// 1) to synchronize accesses to i
// 2) to synchronize accesses to std::cerr
// 3) for the condition variable cv
int i = 0; void waits(int index)
{
// 1. it must be lock before waiting
std::unique_lock<std::mutex> lk(cv_m);
std::cerr << "index = " << index << "Waiting... \n";
// 2. block this thread
cv.wait(lk, [] {return i == 1; });
std::cerr << "index = " << index << "...finished waiting. i == 1\n"; } void signals()
{
std::this_thread::sleep_for(std::chrono::seconds(1));
{
std::lock_guard<std::mutex> lk(cv_m);
std::cerr << "Notifying...\n";
}
cv.notify_all(); std::this_thread::sleep_for(std::chrono::seconds(1));
{
std::lock_guard<std::mutex> lk(cv_m);
i = 1;
std::cerr << "Notifying again...\n";
}
cv.notify_all(); } int main(int argc, char *argv[])
{
vector<std::thread> thread_vec;
thread_vec.push_back(std::thread(waits, 1));
thread_vec.push_back(std::thread(waits, 2));
thread_vec.push_back(std::thread(waits, 3));
thread_vec.push_back(std::thread(signals)); std::for_each(thread_vec.begin(), thread_vec.end(), std::mem_fn(&std::thread::join)); return 0;
}

4.std::mutex

  • 4.1 作用 :保护临界资源,注意条件变量 不同,条件变量控制的是线程同步。
  • 4.2 配对使用: unlocklock 需要 配对使用。切记
  • 4.3 一个简单例子:
#pragma once
#include <iostream>
#include <thread>
#include <condition_variable>
#include <vector> // 用作保护临界资源:count_10
std::mutex mtx;
// 临界资源
int count_10 = 10;
void thread_func()
{
while (0 < count_10 )
{
// 记得需要手动 unlock
mtx.lock();
std::cout << "count = " << count_10-- << std::endl;
// unlock, 及时释放,服务其他线程
mtx.unlock(); std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
} int main(int argc, char *argv[])
{
std::vector<std::thread> thread_vec;
thread_vec.push_back(std::thread(thread_func));
thread_vec.push_back(std::thread(thread_func)); std::for_each(thread_vec.begin(), thread_vec.end(), std::mem_fn(&std::thread::join)); return 0;
}

5.std::async

  • 5.1 头文件: #include <future>
  • 5.2 std::async: 这是一个函数模板,用于异步执行函数,函数的返回值是一个std::future的对象,如其名,future:将来, 异步函数的返回值将会在将来的某个时刻返回,既然是将来的某个时刻返回,那么他现在是没有值的,类似 tensorflow 中的占位符。
  • 5.3 policy之 std::launch::asyncstd::launch::deferred
常量 释义
std::launch::async 新的执行线程(初始化所有线程局域对象后)立即执行,等同于使用std::thread创建线程,线程立即执行,区别于std::thread的是std::thread创建的线程无法获取线程函数的返回值
std::launch::deferred 延迟启动线程,甚至可能不会创建线程执行。通常调用 std::future 的get()/wait_*()启动线程
  • 5.4 std::shared_future, 与 std::future 名字差不多,功能也相似。如其名,shared,共享、分享之意。这两者都是 用来提前占位,保存线程所在函数的返回值。区别:

    类型 含义
    std::shared_future std::shared_future对象的get() 可被多次调用
    std::future std::future对象的get()只能调用一次
  • 5.5 std::future_status 查询延迟启动线程的状态,future_status状态定义如下:

    状态 含义
    std::future_status::deferred 异步操作还没开始
    std::future_status::timeout 异步操作超时
    std::future_status::ready 异步操作已经完成

当异步操作完成(std::future_status::ready),即可获取线程返回结果。一个例子:

......
std::future_status status;
do
{
// the thread is to start after 3 seconds
status = future.wait_for(std::chrono::seconds(3)); if (status == std::future_status::deferred)
{
std::cout << "deferred\n";
}
else if (status == std::future_status::timeout)
{
std::cout << "timeout\n";
}
else if (status == std::future_status::ready)
{
std::cout << "ready!\n";
} } while (status != std::future_status::ready);
......
  • 5.6 std::async的基本用法(完整版), 下面的例子分别使用 get()wait()wait_for()启动异步线程。
// 1. use get() to start the thread
std::future<int> f1 = std::async(std::launch::async, []()
{
return 3;
}); std::cout << f1.get() << std::endl; // 2. use wait() to start the thread
std::future<int> f2 = std::async(std::launch::async, []()
{
std::cout << 3 << endl;
}); f2.wait(); // 3. use wait_for() to start it
std::future<int> future = std::async(std::launch::async, []()
{
std::this_thread::sleep_for(std::chrono::seconds(3));
return 3;
}); std::cout << "waiting...\n";
std::future_status status;
do
{
status = future.wait_for(std::chrono::seconds(3)); if (status == std::future_status::deferred)
{
std::cout << "deferred\n";
}
else if (status == std::future_status::timeout)
{
std::cout << "timeout\n";
}
else if (status == std::future_status::ready)
{
std::cout << "ready!\n";
}
} while (status != std::future_status::ready); std::cout << "value = " << future.get();

6.std::atomic

Note: 自己在这方面使用偏偏偏 欢迎指正

  • 6.1 头文件:#include <atomic>

  • 6.2 原子操作: 线程不会被打断的代码执行片段。原子操作不可再分。状态只有 2 种: 操作完成操作没有完成。常用于一个变量, 而互斥常用于 临界资源,一片(段)代码。

  • 6.3 std::atomic 是一个模板类,且头文件种提供了常用的基础数据类型的原子类型: boolintcharlong......

  • 6.4 并发编程常用到 原子操作 等概念。 既然是并发,如何保证线程之间不会冲突? 我是这样理解的: 加锁解锁。线程A对 原子变量(可能不够准确,个人理解) 操作时,先加锁,xxx, 再解锁,线程B只能等待线程A释放锁才可以加锁。 只不过加锁解锁的过程是由 原子变量 本身提供的,不需要手动 lock 和 unlock

// 定义一个原子变量
std::atomic<int> _count_apple_10(10); // 线程A
void thread_a()
{
// 这行代码,原子变量将完成:加锁、读取、解锁
std::cout << "apple count = " << _count_apple_10 << "\n";
}

c++11多线程常用代码总结的更多相关文章

  1. 多线程常用代码 Future Callable Runable

    public class ThreadPoolTest { public static void main(String[] args) throws InterruptedException { E ...

  2. 11.多线程&&并发

    11.1 操作系统中线程和进程的概念 一些常见的概念: 程序:指令和数据的byte序列,eg:qq.exe;a2. 进程:正在运行的程序(如QQ);a3.一个进程中可能有一到多个线程. 线程的概念:T ...

  3. 转--Android实用的代码片段 常用代码总结

    这篇文章主要介绍了Android实用的代码片段 常用代码总结,需要的朋友可以参考下     1:查看是否有存储卡插入 复制代码 代码如下: String status=Environment.getE ...

  4. jquery常用代码集锦

    1. 如何修改jquery默认编码(例如默认GB2312改成 UTF-8 ) 1 2 3 4 5 $.ajaxSetup({     ajaxSettings : {         contentT ...

  5. C++11多线程教学(二)

    C++11多线程教学II 从我最近发布的C++11线程教学文章里,我们已经知道C++11线程写法与POSIX的pthreads写法相比,更为简洁.只需很少几个简单概念,我们就能搭建相当复杂的处理图片程 ...

  6. C++11多线程教学(一)

    本篇教学代码可在GitHub获得:https://github.com/sol-prog/threads. 在之前的教学中,我展示了一些最新进的C++11语言内容: 1. 正则表达式(http://s ...

  7. javascript常用代码大全

    http://caibaojian.com/288.html    原文链接 jquery选中radio //如果之前有选中的,则把选中radio取消掉 $("#tj_cat .pro_ca ...

  8. C++11多线程教学II

    从我最近发布的C++11线程教学文章里,我们已经知道C++11线程写法与POSIX的pthreads写法相比,更为简洁.只需很少几个简单概念,我们就能搭建相当复杂的处理图片程序,但是我们回避了线程同步 ...

  9. c++ 11 多线程教学(1)

    本篇教学代码可在GitHub获得:https://github.com/sol-prog/threads. 在之前的教学中,我展示了一些最新进的C++11语言内容: 1. 正则表达式(http://s ...

随机推荐

  1. 【python】python之list

    1.判断list是否为空 方式一: list_temp=[] if len(list_temp): #非空即为真 print('list is not empty') else: print('lis ...

  2. 【7】基于NGS检测体系变异解读和数据库介绍

    目录 解读相关专业术语 体系变异解读规则 体系变异和用药解读流程 主要数据库介绍 解读相关专业术语 2个概念:胚系.体系突变 4种变异类型:SNV.Indel.融合/SV(大的易位/倒位/缺失).CN ...

  3. Augustus指南(Trainning部分)

    Augustus指南 官方 Tutorial Index Augustus是一个真核生物基因预测软件,目前有网页服务端和本地版,它基于Hidden-Markov Model(隐马尔科夫链模型HMM)( ...

  4. 52-Linked List Cycle

    Linked List Cycle My Submissions QuestionEditorial Solution Total Accepted: 102785 Total Submissions ...

  5. 30-Container With Most Water-Leetcode

    Given n non-negative integers a1, a2, -, an, where each represents a point at coordinate (i, ai). n ...

  6. A Child's History of England.27

    Then, the Red King went over to Normandy, where the people suffered greatly under the loose rule of ...

  7. 【Go语言学习笔记】包

    包其实是每个大型工程都会使用的模块化工具. 将相关的代码封装成一个包,给其他项目调用,提供不同的功能. GO的设计是将一个文件夹看成一个包,虽然不一定非要用文件夹的名字,但是比较建议. 同一个文件夹下 ...

  8. Kafka入门教程(二)

    转自:https://blog.csdn.net/yuan_xw/article/details/79188061 Kafka集群环境安装 相关下载 JDK要求1.8版本以上. JDK安装教程:htt ...

  9. Linux 【复习巩固】

    目录 一.网络和服务 1.查看ip 2.查看主机名 配置 3.临时服务 1)基本语法(CentOS 6) 2)基本语法(CentOS 7) 3)示例 4.开机自启动服务 1)基本语法(CentOS 6 ...

  10. gitlab之实战部署

    #:准备Java环境,安装jdk root@ubuntu:~# cd /usr/local/src/ root@ubuntu:/usr/local/src# ls jdk-8u191-linux-x6 ...