Bootstrap

C++11:thread详细总结

  1. 线程之间是相互运行的,不是说谁先创建,就先一直执行谁。要看CPU的调度机制。

  2. 线程入口函数可以使用仿函数(函数对象,函数符)。

    1. 我们创建线程的数量受CPU的影响,如果超出CPU核心数所能搭载线程数的最大值,则会有许多转换操作,导致CPU性能下降。C++11 thread提供了一个公有静态函数得到此电脑能搭载的线程的最大值。[详解]((135条消息) 多线程线程数设置多少合适_OrderDong的博客-CSDN博客_线程池线程数量设置多少最好)

      cout << thread::hardware_concurrency() << endl;
      
    2. 线程函数的8种方式
      在这里插入图片描述

    ref、move

    1. 线程之间默认是不会数据分享的,即是我们在线程入口函数

      采用引用传值的方式,也不会影响父线程的数据(也就是说传递的值还是一个副本),有的编译器会直接编译不通过。我们可以用ref来解决:
      在这里插入图片描述

    2. 但是线程间引用传递会导致数据竞争的问题,所以有了move,将父线程的数据转移到子线程中。这样能避免按值传递效率上的问题,也能避免数据竞争。但是使用move移动后,不能再使用数据(常用于临时对象),并且线程函数不用声明引用类型(move使用了引用折叠)。注:如果所传对象只有移动构造没有拷贝构造,则不用显示使用move。[move详细介绍]((134条消息) c++ 之 std::move 原理实现与用法总结_ppipp1109的博客-CSDN博客_std::move)

在这里插入图片描述

join,detach

  1. 一个线程一旦被detach或者join后一次后都将变为non-joinable。
  2. join 会阻塞调用该函数的线程。
  3. join和detach都可以将函数对象安全销毁。

lock_guard,unique_lock

  1. lock_guard

    在上锁期间,如果当中的操作有异常发生,会导致死锁。避免这类问题的发生,我们可以用:

    std::mutex mymutex;
    std::lock_guard< std::mutex > guard(mymutex);

    将锁和guard绑定(并且同时执行上锁操作),锁的所有权交给了guard。那么在当前作用域完结时,guard对象销毁时,_mutex 也会解锁(在guaed析构时,解锁),这就避免了死锁。

    注:如果在绑定之前已经上锁,可以在构造guard时填上第二个参数:std::adopt_lock,表示绑定的锁已经上锁不再执行上锁操作。 lock_guard不能被复制和移动

    弊端:guard没有提供解锁的方法并且当前mutex已经没有了所有权,意思就是上锁后,锁的周期会一直持续到 guard对象被销毁。详解

  2. unique_lock

    因为 lock_guard不够灵活,所以有提供了 uniyque_lock。它管理锁的两种状态(已上锁和已解锁),其不仅具有绑定锁的特性(获得锁的所有权,对象析构同时解锁),同时提供了许多锁的对外方法。

    std::mutex mymutex;

    std::unique_lock< std::mutex > loc(mymutx,defer_lock) ///第二个可选参数表示推迟上锁(默认是上锁,同时也有adopt_lock表示已上锁)

    我们可再去执行 上锁解锁的等操作:
    在这里插入图片描述

    注:unique_lock 不能赋值可以移动

    弊端: 相比lock_guard 会消耗更多的计算机性能。[详解](unique_lock - C++ Reference (cplusplus.com))

once_flag

在多线程的并发执行中,我们可能某些操作想要只执行一次。比如说读取文件的多线程中,打开文件只需要执行一次,但是CPU调度机制我们不知道哪个线程先执行,所以便有了 once_flag 这个类,和call_once函数配和使用。

它是一个不可复制、**不可移动、**只有默认可构造的类。

std::once_flag m_flag;

std::call_once(m_flag,回调函数,回调参数);//回调函数如果有返回值将忽略。

回调函数只会执行一次,即使它被多个线程调用。

