【C++ 多线程编程|并发编程】



1.C++ 多线程编程|并发编程

阅读了大量优秀的博客(参考列表在文章最后),总结了C++多线程编程的相关知识如下,字数约18000字,应该是目前比较全面的了,感谢文末列表里优秀的文章。

文章目录

  • 1.C++ 多线程编程|并发编程
    • 1.1 简介
  • 2. thread类
    • 2.1 数据结构
      • 2.1.1 构造函数&析构函数
      • 2.1.2 常用成员函数
      • 2.1.3 用法
    • 2.2 示例
      • 示例1 简单构造
      • 示例2 批量构造 & 带参构造
      • 示例3 参数为引用
      • 示例4 线程调用函数模板
      • 示例5 detach
    • 2.3 互斥量(锁相关)
      • 2.3.1 简单示例
      • 2.3.2 mutex对象
      • 2.3.3 安全问题
        • 2.3.3.1 lock_guard
          • 2.3.3.1.1 用法
          • 2.3.3.1.2 源码分析
          • 2.3.3.1.3 adopt_lock参数
          • 2.3.3.1.4 总结
        • 2.3.3.2 unique_lock
          • 2.3.3.2.1 参数
            • adopt_lock参数
            • try_to_lock参数
            • defer_lock参数
          • 2.3.3.2.2 成员函数
            • std::unique_lock::owns_lock()
            • std::unique_lock::lock()
            • std::unique_lock::try_lock()
            • std::unique_lock::release()
          • 2.3.3.2.3 所有权传递
            • 移动语义
            • 移动构造函数
      • 2.3.4其他方面
        • 2.3.4.1 效率问题
          • std::atomic
        • 2.3.4.2 锁的粒度
        • 2.3.4.3 std::lock 同时锁住多个互斥量
      • 2.3.5 总结
  • 3. async函数
    • 3.1 简单示例
    • 3.2 函数模板
    • 3.3 异步 vs 同步
      • 3.3.1 异步启动 示例
      • 3.3.2 同步/延迟启动 示例
  • 4. 基础 线程同步及数据共享
    • 4.1 共享状态
    • 4.2 std::future
      • 4.2.1 数据结构
      • 4.2.2 创建std::future
        • 4.2.2.1 std::async 函数
        • 4.2.2.2 std::promise::get_future
        • 4.2.2.3 std::packaged_task::get_future
      • 4.2.3 示例 std::future::wait_for()
    • 4.3 std::promise
      • 4.3.1 数据结构
      • 4.3.2 示例 为什么要有promise
    • 4.4 总结
  • 5. 进阶 线程间的同步通信
    • 5.1 线程同步
    • 5.2 条件变量 condition_variable
      • 5.2.1 数据结构
      • 5.2.2 等待
        • 5.2.2.1 wait
        • 5.2.2.2 wait_for
        • 5.2.2.3 wait_until
      • 5.2.3 通知
        • 5.2.3.1 notify_one
        • 5.2.3.2 notify_all
      • 5.2.4 虚假唤醒 spurious wakeup
    • 5.3 条件变量 condition_variable_any
    • 5.4 经典示例:生产者-消费者模型
  • 6. 线程自我控制 this_thread
    • 6.1 sleep_for
    • 6.2 sleep_for,sleep与yield函数
        • 6.2.1 yield函数
        • 6.2.2 sleep系统函数
    • 6.3 时间相关:chrono时间库
        • 时间函数
  • 7. 参考

1.1 简介

一个程序只有一个进程,该进程拥有至少一个线程。

不同进程的地址空间不同,互不相关。

同一进程下的不同线程共享同一个地址空间。

C++11中引入了thread类(注意:这是一个类),使用它可以很方便的创建线程。创建的形式为thread t(func, arg1…),执行完此行代码,线程函数就直接运行起来了,不需要单独的启动函数。不过主线程中仍然需要join()来等待子线程运行完毕。thread中线程对变量的操作,通过提前定义全局函数的方式传参,也可以普通变量用引用的方式传参。

此外C++11中还引入了async函数(注意:这是一个函数),是一种更高层的异步方式。它不必显式手写join函数来让主线程等待子线程。它可以用std::future类型的变量来接收线程函数运行的结果并通过get()的方法来获得结果。这样就不必像thread那样提前定义一个全局变量,在线程函数中进行赋值操作。此外async还可以指定线程创建策略–是立马执行还是延迟执行。

2. thread类

2.1 数据结构

先列一下thread类的数据结构,用法在下文。

2.1.1 构造函数&析构函数

函数 类别 作用
thread() noexcept 默认构造函数 创建一个线程,什么也不做
template
explicit thread(Fn&& fn, Args&&… args)
初始化构造函数 创建一个线程,以args为参数执行fn函数
thread(const thread&) = delete 复制构造函数 (已删除)
thread(thread&& x) noexcept 移动构造函数 构造一个与x相同的对象,会破坏x对象
~thread() 析构函数 析构对象

2.1.2 常用成员函数

函数 作用
void join() 等待子线程,主线程处于阻塞模式,直到子线程线程执行完毕, join才返回,join()执行完成之后,底层线程id被设置为0,即joinable()变为false。注意:thread1.join();这样的语句,主线程会阻塞在这一行,只有当thread1这个线程执行完毕了,代码才会运行下一行。
bool joinable() 返回线程是否可以执行join函数,即检测线程对象是否表示是一个活动的执行线程
void detach() 将线程与调用其的主线程分离,彼此独立执行(此函数必须在线程创建时立即调用,且调用此函数会使其不能被join)。调用join函数以后就会停下来等待线程执行完毕然后继续下一步,这是同步的方式,而detach是一种异步的方式,下面有示例
std::thread::id get_id() 获取线程id
thread& operator=(thread &&rhs) 见移动构造函数(如果对象是joinable的,那么会调用std::terminate()结果程序)

2.1.3 用法

首先定义一个线程。

template 
explicit thread(Fn&& fn, Args&&… args)

fn是线程调用的函数,argsfn用到的参数,如果是无参数的函数,可以缺省。

值得注意的是,fn一般是**无返回值的函数**(待定)。而asyn函数创建的线程函数是带返回值的。

比如,无参数函数,见示例1,带参数函数见示例2。

注意1:主线程定义了thread类子线程之后,一定要在主线程内join,否则程序会报错,异常退出。

注意2:Args&&… args意味着fn的参数传递必须是右值引用,如果是常量,直接传就行,如果是一个左值(简单理解为变量),传递引用参数需要std::refstd::cref将其转为右值引用,详解见示例3。

2.2 示例

示例1 简单构造

#include 
#include 
using namespace std;
void func1()
{
    cout << "func_1 " << endl;
}
void func2()
{
    cout << "func_2 " << endl;
}
int main()
{
    thread p1(func1), p2(func2);
    p1.join();
    p2.join();
    return 0;
}

输出:

func_1 func_2

示例2 批量构造 & 带参构造

#include 
#include 
using namespace std;
void func(int id, unsigned int num)
{
    for(int i = 0; i < num; i++);
    cout << "Thread " << id << " finished." << endl;
}
int main()
{
    thread th[10];
    for (int i = 0; i < 10; i++)
    {
        th[i] = thread(func, i, 100000);
    }
    for (int i = 0; i < 10; i++)
    {
        th[i].join();
    }
    return 0;
}

输出:

Thread Thread 2 finished.
Thread 8Thread finished.Thread 9Thread finished.Thread
7Threadfinished.Thread 30 finished.Thread
4 finished.1
5finished. finished
 
 finished

6finished

注意:运算结果,每次都会不一样。不仅每一个线程的运算结束时间是随机的,而且打印的顺序也是乱的,各个线程的打印是交叉的,同时向同一个窗口输出,不会等某个线程的某一行代码执行完才执行另一个线程。

cout << "Thread " << id << " finished." << endl;

也就是说,每个线程运行这一行代码时,不会管其他的线程,所以线程间语序是交叉的。

但是,上述输出顺序交叉的本质原因是:

线程是在thread对象被定义的时刻开始运行的。

那么,在每次定义thread对象后,让程序短暂地暂停一下,输出就是顺序的了(因为程序运行地太快了,所有线程都差不多在同一时刻开始运行,让它们错开就行了)。测试一下:

#include 
#include 
#include 

using namespace std;

void func(int id, unsigned int num)
{
    for(int i = 0; i < num; i++)
    {
        ;
    }
    cout << "Thread " << id << " finished." << endl;
}

int main()
{
    thread th[10];

    for (int i = 0; i < 10; i++)
    {
        th[i] = thread(func, i, 100000);
        Sleep(1); //定义后暂停1ms
    }

    for (int i = 0; i < 10; i++)
    {
        th[i].join();
    }

    return 0;
}

输出:

Thread 0 finished.
Thread 1 finished.
Thread 2 finished.
Thread 3 finished.
Thread 4 finished.
Thread 5 finished.
Thread 6 finished.
Thread 7 finished.
Thread 8 finished.
Thread 9 finished.

示例3 参数为引用

线程函数传参为引用:

#include 
#include 
#include 
using namespace std;

