c++11多线程常用代码总结

关于

好记性不如烂笔头
理解虽然到位,但是时间长了就容易忘。
本文仅总结自己经常忘记的知识点, 详细解释多线程某些原理、概念。
抱着复习的态度总结此文。
本文参考: 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";
}

原文链接: https://www.cnblogs.com/pandamohist/p/13779671.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

    c++11多线程常用代码总结

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/203631

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月12日 下午9:35
下一篇 2023年2月12日 下午9:36

相关推荐