条件变量

  1. 构造一个 条件变量对象: std::condition_variable cond;

  2. 阻塞等待条件变量:

    我们在阻塞等待之前要上锁,阻塞等待的时候会解锁,当被唤醒时,又重新上锁:

    cond.wait(锁);

    拓展:假如当前线程等待一个队列不为空,当其他线程唤醒时,队列是为空的话,此时就是假唤醒。解决班法:加上判断

    cond.wait(锁,[] ( ){return !q.empty();})

  3. 唤醒一个线程:cond.notify_one();

  4. 唤醒全部线程:cond.notify_all();

线程异步编程(future,async,promise)

  1. 主线程获取子线程的值

我们可以使用引用传值,但是这样就需要锁和条件变量,这样程序会变得繁琐,并且性能低下,C++11提供了另一种方法:

在这里插入图片描述

这里主线程得到了 x=24 (4的阶乘)

**future<>**类模板:

​ 从未来获得某个变量的模板类,类模板 std::future 提供同步访问异步操作结果的机制get函数只能调用一次(并且阻塞等待),否则程序会崩溃。不能拷贝构造有移动构造。

​ 类模板 std::future 提供访问异步操作结果的机制:

  • (通过 std::asyncstd::packaged_taskstd::promise 创建的)异步操作能提供一个 std::future 对象给该异步操作的创建者。

  • 然后,异步操作的创建者能用各种方法查询、等待或从 std::future 提取值。若异步操作仍未提供值,则这些方法可能阻塞。

  • 异步操作准备好发送结果给创建者时,它能通过修改链接到创建者的 std::future共享状态(例如 std::promise::set_value )进行。

    我们可以通过future_status去查询future的三种状态,分别是deferred(还未执行),ready(已经完成),timeout(执行超时),所以我们可以通过这个去查询异步操作的状态。

    成员函数
    (构造函数)构造 future 对象 (公开成员函数)
    (析构函数)析构 future 对象 (公开成员函数)
    operator=移动future对象 (公开成员函数)
    share*this 转移共享状态给 shared_future 并返回它 (公开成员函数)
    获取结果
    get返回结果 (公开成员函数)
    状态
    valid检查 future 是否拥有共享状态 (公开成员函数)
    wait等待结果变得可用 (公开成员函数)
    wait_for等待结果,如果在指定的超时间隔后仍然无法得到结果,则返回。 (公开成员函数)
    wait_until等待结果,如果在已经到达指定的时间点时仍然无法得到结果,则返回。 (公开成员函数)

注意, std::future 所引用的共享状态不与另一异步返回对象共享(与 std::shared_future 相反)。

future详解

shared_future<> 类模板:

​ 如果有需求我们要在多个线程中共享一个共享状态,但是因为future.get函数只能调用一次并且 future只能移动构造,所以future是行不通的。

​ 类模板 std::shared_future 提供访问异步操作结果的机制,类似 std::future ,除了允许多个线程等候同一共享状态。不同于仅可移动的 std::future (故只有一个实例能指代任何特定的异步结果),std::shared_future 可复制而且多个 shared_future 对象能指代同一共享状态。

​ 若每个线程通过其自身的 shared_future 对象副本访问,则从多个线程访问同一共享状态是安全的。

​ 可用于同时向多个线程发信,类似 std::condition_variable::notify_all()

因为 该类模板提供了 普通拷贝构造,所以传值可以不用引用传值。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J7YCPFwP-1650597592639)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\1650538942707.png)]

async函数模板:

​ 会自动创建一个异步任务去调用线程函数,它返回一个std::future,这个future中存储了线程函 数返回的结果,当我们需要线程函数的结果时,直接从future中获取,非常方便。

std::async有两个版本:

​ 1.无需显示指定启动策略,自动选择,因此启动策略是不确定的,可能是std::launch::async,也可

​ 能是std::launch::deferred,或者是两者的任意组合,取决于它们的系统和特定库实现。

​ 2.允许调用者选择特定的启动策略。

​ std::async的启动策略类型是个枚举类enum class launch,包括:

​ 1.std::launch::async:异步,立即启动一个新的线程调用Fn,该函数由新线程异步调用,并且将其返 回值与共享状态的访问点同步。