void func(int& n, int number)
{
    n = number;
}

int main()
{
    thread th[10];
    int nums[10];

    for (int i = 0; i < 10; i++)
    {
        th[i] = thread(func, nums[i], i);
        Sleep(1);
    }
    for (int i = 0; i < 10; i++)
    {
        th[i].join();
    }
    for (int i = 0; i < 10; i++)
    {
        cout << nums[i] << " ";
    }

    return 0;
}

按照上面写,会报错:

std::thread::thread(_Callable&&, _Args&& ...) [with _Callable = void (&)(int&, int); _Args = {int&, int&}]

分析一下,&&表示传入的参数需要是右值引用。什么是右值左值?

右值只能在‘=’右边,不能取地址,不能修改;左值可以在‘=’左右两边,可以取地址,可以修改。简单的说法,右值是常量,左值是变量。

注意:const修饰的左值不能赋值,只能取地址。

那么右值引用呢?对右值的引用,就变成左值了,可以取地址可以更改。示例:

int num = 10;
// int && a = num;  //错误,右值引用不能初始化为左值
int && a = 10;  //正确,右值引用初始化为常量
a = 100;  //a是右值引用,值可以改

回到正题。

thread的定义要求是这样的:

template
explicit thread(_Callable&& __f, _Args&&... __args)  //explicit只对构造函数起作用,用来抑制隐式转换

这是在说,thread是用函数模板定义的,thread()的参数,第一个是函数f,其余的是函数f的参数,且必须是右值引用。为什么必须是右值引用?就是为了函数f可能会需要改变参数的值,比如当前情景。

再看这个测试用例。

th[i] = thread(func, nums[i], i);

第二个参数是i,是个常量,而上面也说了右值引用可以是一个常量,所以i没问题。

但是第一个参数nums[i]明显是一个变量,是一个可以取地址的左值。分不清楚左值右值,可以对它取地址试试,能取地址就是左值

那么怎么传递一个左值进去呢?std::ref和std::cref。

std::ref ,用于取某个变量的引用,解决函数式编程的传参问题,多线程std::thread的可调用函数对象期望入参为引用时,必须显式第通过std::ref来绑定引用进行传参。

std::cref ,用于包装按const引用传递的值,不希望这个引用的值传进去后被改变。

所以只需要写成下面这样就行。

th[i] = thread(func, std::ref(nums[i]), i);

输出:

0 1 2 3 4 5 6 7 8 9

示例4 线程调用函数模板

使用函数模板来定义thread。

#include 
#include 
#include 
using namespace std;
//函数模板
template
void func(T n)
{
    cout << n << endl;
}

int main()
{
    thread th[10];
    for (int i = 0; i < 10; i++)
    {
        th[i] = thread(func, i);
        Sleep(1);
    }
    for (int i = 0; i < 10; i++)
    {
        th[i].join();
    }
    return 0;
}

输出:

0 
1 
2 
3
4
5
6
7
8
9

示例5 detach

1.detach要在线程对象创建后立刻调用;

2.调用了detach后,子线程就和主线程执行分离了,不需要再对子线程执行join。而没有detach的子线程必须执行join,否则会出错;

3.主线程不会等待子线程线程结束然后结束,也就是说,主线程和子线程的执行互相独立,但是主线程结束后,它下面的所有子线程都立刻结束,所以在使用detach() 需要确保主线程不会结束。

4.没有detach的子线程,和主线程的运行是同步的,变现为主线程需要通过join阻塞自己来等待子线程结束;而被detach的子线程与主线程的运行完全是异步的了。

#include 
#include 
#include 
using namespace std;
void func(int id)
{
    for(int i = 0; i < 10; i++)
    {
        cout << "Thread " << id << ": " << i << endl;
        Sleep(1000+id);
    }
}

int main()
{
    thread th[10];
    for (int i = 0; i < 10; i++)
    {
        th[i] = thread(func, i);
        th[i].detach();//thread对象创建后立刻detach,后面不需要join
        Sleep(1);
    }
    Sleep(13000);//防止主线程结束后,导致子线程全部结束
    return 0;
}

2.3 互斥量(锁相关)

2.3.1 简单示例

设计一个程序,开辟100个线程,每个线程都对初始值为0的全局变量n做10,000次自增。

#include 
#include 
#include 
#include 

using namespace std;
int n = 0;

void func_increase()
{
    for (int i = 0; i < 10000; i++)
    {
        n++;
    }
}
int main()
{
    thread th[100];

    for (thread &t:th)  // foreach用法
    {
        t = thread(func_increase);
    }
    for (thread &t:th)
    {
        t.join();
    }
    cout << n << endl;
    return 0;
}

理想的结果是1,000,000,但实际结果是:

906216

而且每次运行都不一样。这是因为100个线程都对同一个变量操作,同一时刻会有冲突的,有操作失败的情况。那么,就要考虑多线程需要对同一个变量操作怎么办?

答案是对变量加锁,保证变量每个时刻只能由一个线程操作,解锁后其他线程才能操作它。

mutex的翻译就是互斥锁。

最基本的方式是使用C++11中的互斥量std::mutex,一个线程将mutex锁住时,其他线程必须等解锁后才能操作。

锁住后必须解锁,不然就会出现死锁。

#include 
#include 
#include 
#include   //头文件

using namespace std;
int n = 0;
mutex mtx;  //定义mutex

void func_increase()
{
    for (int i = 0; i < 10000; i++)
    {
        mtx.lock();  //加锁
        n++;
        mtx.unlock();  //解锁
    }
}
int main()
{
    thread th[100];

    for (thread &t:th)
    {
        t = thread(func_increase);
    }
    for (thread &t:th)
    {
        t.join();
    }
    cout << n << endl;
    return 0;
}

此时,结果就是完美的1,000,000。

2.3.2 mutex对象

函数 作用
void lock() 将mutex上锁。
如果mutex已经被其它线程上锁,
那么会阻塞,直到解锁;
如果mutex已经被同一个线程锁住,
那么会产生死锁。
void unlock() 解锁mutex,释放其所有权。
如果有线程因为调用lock()不能上锁而被阻塞,则调用此函数会将mutex的主动权随机交给其中一个线程;
如果mutex不是被此线程上锁,那么会引发未定义的异常。
bool try_lock() 尝试将mutex上锁。
如果mutex未被上锁,则将其上锁并返回true;
如果mutex已被锁则返回false。

2.3.3 安全问题

2.3.3.1 lock_guard
2.3.3.1.1 用法

mutex是不安全的,如果lock()和unlock()之间出错,unlock()没有执行,其他线程就死锁了。

lock_guard的作用:为了防止在线程使用mutex加锁后异常退出导致死锁的问题,建议使用lock_guard代替mutex。

lock_guard在头文件mutex中定义。构造函数:
explicit lock_guard (mutex_type& m);

创建时加锁,析构时解锁。

lock_guard对象在构造的时候对mutex进行加锁,析构的时候对这个mutex对象解锁,也就是说要先定义一个mutex对象,再用这个mutex对象定义一个lock_guard对象,这个lock_guard对象守护着mutex对象,保证mutex对象总能被unlock,避免死锁。

lock_guard不能在中途解锁,只能通过析构时解锁。

lock_guard对象不能被拷贝和移动。

mutex的示例最好改造成:

#include 
#include 
#include 
#include   //定义了lock_guard

using namespace std;
int n = 0;
mutex mtx;  //定义mutex

void func_increase()
{
    for (int i = 0; i < 10000; i++)
    {
        lock_guard guard(mtx);//作用域开始
        n++;//作用域结束
    }
}
int main()
{
    thread th[100];

    for (thread &t:th)
    {
        t = thread(func_increase);
    }
    for (thread &t:th)
    {
        t.join();
    }
    cout << n << endl;
    return 0;
}

那么,“创建时加锁,析构时解锁”怎么理解?

其实就是,当lock_guard对象离开作用域时自动解锁mutex,从而避免加锁后没有解锁的问题。
具体来讲:for循环的某一层循环中,通过

lock_guard guard(mtx);

定义了一个lock_guard对象,作用域就开始了。此时相当于执行了一次mtx.lock()。

如果程序没发生错误,那么在结束当前轮次循环时(也就是遇到了代码块后面的大括号‘}’),就是要离开作用域了,此时隐式地自动解锁,相当于隐式地运行了一次mtx.unlock()。

如果在这个作用域内代码执行出了任何错误,退出之前lock_guard都会保证先解锁再退出(也就是保证能隐式地运行一次mtx.unlock()),避免因出错导致的死锁。

2.3.3.1.2 源码分析

查看std::lock_guard的源码:

template 
class lock_guard { // class with destructor that unlocks a mutex
public:
    using mutex_type = _Mutex;
	//无adopt_lock参数,构造时加锁
    explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) { // construct and lock
        _MyMutex.lock();
    }
	//有adopt_lock参数,构造时不加锁
    lock_guard(_Mutex& _Mtx, adopt_lock_t) : _MyMutex(_Mtx) {} // construct but don't lock
	//析构解锁
    ~lock_guard() noexcept {
        _MyMutex.unlock();
    }
	//屏蔽拷贝构造
    lock_guard(const lock_guard&) = delete; 
    lock_guard& operator=(const lock_guard&) = delete; 

private:
    _Mutex& _MyMutex;
};

就会发现,lock_guard类有个私有变量是mutex类型,构造和析构的时候分别对这个mutex私有变量进行lock和unlock,这就解释了“创建时加锁,析构时解锁”。lock_guard对象离开作用域时的析构,会触发unlock。

2.3.3.1.3 adopt_lock参数

adopt_lock参数只影响lock_guard类对象在构造时是否执行lock操作。

lock_guard类的对象有两种构造方式,对应两个构造函数。

第一种,无adopt_lock参数。

mutex mtx;//先定义mutex对象
lock_guard guard(mtx);//再定义lock_guard对象

此时,lock_guard类的对象guard被构造时,就执行lock,即“构造时上锁”。

第二种,有adopt_lock参数。

mutex mtx;//先定义mutex对象
mtx.lock();
lock_guard guard(mtx, std::adopt_lock);//再定义lock_guard对象

此时,lock_guard类的对象guard只构造,不lock,因为调用的构造函数中没有_MyMutex.lock();。即“构造时不上锁”。这就要求mutex对象先调用mutex.lock()将自己上锁,再将自己用lock_guard类包装一下,保证能够自动解锁。

也就是说,adopt_lock参数决定了上锁这个步骤由mutex执行,还是由lock_guard执行

2.3.3.1.4 总结

std::lock_guard是非常巧妙的一种设计思路,利用类对象的生命周期,构造时使互斥量加锁,析构时使互斥量解锁,从而实现其作用域内对互斥量的管理。lock_guard是对mutex的守护机制

2.3.3.2 unique_lock

unique_lock是一个比lock_guard 效率差一点的自动锁。同样,unique_lock也是一个类模板,但是比起lock_guard,它有自己的成员函数来更加灵活进行锁的操作,所以功能强大且灵活度强。

工作中,一般lock_guard就足够了(推荐使用)。

unique_lock使用方式和lock_guard一样,最终也是通过析构函数来unlock。

不同的是unique_lock有不一样的参数和成员函数。它的定义是这样的:

std::unique_lock my_unique(mtx1);
2.3.3.2.1 参数

lock_guard只有一种参数std::adopt_lock。

unique_lock有3中参数:std::adopt_lock,std::try_to_lock,std::defer_lock。

adopt_lock参数
std::unique_lock my_unique(mtx1,std::adopt_lock);

表示互斥量已经被lock,不需要再重复lock。该互斥量之前必须已经lock,提前加锁才可以使用该参数。

这点和lock_guard差不多。

try_to_lock参数
std::unique_lock my_unique(mtx1,std::try_to_lock);

可以避免一些不必要的等待,会判断当前mutex能否被lock,如果不能被lock,可以先去执行其他代码。这个和adopt不同,不需要自己提前加锁。举个例子来说就是如果有一个线程被lock,而且执行时间很长,那么另一个线程一般会被阻塞在那里,反而会造成时间的浪费。那么使用了try_to_lock后,如果被锁住了,它不会在那里阻塞等待,它可以先去执行其他没有被锁的代码。

示例,用两个线程完成一个复杂计算,比如计算1到10000的求和(模拟一下复杂计算):

#include 
#include 
#include 
#include 
using namespace std;
mutex mtx1;
void myThread1(int &ans)
{
    for (int i = 1; i <= 5000; )
    {
        std::unique_lock my_unique(mtx1, std::try_to_lock);
        if (my_unique.owns_lock() == true)  //当前unique_lock拥有对mtx1的所有权
        {
            ans += i;
            i++;//注意:计算之后再++,如果把i++放在for()里,就会跳过很多i
        }
        else
        {
            //如果当前my_unique没有对mtx1的所有权,与其阻塞在这,不如干些别的
            //执行一些没有共享内存的代码,充分利用计算资源
        }
    }
}

void myThread2(int &ans)
{
    for (int i = 5001; i <= 10000; )
    {
        std::unique_lock my_unique(mtx1, std::try_to_lock);
        if (my_unique.owns_lock() == true)
        {
            ans += i;
            i++;
        }
        else
        {
            //执行一些没有共享内存的代码
        }
    }
}
int main()
{
    int ans = 0;
    thread thread1(myThread1, std::ref(ans));
    thread thread2(myThread2, std::ref(ans));
    thread1.join();
    thread2.join();
    cout << ans << endl;
    return 0;
}

两个线程都分到了总任务的一半,每个线程获得mtx所有权的时候就去计算,没有mtx1所有权的时候也不闲着(否则就阻塞在哪,等另一个线程释放mtx1)。

这个示例用到了unique_lock的成员函数owns_lock(),

defer_lock参数
std::unique_lock my_unique(mtx1,std::defer_lock);

这个参数表示暂时先不lock,即延迟上锁,之后手动去lock,但是使用之前也是不允许去lock。一般用来搭配unique_lock的成员函数去使用。当使用了defer_lock参数时,在创建了unique_lock的对象时就不会自动加锁,那么就需要借助lock这个成员函数来进行手动加锁,当然也有unlock来手动解锁。

2.3.3.2.2 成员函数
std::unique_lock::owns_lock()

可以和try_to_lock参数搭配使用,示例见try_to_lock参数章节的示例。

std::unique_lock::lock()

与defer_lock参数搭配使用示例:

#include 
#include 
#include 
#include 
using namespace std;
mutex mtx1;
void myThread1()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::unique_lock my_unique(mtx1, std::defer_lock);//myThread2释放mtx1之前,会一直阻塞,释放后会立刻=执行这一句
    my_unique.lock();
    cout << "myThread1: mtx1 lock." << endl;    
    my_unique.unlock();
    cout << "myThread1: mtx1 unlock." << endl;
}
void myThread2()
{
    std::unique_lock my_unique(mtx1, std::defer_lock);
    my_unique.lock();    
    cout << "myThread2: mtx1 lock." << endl;
    std::this_thread::sleep_for(std::chrono::seconds(2));    
    my_unique.unlock();
    cout << "myThread2: mtx1 unlock." << endl;
}
int main()
{
    thread thread1(myThread1);
    thread thread2(myThread2);
    thread1.join();
    thread2.join();
    return 0;
}
std::unique_lock::try_lock()

和try_to_lock参数的作用差不多,判断当前是否能lock,如果不能,先去执行其他的代码并返回false,如果可以,进行加锁并返回true。

void myThread1(int &ans)
{
    for (int i = 1; i <= 5000; )
    {
        std::unique_lock my_unique(mtx1, std::defer_lock);
        if (my_unique.try_lock() == true)
        {
            ans += i;
            i++;
        }
        else
        {
            //执行一些没有共享内存的代码,充分利用计算资源
        }
    }
}
void myThread2(int &ans)
{
    for (int i = 5001; i <= 10000; )
    {
        std::unique_lock my_unique(mtx1, std::defer_lock);
        if (my_unique.try_lock() == true)
        {
            ans += i;
            i++;
        }
        else
        {
            //执行一些没有共享内存的代码
        }
    }
}
std::unique_lock::release()

返回它所管理的mutex对象指针,并释放所有权;也就是说,这个unique_lock和mutex不再有关系。只是释放所有权,严格区分unlock()与release()的区别,不要混淆

release返回的是原始mutex的指针。

如果原来mutex对像处于加锁状态,你有责任接管过来并负责解锁。

也就是说:

mutex *p = my_unique2.release();  // release()返回mtx1的指针
p->unlock();

下面示例,mtx1先由myThread2占有,myThread2释放了mtx1之后,myThread1占有。

void myThread1(int &ans)
{
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::unique_lock my_unique1(mtx1);
    cout << "myThread1: mtx1 lock, bind to my_unique1\n";
}

void myThread2(int &ans)
{
    std::unique_lock my_unique2(mtx1);
    cout << "myThread2: mtx1 lock, bind to my_unique2\n";

    std::this_thread::sleep_for(std::chrono::seconds(2));

    mutex *p = my_unique2.release();  // release()返回mtx1的指针
    cout << "myThread2: mtx1 released by my_unique2\n";
    p->unlock();
    cout << "myThread2: mtx1 unlock\n";
}
2.3.3.2.3 所有权传递

unique_lock的一个对象只能和一个mutex锁唯一对应,不能存在一对多或者多对一的情况,不然会造成死锁的出现。所以如果想要传递两个unique_lock对象对mutex的权限,需要运用到移动语义或者移动构造函数两种方法。

移动语义或者移动构造函数都是为了避免复制构造函数高开销的C++11新特性。

移动语义

C++11引入移动语义,源对象资源的控制权全部交给目标对象。移动语义通过使用**右值引用(&&)**来实现。右值引用表示一个将要被销毁的临时对象或者一个可以被转移所有权的对象。

在C++11中,标准库在utility中提供了一个有用的函数std::move,std::move并不能移动任何东西,它唯一的功能是将一个左值强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义。从实现上讲,std::move基本等同于一个类型转换:static_cast(lvalue);