​ 2.std::launch::deferred:延迟,在访问共享状态时该函数才被调用。对Fn的调用将推迟到返回的

std::future的共享状态被访问时(使用std::future的wait或get函数)

参数Fn:可以为函数指针、成员指针、任何类型的可移动构造的函数对象(即类定义了operator()的对象)。 Fn的返回值或异常存储在共享状态中以供异步的std::future对象检索。

参数Args:传递给Fn调用的参数,它们的类型应是可移动构造的

​ **返回值:**当Fn执行结束时,共享状态的std::future对象准备就绪。std::future的成员函数get检索的值 是Fn返回的值。当启动策略采用std::launch::async时,即使从不访问其共享状态,返回std::future 也会链接到被创建线程的末尾。在这种情况下,std::future的析构函数与Fn的返回同步。
​ 摘自:https://blog.csdn.net/fengbingchun/article/details/104133494

async详解

  1. 子线程获取主线程的值。
    在这里插入图片描述

    promise< T >类模板:

    ​ std::promise是个模板类。一个promise对象可以存储由future对象(可能在另一个线程中)检索的T类型的值或派生自std::exception的异常,并提供一个同步点。不可拷贝构造可以移动构造。

    ​ 在构造std::promise对象时,该对象与新的共享状态(shared state)关联。通过调用std::promise的get_future函数,可以将该共享状态与std::future对象关联。调用之后,两个对象共享相同的共享状态:(1).std::promise对象是异步提供程序(asynchronous provider,async同样是提供异步程序,但是其共享状态是 线程函数的返回值),应在某个时刻为共享状态设置一个值。(2).std::future对象是个异步返回对象,可以检索共享状态的值,并在必要时等待其准备就绪(调用get())

    成员函数
    (构造函数)构造std::promise对象 (公开成员函数)
    (析构函数)析构std::promise对象 (公开成员函数)
    operator=赋值共享状态 (公开成员函数)
    swap交换二个 promise 对象 (公开成员函数)
    获取结果
    get_future返回与承诺的结果关联的 future (公开成员函数)
    设置结果
    set_value设置结果为指定值 (公开成员函数)
    set_value_at_thread_exit设置结果为指定值,同时仅在线程退出时分发提醒 (公开成员函数)
    set_exception设置结果为指示异常 (公开成员函数)
    set_exception_at_thread_exit设置结果为指示异常,同时仅在线程退出时分发提醒 (公开成员函数)
    非成员函数
    std::swap(std::promise)(C++11)特化 std::swap 算法 (函数模板)
    辅助类
    std::uses_allocator(C++11)特化 std::uses_allocator 类型特征 (类模板特化)

    set_value_at_thread_exit()当在这个线程执行结束的时候才会将future的状态设置为ready,而set_value()则直接将future的状态设置为ready。需要注意的是在使用的过程中不能多次set_value(),也不能多次get_future()和多次get(),因为一个promise对象只能和一个对象相关联,否则就会抛出异常。

    共享状态的生存期至少要持续到与之关联的最后一个对象释放或销毁为止。

    ​ [promise详解](std::promise - cppreference.com)

packaged_task<>类模板

类模板 std::packaged_task 包装任何可调用 (Callable) 目标(函数、 lambda 表达式、 bind 表达式或其他函数对象),使得能异步调用它。其返回值或所抛异常被存储于能通过 std::future 对象访问的共享状态中。

正如 std::functionstd::packaged_task 是多态、具分配器的容器:可在堆上或以提供的分配器分配存储的可调用对象。

用法

std::packaged_task<函数返回类型(参数类型)> 变量名(函数名)

int fun(int x) {
x++;
std::cout << std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(5));
return x;
}

int main()
{
std::packaged_task<int(int)> pt(fun); // 将函数打包起来
std::future fu = pt.get_future(); // 并将结果返回给future
std::thread t(std::ref(pt), 1);
std::cout << fu.get() << std::endl;
std::cout << std::this_thread::get_id() << std::endl;
t.join();
return 0;
}

packaged_task详解

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;