std::move函数可以以非常简单的方式将左值引用转换为右值引用。 通过std::move,可以避免不必要的拷贝操作。 std::move是为性能而生。 std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝。

std::unique_lock munique1(mtx);
std::unique_lock munique2(std::move(mtx));
// 此时munique1失去mtx的权限,并指向空值,munique2获取mlock的权限
移动构造函数

所谓移动构造函数,就是借用一个临时对象,把它原本控制的内存的空间转移给构造出来的对象,这样就相当于把它移动过去了。

移动构造函数的目的是为了代替C++11之前的复制构造函数,C++11之前如果要将源对象的状态转移到目标对象只能通过复制,开销很大。

std::unique_lock rtn_unique_lock()
{
	std::unique_lock tmp(mlock);
	return tmp;
}
void myThread1(int& s) {
	for (int i = 1; i <= 5000; i++)
	{
		std::unique_lock munique = rtn_unique_lock();
		s += i;
	}
}

2.3.4其他方面

2.3.4.1 效率问题

mutex的每次lock和unlock都浪费资源(lock_gurad和unique_lock也是,因为他们是mutex的包装)。

尤其是大量的循环中上锁和解锁就更多了。

比如之前的例子,两个线程求1到n的和,把n设的大一点,比如1,000,000:

#include 
#include 
#include 
#include 
using namespace std;
mutex mtx1;
void myThread1(long long &ans)
{
    for (int i = 1; i <= 500000; )
    {
        std::unique_lock my_unique(mtx1, std::try_to_lock);
        if (my_unique.owns_lock() == true)
        {
            ans += i;
            i++;
        }
    }
}

void myThread2(long long &ans)
{
    for (int i = 500001; i <= 1000000; )
    {
        std::unique_lock my_unique(mtx1, std::try_to_lock);
        if (my_unique.owns_lock() == true)
        {
            ans += i;
            i++;
        }
    }
}
int main()
{
    clock_t startTime,endTime;
    startTime = clock();

    long long ans = 0;
    thread thread1(myThread1, std::ref(ans));
    thread thread2(myThread2, std::ref(ans));
    thread1.join();
    thread2.join();
    cout << ans << endl;

    endTime = clock();
    cout << "The run time is: " << 1000 * (double)(endTime - startTime) / CLOCKS_PER_SEC << "ms" << endl;

    return 0;
}

这样就会有n次lock和n次unlock。

在我的电脑上用了110ms的计算时间。

为了提高效率,就有了std::atomic。

std::atomic

atomic,本意为原子。std::atomic就是原子变量,对原子变量的操作都是原子操作,所以也就不用担心线程间的冲突了。因为:

原子操作是最小的且不可并行化的操作。

这就意味着即使是多线程,也要像同步进行一样同步操作atomic对象,从而省去了mutex上锁、解锁的时间消耗。

用法:

#include 
std::atomic ans;//原子变量,也是全局变量

然后这个原子变量就可以像正常的全局变量一样了,只不过是可各个子线程都可以用原子操作的方式操作它。

T可以是常见的类型,比如int,long long,double等等。

值得注意的是,声明原子变量的时候不能初始化

std::atomic ans = 0;//错误

此时会报错:

use of deleted function 'std::atomic::atomic(const std::atomic&)'

报这个错误的主要原因是原子变量不能使用拷贝构造。

这个限制只在原子变量初始时生效,初始之后时可以在其他地方使用赋值操作符初始化。

示例:

同样的计算任务,使用atomic:

#include 
#include 
#include 
#include 
using namespace std;
std::atomic ans;//只在这声明,不能在这初始化
void myThread1()
{

    for (int i = 1; i <= 500000; i++)
    {
        ans += i;
    }
}

void myThread2()
{
    for (int i = 500001; i <= 1000000; i++)
    {
        ans += i;
    }
}
int main()
{
    clock_t startTime,endTime;
    startTime = clock();

    ans = 0;//在这初始化
    thread thread1(myThread1);
    thread thread2(myThread2);
    thread1.join();
    thread2.join();
    cout << ans << endl;

    endTime = clock();
    cout << "The run time is: " << 1000 * (double)(endTime - startTime) / CLOCKS_PER_SEC << "ms" << endl;

    return 0;
}

此时,同样的任务用时10-20ms。可见,省去了很多上锁、解锁的时间。

2.3.4.2 锁的粒度

lock()锁住的代码段越少,执行越快,整个程序运行效率越高。

锁住的代码多少成为锁的粒度,粒度一般用粗细来描述;

a)锁住的代码少,这个粒度叫细,执行效率高;

b)锁住的代码多,这个粒度叫粗,执行效率低;

选择合适粒度的代码进行保护,粒度太细,可能漏掉共享数据的保护,粒度太粗,影响效率。

2.3.4.3 std::lock 同时锁住多个互斥量

有的时候有多个互斥量需要同时锁定,这个时候用mutex().lock()总是存在先后循序。

C++引入了std::lock()来实现同时锁定多个互斥量(注意最少要锁住两个),如果有锁不是解锁状态,当前线程会阻塞,等待所有的锁都是解锁状态才能继续往下执行。

示例:

#include 
#include 
#include 
#include 

using namespace std;

mutex mtx1;
mutex mtx2;

void myThread1()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));//让thread2先把mtx1上锁
    std::lock(mtx1, mtx2);//thread2把mtx1解锁后,这里才能将mtx1和mtx2同时上锁
    cout << "myThread1: mtx1 and mtx2 all lock." << endl;//所有锁都锁上之后才能执行这一行
    mtx1.unlock();//每个锁单独地显式解锁
    mtx2.unlock();
    cout << "myThread1: mtx1 and mtx2 all unlock." << endl;
}

void myThread2()
{
    mtx1.lock();
    cout << "myThread2: mtx1 lock." << endl;
    std::this_thread::sleep_for(std::chrono::seconds(2));
    mtx1.unlock();
    cout << "myThread2: mtx1 unlock.\n" << endl;
}
int main()
{
    thread thread1(myThread1);
    thread thread2(myThread2);

    thread1.join();
    thread2.join();

    return 0;
}

thread1和thread2同时创建,thread2先把mutex1上锁,然后thread1想同时上锁mutex1和mutex2,就要等thread2把mutex1解锁之后才行。

输出:

myThread2: mtx1 lock.
myThread2: mtx1 unlock.

myThread1: mtx1 and mtx2 all lock.
myThread1: mtx1 and mtx2 all unlock.

注意,使用std::lock()时,每个锁要单独解锁。当然,为了防止上锁后因为出错没能unlock而导致死锁,可以使用lock_guard来改进:

void myThread1()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));//让thread2先把mtx1上锁
    std::lock(mtx1, mtx2);//thread2把mtx1解锁后,这里才能将mtx1和mtx2同时上锁
    cout << "myThread1: mtx1 and mtx2 all lock." << endl;//所有锁都锁上之后才能执行这一行
    //因为std::lock()函数已经执行过lock了,所以lock_guard要带lock_guard参数构造
    lock_guard guard1(mtx1, std::adopt_lock);
    lock_guard guard2(mtx2, std::adopt_lock);
    cout << "myThread1: mtx1 and mtx2 all unlock." << endl;
}
void myThread2()
{
    lock_guard guard(mtx1);
    cout << "myThread2: mtx1 lock." << endl;
    std::this_thread::sleep_for(std::chrono::seconds(2));
    cout << "myThread2: mtx1 unlock.\n" << endl;
}

mutex互斥锁毕竟比较重,对于系统消耗有些大,C++11的thread类库提供了针对简单类型的原子操作类,如std::atomic_int,atomic_long,atomic_bool等,它们值的增减都是基于CAS操作的,既保证了线程安全,效率还非常高

2.3.5 总结

mutex是不安全的,建议使用自动锁lock_guard代替mutex;unique_lock是一个比lock_guard 效率差一点的自动锁。

工作中,一般lock_guard就足够了(推荐使用)。

3. async函数

thread类懂了,看async函数就简单多了。

thread是一个类,而asyn是一个函数,头文件是future

asyn函数创建一个线程,并返回一个future类型的对象。

简要来说,async可以根据情况选择同步执行或创建新线程来异步执行,当然也可以手动选择。对于async的返回值操作也比thread更加方便。

3.1 简单示例

一个简单的示例,开启一个线程计算两个数的和:

#include 
#include 

using namespace std;

int myThread1(int a, int b)
{
    int sum;
    sum = a + b;
    return sum;
}
int main()
{
    int a = 1;
    int b = 2;
    auto ans = async(myThread1, a, b);
    cout << "ans = " << ans.get() << endl;
    return 0;
}

注意这一句是比较懒得写法:

auto ans = async(myThread1, a, b);

是自动判断返回类型了。实际上,简介里有提到过“用std::future类型的变量来接收线程函数运行的结果,并通过get()的方法来获得结果”,所以auto自动判定的类型其实就是future

future ans = async(myThread1, a, b);

后面会讲future

3.2 函数模板

第一种重载版本

template 
future::type>
	async (Fn&& fn, Args&&… args)

作用:根据操作系统决定,线程的执行是同步还是异步,以args为参数执行fn。与thread类一样,传递引用参数需要std::refstd::cref

第二种重载版本

template 
future::type>
	async (launch policy, Fn&& fn, Args&&… args);

作用:根据policy参数决定,线程的执行是同步启动还是异步启动。

至于同步和异步的区别,下文再讲。

这两者,都是将线程运行结果保存在std::future模板类的对象中,用std::future类型的变量来接收线程函数运行的结果并通过get()的方法来获得结果。比如:

future ans = async1(sum, 1, 2);
future ans = async2(launch::async, sum, 1, 2);
future ans = async3(launch::deferred, sum, 1, 2);

先说一下这两个重载版本的区别,在于是否有launch policy这个参数。第一种是没有这个参数,意味着由操作系统自动决定,如果想手动选择,就加上launch policy这个参数。

这个参数是std::launch强枚举类(enum class),也包含在头文件future中,有两个枚举值和一个特殊值:

标识符 作用
枚举值:launch::async 异步启动
枚举值:launch::deferred 同步启动,在调用future::get、future::wait时延迟启动(std::future见后文)
特殊值:launch::async | launch::defereed 同步或异步,根据操作系统而定,相当于第一种重载方式,也就是不加这个参数。

所以也就是两种情形:1)不加参数,由操作系统自动决定;2)需要手动选择,在async和deferred之间选一个。

这样的话,就来到了上面的遗留问题,怎么理解异步和同步?

3.3 异步 vs 同步

std::launch::async:异步启动,启动一个新的线程调用Fn,该函数由新线程异步调用,并且将其返回值与共享状态的访问点同步。也就是说,async()创建了一个线程后,该线程会立刻开始运行,即子线程的运行和主线程是异步的。值得注意的是,子线程的运行结果返回给主线程中的std::future类型的变量,主线程可能会发生阻塞:如果子线程运行的很快,对主线程中的std::future::get()影响不大;但是如果子线程的运行需要一定的时间,那么主线程会阻塞在std::future::get(),直到子线程结束,才能真正get到子线程的计算结果。总之,std::future::get()本质上是通过阻塞主线程等待子线程结束并获取其返回值的,只是有时候子线程的运行时间小于主线程中async()std::future::get()的运行时间,主线程才是不阻塞的。

std::launch::deferred:同步延迟,也可以说延迟启动,在访问共享状态(后面会讲共享状态)时该函数才被调用。对Fn的调用将推迟到返回的std::future的共享状态被访问时(使用std::future的wait或get函数),此时**主线程必然会阻塞,阻塞时间就是子线程的运行时间**。

下面是两个对比示例。

3.3.1 异步启动 示例

主线程用launch::async参数创建启动两个子线程,1秒后get这两个子线程的结果,这两个子线程分别运行2秒和4秒,观察主线程的阻塞情况。

#include 
#include 

using namespace std;

int myThread1()
{
    cout << "myThread1 here" << endl;
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return 1;
}
int myThread2()
{
    cout << "myThread2 here" << endl;
    std::this_thread::sleep_for(std::chrono::seconds(4));
    return 2;
}
int main()
{
    future rt1 = async(launch::async, myThread1);
    future rt2 = async(launch::async, myThread2);

    std::this_thread::sleep_for(std::chrono::seconds(1));

    cout << "rt1 = " << rt1.get() << endl;
    cout << "rt2 = " << rt2.get() << endl;
    return 0;
}

结果:

myThread1 heremyThread2 here


rt1 = 1
rt2 = 2

async()创建两个子线程,这两个线程立刻开始运行,然后主线程阻塞在rt1.get()等待myThread1返回结果::

myThread1 heremyThread2 here


rt1 =

myThread1结束后将结果返回给rt1后,输出get的结果,然后输出“rt2 = ”,因为此时myThread2还没返回,所以阻塞在rt2.get():

myThread1 heremyThread2 here


rt1 = 1
rt2 = 

myThread2结束后,输出get结果:

myThread1 heremyThread2 here


rt1 = 1
rt2 = 2

总结:子线程立刻运行,子线程结束前主线程会阻塞在std::future::get()。

3.3.2 同步/延迟启动 示例

同样的代码,将std::launch参数由async改为deferred。

#include 
#include 

using namespace std;

int myThread1()
{
    cout << "myThread1 here" << endl;
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return 1;
}
int myThread2()
{
    cout << "myThread2 here" << endl;
    std::this_thread::sleep_for(std::chrono::seconds(4));
    return 2;
}
int main()
{

    future rt1 = async(launch::deferred, myThread1);
    future rt2 = async(launch::deferred, myThread2);

    std::this_thread::sleep_for(std::chrono::seconds(1));

    cout << "rt1 = " << rt1.get() << endl;
    cout << "rt2 = " << rt2.get() << endl;
    return 0;
}

结果:

rt1 = myThread1 here
1
rt2 = myThread2 here
2

async()创建两个子线程,这两个线程延迟启动,1秒后然后主线程阻塞在rt1.get(),等待myThread1返回结果,此时myThread1才启动,这时候的输出是:

rt1 = myThread1 here

过了2秒,myThread1结束,主线程获取到rt1的结果后打印,然后阻塞在rt2.get(),此时myThread2才启动,所以输出是:

rt1 = myThread1 here
1
rt2 = myThread2 here

4秒后,myThread2结束,主线程获取到rt2的结果,打印:

rt1 = myThread1 here
1
rt2 = myThread2 here
2

总之,所谓延迟启动,就是定义的子线程不立刻启动,主线程阻塞在std::future::get的时候才启动

上面只用到了std::future::get()获取子线程的结果,这个时候就要细说一下std::future了。

4. 基础 线程同步及数据共享

C++ 标准库当中提供了线程同步及共享的方案:std::future 与 std::promise,它们在**thread类和async函数中都能用**。

future用在asyn,promise+future用在thread。

首先,std::future 负责访问,std::promise 负责存储,同时 promise 是 future 的管理者。

std::future 是一个模板类,它提供了可供访问异步执行结果的一种方式。

std::promise 也是一个模板类,它提供了存储异步执行的值和异常的一种方式。

不好理解,慢慢道来。

future,未来,表示未来一段时间后,就能获取一个值。

promise,承诺,表示承诺要传一个数给自己管理的future对象。

4.1 共享状态

首先什么是共享状态?

  1. 用于**保存线程函数及其参数、返回值以及新线程状态等信息。该对象通常创建在堆上,由std::async、std::promise和std::package_task等提供(Provider),并交由future/shared_future管理**。

  2. Provider将**计算结果写入“共享状态”对象**,而future/shared_future通过get()函数来读取该结果。“共享状态”作为异步结果的传输通道,future可以从中方便地获取线程函数的返回值。

  3. “共享状态”内部保存着一个引用计数,当引用计数为0时会通过delete this来销毁自身

4.2 std::future

std::future 一个实例只能与一个异步线程相关联,多个线程则需要使用 std::shared_future。std::future 的共享状态是由异步操作所使用的、且与其关联的 std::promise 所修改。

std::future 用来访问子线程的运行结果,注意只是访问,存储由promise负责。

4.2.1 数据结构

成员函数 作用
一般:T get()
当类型为引用:R& future::get()
当类型为void:void future::get()
阻塞等待线程结束并获取返回值。若类型为void,则与future::wait()相同。只能调用一次。
share() 返回一个可在多个线程中共享的 std::shared_future 对象。调用该函数之后,该 std::future对象本身已经不和任何共享状态相关联,因此该std::future的状态不再是 valid 的了
void wait() const 等待共享状态就绪。如果共享状态尚未就绪(即提供者尚未设置其值或异常),则该函数将阻塞调用的线程直到就绪。 当共享状态就绪后,则该函数将取消阻塞并返回。但是wait()并不读取共享状态的值或者异常。
template
future_status wait_for(const chrono::duration& rel_time) const;
在规定时间rel_time内阻塞等待调用它的线程到共享值成功返回。
例如fu.wait_for(chrono::seconds(1))
若在这段时间内线程结束则返回future_status::ready
若没结束则返回future_status::timeout
若async是以launch::deferred启动的,则不会阻塞并立即返回future_status::deferred
wait_until() 在指定时间节点内 阻塞等待调用它的线程到共享值成功返回。返回值类型为枚举类future_status
valid() 检查 future 是否处于被使用状态,检查共享状态的有效性,也就是它被首次在首次调用 get() 或 share() 前。由 std::future 默认构造函数创建的 std::future 对象是无效(invalid)的,当然通过 std::future 的 move 赋值后该 std::future 对象也可以变为 valid。一旦调用了std::future::get()函数,再调用此函数将返回false。

future_status强枚举类:

future_status::ready 共享状态已就绪
future_status::timeout 在指定的时间内未就绪
future_status::deferred 共享状态包含了一个延迟函数deferred function

void特化的std::future:std::future的作用并不只有获取返回值,它还可以检测线程是否已结束、阻塞等待,所以对于返回值是void的线程来说,future也同样重要。

template class future; 
template class future;
template<> class future;  

4.2.2 创建std::future

通常由某个 Provider 创建,Provider 就是异步任务的提供者,Provider 在某个线程中设置共享状态的值,与该共享状态相关联的 std::future 对象调用 get(通常在另外一个线程中) 获取该值,如果共享状态的标志不为 ready,则调用 std::future::get 会阻塞当前的调用者,直到 Provider 设置了共享状态的值(此时共享状态的标志变为 ready),std::future::get 返回异步任务的值或异常(如果发生了异常)。

一个有效(valid)的 std::future 对象通常由以下三种 Provider 创建,并和某个共享状态相关联。Provider 可以是函数或者类,他们分别是:std::async,std::promise::get_future,std::packaged_task::get_future。

4.2.2.1 std::async 函数

作为async函数的返回值类型。适合线程函数有返回值的情况,get到的值就是线程函数myThread1的返回值。

int myThread()
{
    return 2;
}
int main()
{
    future fu = async(myThread);
    cout << fu.get();
}
4.2.2.2 std::promise::get_future

get_future 为 promise 类的成员函数,后面会讲。适合线程函数无返回值得情况。

首先声明promise对象,利用这个promise对象的get_future方法返回一个future对象,这个promise对象管理着future对象。此时,两者关联的就是同一个共享状态,一个负责访问,一个负责存储。

然后将promise对象传进线程函数,在子线程中用promise对象set_value方法设置共享状态的值,当子线程结束时,共享状态的标志变为ready,future对象可以通过get方法,获取刚才设置的共享状态的值。

#include 
#include 
#include 

using namespace std;

void myThread(std::promise &po)
{
    po.set_value(1);
}

int main()
{
    std::promise myPromise;
    std::future myFuture(myPromise.get_future());

    std::thread t(myThread, ref(myPromise));
    cout << myFuture.get() << endl;

    t.join();

    return 0;
}

上述是主线程与子线程之间的数据共享。如果future在另一个子线程中get,就可以做到子线程之间的数据共享,例如:

#include 
#include 
#include 

using namespace std;

void myThread1(std::promise &po)
{
    po.set_value(1);
}
void myThread2(std::future &fu)
{
    cout << fu.get() << endl;
}

int main()
{
    std::promise myPromise;
    std::future myFuture(myPromise.get_future());

    std::thread t1(myThread1, ref(myPromise));
    std::thread t2(myThread2, ref(myFuture));


    t1.join();
    t2.join();

    return 0;
}
4.2.2.3 std::packaged_task::get_future

此时 get_future为 packaged_task 的成员函数

4.2.3 示例 std::future::wait_for()

模拟游戏加载过程。

#include 
#include 
using namespace std;

void load()
{
	std::this_thread::sleep_for(std::chrono::seconds(5));
}
int main() {
	future fu = async(load);
	cout << "Loading..." << endl;
	// 每次等待1秒
	while (fu.wait_for(chrono::seconds(1)) != future_status::ready)
    {
        cout << "please wait."<< endl;
    }

	cout << "Finished!" << endl;
	return 0;
}

4.3 std::promise

std::promise 对象可以保存某一类型 T 的值,该值能被 std::future 对象读取(存在是另一个线程的可能),所以std::promise 提供了一种线程同步的手段,像一个用来装数据的容器,不同的线程可以通过这个容器传递数据。

1、std::promise 负责存储,注意 std::promise 应当只使用一次。

2、std::promise不可拷贝,但是可以被引用。

3、std::promise 在作为使用者的异步线程当中,如果没有共享值没有被 set,而异步线程却结束,future 端会抛出异常。

4、std::promise 的 set 操作函数只能被调用一次,get_future() 函数只能被调用一次。

5、std::promise 空类型创建是可以的,任何 set 函数不接受任何形式的参数,此操作用于传递通知,通知与其关联的 std::future 端解除阻塞。

4.3.1 数据结构

名称 作用
operator= 从另一个 std::promise 移动到当前对象。
swap() 交换移动两个 std::promise。
get_future() 获取与它关联的 std::future,当前promise对象管理着这个关联的future对象。
void set_value (const T& val)
void set_value (T&& val)
当类型为引用:void promise::set_value (R& val)
当类型为void:void promise::set_value (void)
设置值,类型由初始化时的模板类型而定。
设置promise的值并将共享状态设为ready(将future_status设为ready)
void特化:只将共享状态设为ready
set_value_at_thread_exit() 设置值,但是到该线程结束时才会发出通知。
set_exception() 设置异常,类型为 exception_ptr。
set_exception_at_thread_exit() 设置异常,但是到该线程结束时才会发出通知。

4.3.2 示例 为什么要有promise

有一个问题:既然有了上面的future可以在线程间共享数据,那为什么还要有promise?

这是因为,创建线程有thread类和asyn函数两种方式。上面提到,asyn函数创建一个线程,并返回一个future类型的对象,asyn创建的线程函数是有返回值的,线程结束后这个返回值就通过asyn函数返回的future对象的get方法访问。例如:

int myThread()
{
    return 2;
}
int main()
{
    future fu = async(myThread);
    cout << fu.get();
}

但是,上面讲过,如果用thread类创建线程,线程函数是没有返回值的。thread竟然不能获取返回值!因为thread::join()的返回值是void类型,所以你不能通过join来获得线程返回值。

thread th(func);
future return_value = th.join();//肯定报错

借鉴一下函数传参的知识,如果向线程函数中传递一个引用,在线程中改变这个引用的值,在线程外或者其他线程就能同步获取这个引用的值,也就变相实现了thread线程结果的返回了。

同时,future的值不能改变,promise的值可以改变;promise存储,future访问;promise管理着对应的future。

所以:创建一个promise对象,并通过它获取一个future对象,将promise对象传给thread创建的线程,在这个线程中对promise的共享状态值进行更改,可以通过future获取到更改的值。

示例,thread1生成一个值,thread2读取这个值:

#include 
#include 
#include 
using namespace std;
void ReturnIntValue(std::promise &po) 
{
    try
    {
        po.set_value_at_thread_exit(3);
    }
    catch (const std::exception&)
    {
        po.set_exception(std::current_exception());
    }
    return;
}

void PrintIntValue(std::future &fu) 
{
    cout << "Value: " << fu.get() << endl;
    return;
}

int main()
{
    std::promise myPromise;
    std::future myFuture(myPromise.get_future());

    std::thread t1(ReturnIntValue, ref(myPromise));
    std::thread t2(PrintIntValue, ref(myFuture));

    t1.join();
    t2.join();

    return 0;
}

额外说一句,promise+future的这种方式是没办法用在asyn函数上的。原因也很简单,上面讲了future的创建方式有三种,如果是通过std::promise::get_future创建了future,那还怎么用asyn函数再创建一次呢?

4.4 总结

方式 thread类 asyn函数
单纯future No Yes
promise+future Yes No

thread:

std::promise myPromise;
std::future myFuture(myPromise.get_future());
std::thread t1(ReturnIntValue, ref(myPromise));
std::thread t2(PrintIntValue, ref(myFuture));

asyn:

future fu = async(myThread);
cout << fu.get();

5. 进阶 线程间的同步通信

5.1 线程同步

主要参考:https://blog.csdn.net/iuices/article/details/123240544,写的非常好。

线程同步:线程间需要按照预定的先后次序顺序进行的行为。

并发有两大需求,一是互斥,二是等待/同步。

互斥是因为线程间存在共享数据,通过互斥锁能搞定,常见的有依赖操作系统的 mutex。

等待/同步则是因为线程间存在依赖,因为数据的原因,线程间的运行顺序有特殊的要求。条件变量,就是为了解决等待需求。

同步是比互斥更严格的关系,互斥只要求线程间访问某一资源时不存在同时处理或者交替处理的可能,而对线程本身的调度顺序没有限制。同步就是在互斥的基础上,虽然线程之间的调度我们没办法控制,但我们可以原子地让某些线程在唤醒时检查某个条件,如果条件不满足就释放锁然后进入阻塞,通过这种方式达到控制不同线程按照某一种你设定的顺序访问资源。

考虑实现生产者消费者队列,生产者和消费者各是一个线程,消费者线程必须等待生产者线程 push 元素进队列。

方式一:轮询模式(poll)。消费者线程一直轮询队列(需要加 mutex),如果是队列里有值,就去消费;如果为空,要么是继续查,要么sleep一下,系统过一会再次唤醒消费者线程进行轮询。带来的问题是高额的CPU开销,同时线程可能过分sleep,影响该线程的性能。

方式二:事件模式(event)。消费者线程发现队列为空,通知操作系统自己要wait并进入休眠,等待生产者线程发信号来唤醒自己。生产者线程push队列之后,则调用signal通知操作系统唤醒消费者线程。事件模式比较通用,性能不会太差(但存在切换上下文的开销)。

没有条件变量,就只能用轮询模式实现;有了条件变量,就可以使用时间模式了,条件就是检查队列是否为空。

5.2 条件变量 condition_variable

条件变量跟 c++11 没特别大关系,它是操作系统实现的(Linux下使用 pthread库中的 pthread_cond_*() 函数提供了与条件变量相关的功能)。

条件变量自身并不包含条件,因为它通常和 if (或者while) 一起用,所以叫条件变量。

C++11中的实现方式是条件变量(condition_variable和condition_variable_any)。条件变量位于头文件condition_variable下。

condition_variable/condition_variable_any类用于阻止一个线程或同时阻止多个线程,直到另一个线程修改共享变量(condition),并通知condition_variable,才会继续执行。

当调用它的wait函数时,它使用一个mutex来锁定线程。使得该线程保持阻塞状态,直到被另一个线程调用同一个condition_variable对象上的notify函数才被唤醒。

条件变量要和锁一起使用,锁提供了互斥这一机制,而条件变量在其基础上提供了同步的机制。

condition_variable必须搭配unique_lock使用,而 std::condition_variable_any可以跟任何其他可锁定对象绑定使用, 也可以使用自定义类型。

5.2.1 数据结构

等待
wait() 阻塞当前线程,直到条件变量被唤醒notify
wait_for() 阻塞当前线程,直到条件变量被唤醒notify,或到达指定时长
wait_until() 阻塞当前线程,直到条件变量被唤醒notify,或到达指定时间点
通知
notify_one() 通知一个等待的线程
notify_all() 通知所有等待的线程

5.2.2 等待

5.2.2.1 wait
// 当前线程的执行会被阻塞,直到收到 notify 为止。
void wait (unique_lock& lck);
// 当前线程仅在pred=false时阻塞;如果pred=true时,不阻塞
// Predicate 谓词函数,可以普通函数或者lambda表达式
template 
void wait (unique_lock& lck, Predicate pred);

wait会阻塞当前线程直至条件变量被通知,或虚假唤醒发生。

调用wait时,该函数会自动调用 lck.unlock() 释放锁,使得其他被阻塞在锁竞争上的线程得以继续执行。然后阻塞当前执行线程,另外,一旦当前线程获得通知(notified,通常是另外某个线程调用 notify_* 唤醒了当前线程),wait()函数再次调用 lck.lock()重新上锁然后wait返回退出,可以理解lck的状态变换和 wait 函数被调用(退出)是同时进行的。可见,wait可依次拆分为三个操作:释放互斥锁、等待在条件变量通知、再次获取互斥锁。

示例:

#include 
#include 
#include 
#include 
using namespace std;

std::mutex mtx;
std::condition_variable cv;

void func1()
{
    cout << "Thread1: start wait.\n";
    unique_lock lck(mtx);
    cv.wait(lck);
    cout << "Thread1: end wait.\n";
}
void func2()
{
    cout << "Thread2: sleep for a while\n";
    std::this_thread::sleep_for(std::chrono::seconds(1));
    unique_lock lck(mtx);
    cv.notify_all();
    cout << "Thread2: notify_all()\n";
}
int main()
{
	std::thread t1(func1);
	std::thread t2(func2);

	t1.join();
	t2.join();

	return 0;
}

输出:

Thread1: start wait.
Thread2: sleep for a while
Thread2: notify_all()
Thread1: end wait.

std::condition_variable提供了两种 wait() 函数。第二种情况多了条件参数 Predicate,只有当 pred 条件为false 时调用 wait() 才会阻塞当前线程,并且在收到其他线程的通知后只有当 pred 为 true 时才会被解除阻塞。因此第二种情况类似以下代码:

while (!pred()) 
{
    wait(lck);
}

Predicate表示校验条件,可以避免被虚假唤醒,暂没找到合适的示例。(TODO)

见下面的虚假唤醒章节。

5.2.2.2 wait_for
template 
cv_status wait_for (unique_lock& lck,
          const chrono::duration& rel_time);
                      
template 
bool wait_for (unique_lock& lck,
          const chrono::duration& rel_time, Predicate pred);

与 std::condition_variable::wait() 类似,不过 wait_for 可以指定一个时间段,在当前线程收到通知或者指定的时间 rel_time 超时之前,该线程都会处于阻塞状态。而一旦超时或者收到了其他线程的通知,wait_for 返回。

另外,wait_for 的重载版本带条件参数 pred 表示 wait_for 的预测条件, pred 为 false 时wai会阻塞当前线程,并且在收到其他线程的通知后只有当 pred 为 true 时才会被解除阻塞。

带条件参数Predicate的wait_for返回值是bool,而不带Predicate的wait_for返回值是枚举类型cv_status:

枚举类型cv_status
std::cv_status::no_timeout wait_for 或者 wait_until 没有超时,即在规定的时间段内线程收到了通知。
std::cv_status::timeout wait_for 或者 wait_until 超时。

示例:

void func1()
{
    cout << "Thread1: start wait.\n";
    unique_lock lck(mtx);
    cv.wait_for(lck, std::chrono::seconds(1));//只等1秒
    cout << "Thread1: end wait.\n";
}
void func2()
{
    cout << "Thread2: sleep for a while\n";
    std::this_thread::sleep_for(std::chrono::seconds(3));//实际睡了3秒
    unique_lock lck(mtx);
    cv.notify_all();
    cout << "Thread2: notify_all()\n";
}

输出:

Thread1: start wait.
Thread2: sleep for a while
Thread1: end wait.  //Thread2还没通知,Thread1已经结束等待,提前退出wait_for了。
Thread2: notify_all()
5.2.2.3 wait_until
template 
cv_status wait_until (unique_lock& lck,
          const chrono::time_point& abs_time);
template 
bool wait_until (unique_lock& lck,
          const chrono::time_point& abs_time,Predicate pred);

和wait_for非常像,等待直到指定时间点,不用多说。

5.2.3 通知

5.2.3.1 notify_one
void std::condition_variable::notify_one(void)

没有参数、没有返回值。

解除阻塞当前正在等待此条件的线程之一。如果没有线程在等待,则还函数不执行任何操作。如果超过一个线程在等,不会指定具体哪一线程。

但是,下面示例实测,好像是谁先wait,就通知谁,而且只通知这一个。

#include 
#include 
#include 
#include 
using namespace std;

std::mutex mtx;
std::condition_variable cv;

void func1(int i)
{
    if (i != 7)
    {
        std::this_thread::sleep_for(std::chrono::seconds(2));
    }

    unique_lock lck(mtx);
    cv.wait(lck);
    cout << "Thread " << i <<" is notified by notify_one.\n";
}
void func2()
{
    std::this_thread::sleep_for(std::chrono::seconds(3));
    unique_lock lck(mtx);
    cv.notify_one();
}
int main()
{
    std::thread threads[10];
    for(int i = 0; i < 10; i++)
    {
        threads[i] = thread(func1, i);
    }
    std::thread t2(func2);

    for(int i = 0; i < 10; i++)
    {
        threads[i].join();
    }
	t2.join();

	return 0;
}
5.2.3.2 notify_all
void std::condition_variable::notify_all(void)

没有参数、没有返回值。唤醒所有的等待(wait)线程。如果当前没有等待线程,则该函数什么也不做。

只通知一次。

改造一下notify_one的示例:

void func1(int i)
{
    unique_lock lck(mtx);
    cv.wait(lck);
    cout << "Thread " << i <<" is notified by notify_all.\n";
}
void func2()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    unique_lock lck(mtx);
    cv.notify_all();
}

5.2.4 虚假唤醒 spurious wakeup

当线程从等待已发出信号的条件变量中醒来,却发现它等待的条件未得到满足时,就会发生虚假唤醒。

1、比较简单的虚假唤醒的原因是wait_for。在正常情况下,wait返回时要么是因为被唤醒,要么是因为超时才返回。超时后返回才发现并不满足条件,就尴尬了,此时可以再检查一次条件,不满足的话继续wait_for。

2、但是在实际中发现,因此操作系统的原因(未展开,我现在也没深入去研究),wait类型在不满足条件时,它也会返回,这就导致了虚假唤醒。

3、此外,虚假唤醒不会无缘无故发生,通常是因为在发出条件变量信号和等待线程最终运行之间,另一个线程运行并更改了条件。大多是因为线程之间存在竞争。在许多系统上,尤其是多处理器系统上,虚假唤醒的问题更加严重,因为如果有多个线程在条件变量发出信号时等待它,系统可能会决定将它们全部唤醒,如果有 10 个线程在等待,那么只有一个会获胜并取走数据,另外 9 个会经历虚假唤醒。

**解决办法:当线程在条件变量上唤醒时,它应该始终检查它所寻求的条件是否得到满足。**如果不是,它应该回到条件变量上睡觉,等待另一个机会,这个时候就要用到while了。

while (!pred()) //while循环,解决了虚假唤醒的问题,每次醒来都检查条件,不满足就继续等待
{
    wait(lock);
}

假设系统不存在虚假唤醒的时,代码形式应该如下:

if (不满足xxx条件) {
    //没有虚假唤醒,wait函数可以一直等待,直到被唤醒或者超时,没有问题。
    //但实际中却存在虚假唤醒,导致假设不成立,wait不会继续等待,跳出if语句,
    //提前执行其他代码,流程异常
    wait();  
}

//其他代码
...

但是,正确的使用方式,使用while语句解决虚假唤醒:

while (!(xxx条件) )
{
    //虚假唤醒发生,由于while循环,再次检查条件是否满足,
    //否则继续等待,解决虚假唤醒
    wait();  
}
//其他代码
....

具体怎么理解呢?举个例子。有2个消费者线程,等着用一个vector中的数据,有1个生产者线程在向这个vector中写数据。考虑一下情景:

(1)消费者线程1在取到数据之后去做处理,之后vec为空;
(2)消费者线程2判断vec为空,此时消费者线程2阻塞在wait上;
(3)生产者线程3开始向vec中里添加一个元素,然后调用notify_all通知所有消费者;
(4)消费者线程1先被唤醒获得锁lock,取走数据,清空了vec;
(5)消费者线程2后被唤醒,此时再去去vec数据的时候,就出错了,因为vec已经被消费者线程1清空了。

#include 
#include 
#include 
#include 
#include 
using namespace std;

std::mutex mtx;
std::condition_variable cv;
vector vec;

void consumer(int i)
{
  std::unique_lock lock(mtx);
  if (vec.empty()) // 此处用的是if条件判断,但这样会有虚假唤醒
  {
      cv.wait(lock);
  }
  int data = vec.back();
  cout << "Consumer " << i << " get data: " << data << endl;
  vec.pop_back();//清空数据
}

void producer()
{
  std::unique_lock lock(mtx);
  vec.push_back(1);//存入数据
  cv.notify_all();
}
int main()
{
    std::thread c1(consumer, 1);
    std::thread c2(consumer, 2);
    std::thread p1(producer);

	c1.join();
	c2.join();
    p1.join();
	return 0;
}

输出:

Consumer 2 get data: 1
Consumer 1 get data: 738230705

显然,后唤醒的线程读到了错误的数据,因为数据已经被先醒来的清空了。这就是虚假唤醒。

解决办法也很简单,把if改成while就行了,每次醒来就检查是否满足条件。

void consumer(int i)
{
  std::unique_lock lock(mtx);
  while (vec.empty()) // if改为while,避免虚假唤醒
  {
      cv.wait(lock);
  }
  int data = vec.back();
  cout << "Consumer " << i << " get data: " << data << endl;
  vec.pop_back();//清空数据
}

5.3 条件变量 condition_variable_any

与 std::condition_variable 类似,只不过 std::condition_variable_any 的 wait 函数可以接受任何 lockable 参数,而 std::condition_variable 只能接受 std::unique_lockstd::mutex 类型的参数,除此以外,和 std::condition_variable 几乎完全一样。

5.4 经典示例:生产者-消费者模型

生产者线程每产生一个数据,消费者线程就消费一个数据,按顺序轮回。

#include 
#include 
#include 
#include 
#include 
using namespace std;

// 定义互斥锁(条件变量需要和互斥锁一起使用)
std::mutex mtx;
// 定义条件变量(用来做线程间的同步通信)
std::condition_variable cv;
// 定义vector容器,作为生产者和消费者共享的容器
std::vector vec;

void consumer()
{
    for(int i = 0; i < 5; i++)
    {
        std::unique_lock lck(mtx);

        while (vec.empty())//消费的条件是vec非空,空的话则wait,释放mtx锁
        {
            cv.wait(lck);
        }

        int data = vec.back();
        cout << "Consumer get data: " << data << endl;
        vec.pop_back();//消费数据
        cv.notify_all();//消费完成,通知生产者生产,然后释放锁mtx
        //std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

void producer()
{
    // 生产者每生产一个,就通知消费者消费一个
    for(int i = 0; i < 5; i++)
    {
        std::unique_lock lck(mtx);

        while (!vec.empty())//生产的条件是vec为空,非空的话则wait,释放mtx锁
		{
			cv.wait(lck);
		}

        vec.push_back(i);//存入数据
        cout << "Producer put data: " << i << endl;
        cv.notify_all();//生产完成,通知消费者消费,释放mtx锁
        //std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }

}
int main()
{
    std::thread c1(consumer);
    std::thread p1(producer);

	c1.join();
    p1.join();
	return 0;
}

输出:

Producer put data: 0
Consumer get data: 0
Producer put data: 1
Consumer get data: 1
Producer put data: 2
Consumer get data: 2
Producer put data: 3
Consumer get data: 3
Producer put data: 4
Consumer get data: 4

6. 线程自我控制 this_thread

上面讲的都是创建、控制线程的方法,其实线程控制自也有控制自己的方法。
头文件中,不仅有std::thread这个类,而且还有一个std::this_thread命名空间,它可以很方便地让线程控制自己。

函数 作用
std::thread::id get_id() noexcept 获取当前线程id
template
void sleep_for( const std::chrono::duration& sleep_duration )
当前线程睡眠一会,等待sleep_duration
void yield() noexcept 暂时放弃线程的执行,将主动权交给其他线程(但主动权还会回来)

6.1 sleep_for

这个函数要实现的目的就是阻塞当前线程,换句话说就是要让当前的线程休眠一段时间(具体时间就是我们传进去的参数),而其他进程不休息。

示例

std::this_thread::sleep_for(std::chrono::miliseconds(50));  //表示让该线程休眠50ms
std::this_thread::sleep_for(std::chrono::seconds(1));  //表示让该线程休眠1s

为什么要用sleep_for?

代码中要发给底层硬件发送一条指令,我们知道硬件的处理速度是有一定时间的,所以为了不影响后续的代码的运行,所以需要做一个线程阻塞,保证其在这段时间内硬件处理完毕。

6.2 sleep_for,sleep与yield函数

6.2.1 yield函数

yield的作用域,参数和sleep_for是一样的,函数原型:

std::this_thread::yield(); 

当前线程放弃执行,操作系统调度另一线程继续执行。将当前线程所抢到的CPU”时间片A”让渡给其他线程(其他线程会争抢”时间片A”,等到其他线程使用完”时间片A”后, 再由操作系统调度, 当前线程再和其他线程一起开始抢CPU时间片。

第一,遇到这个函数会优先让其他线程执行;

第二,其他线程执行完我还是会执行的。

6.2.2 sleep系统函数

系统中还有一个阻塞函数sleep函数,所以我们说一说三者的区别。

1、sleep函数是系统函数,换句话说它不需要c++11支持,只要有编译器就能找到这个函数。

2、sleep函数是进程阻塞函数,换句话说一旦调用这个函数,当前进程当中所有的线程全部阻塞。

6.3 时间相关:chrono时间库

这个库是c++11定义出来的关于处理时间的库。用法:

1、duration_cast(将时间单位进行转换,比如说我们获取到的系统时间可能是毫秒,可以换算成秒)。

2、system_clock::now(获取到当前系统时间,是当前时间与1970年1月1之间的差值,单位是time_point)

3、time_since_porch (也是当前时间与1970年1月1日之间的差值,单位是duration)

4、localtime(间隔)(这个函数可以将当前时间与1970年的差值,转换成包含今天年月日的结构体,不过注意,传进去的一定是秒)

5、strftime函数(这个函数可以将结构体指针转换成包含时间格式的字符串数组)

6、to_time_t(将timepoint时间转换成time_t)

时间函数

首先我们说一下,时间到底有啥用,其实我想了想,其用途无非就是两点,第一获取到当前的年月日时分秒,第二就是计算两段代码相隔的时间。所以我就着重说这两部分。

A.获取到当前时间的年月日

无论是什么函数,他的流程一定是这样的。获取到time_point值,然后获取到tm的值,最后获取到年月日时分秒。

//方法1
auto now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count();  //获取到time_point的值并将其转换成秒
ts = localtime(&now)   //获取到当前的tm值 
int year = ts.tm_year + 1900;
int month = ts.tm_mon + 1;
int day = ts.tm_mday;
int hour = ts.tm_hour;
int minute = ts.tm_min;
int second = ts.tm_sec;
     
//方法2
auto tn = std::chrono::system_clock::now();   // 获取到time_point
time_t now1 = std::chrono::system_clock::to_time_t(tn);  // 获取time_t的值
ts = localtime(&now);   // 获取tm的值
const char *fmt = "%Y-%m-%d %H:%M:%S";
strftime(buf, sizeof(buf), fmt, ts);   // 将其转换成字符串的形式

B.获取到当前程序运行的时间

auto t0 = std::chrono::system_clock::now();
j++;
auto time2 = std::chrono::system_clock::now();
std::cout << std::chrono::duration_cast
                   (std::chrono::system_clock::now() - t0).count()<

7. 参考

https://www.cnblogs.com/songyuchen/p/14038180.html
https://blog.csdn.net/weixin_42193704/article/details/113920419
https://blog.csdn.net/sjc_0910/article/details/118861539
https://blog.csdn.net/Windgs_YF/article/details/109624432
https://blog.csdn.net/Stephen___Qin/article/details/115676691
https://blog.51cto.com/u_15060458/3724286
https://blog.csdn.net/Travelerwz/article/details/103442209
https://blog.csdn.net/iuices/article/details/123601858
https://blog.csdn.net/qq_39354847/article/details/126432944
https://codeleading.com/article/12104028151/
https://blog.csdn.net/xhtchina/article/details/90572762

你可能感兴趣的:(c++,开发语言,多线程编程,并发编程,C++11)