菜鸟笔记
提升您的技术认知

C/C++编程:std::thread 详解

构造线程

以一个最经典的hellow world作为开始

 #include <iostream>
 int main()
 {
  
	std::cout << "Hello World\n";
}

这是一个单线程,会将“Hello World”写进标准输出流。我们新启动一个线程来显示这个信息

函数传参作为参数

#include <iostream>
#include <thread>

void do_some_work()
{
  
    std::cout<<"Hello Concurrent World\n";
}

int main()
{
  
    std::thread t(do_some_work);
    t.join();
}
  • std::thread 在 <thread> 头文件中声明,因此使用 std::thread 时需要包含 <thread> 头文件。
  • 每个线程都必须具有一个入口函数,当线程执行完入口函数后,线程也会退出
    • main线程叫做主线程(每个线程都一定会有一个主线程,只有主线程的叫做单线程程序),其入口就是main()函数
    • 其他线程叫做子线程(如果有子线程,那么就是多线程程序,它至少会有两个线程:主线程+一个子线程。这里子线程是由std::thread创建的),其入口函数是hello
  • 当前
    • 在程序启动之后,主线程也就启动了
    • 子线程在std::thread对象创建时启动。

成员函数转为参数

#include <iostream>
#include <thread>

class X
{
  
public:
    void do_work()
    {
  
        std::cout << "Hello World!" << std::endl;
    }
};

int main(int argc, char const *argv[])
{
  

    X my_x;
    std::thread t(&X::do_work, &my_x);
    t.join();
    return 0;
}

还可以传递参数:

#include <iostream>
#include <thread>

class X
{
  
public:
    void do_work(int x)
    {
  
        std::cout << "Hello World!" << x << std::endl;
    }
};




int main(int argc, char const *argv[])
{
  

    X  my_x;
    int num(10);
    std::thread t(&X::do_work, &my_x, num);
    t.join();
    return 0;
}

如果参数是引用:


void f2(int& n)
{
  
    for (int i = 0; i < 5; ++i) {
  
        std::cout << "Thread 2 executing\n";
        ++n;
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}


   std::thread t3(f2, std::ref(n)); // pass by reference

注意,传递的参数只能移动,不可以拷贝。

可调用类型作为参数

std::thread的入参除了是函数外,还可以是可调用类型(即带有函数调用符类型的实例)

  • 此时,函数对象会被复制到新线程的存储空间中,函数对象的执行和调用都在线程的内存空间中进行。
  • 函数对象的副本应该和原始函数对象一致,否则结果会超出预期。
  • 注意,如果你传递的是一个临时变量,那么它将会被解析为函数声明,而不是类型对象的定义。这里相当与声明了一个名为my_thread的函数,这个函数带有一个参数(函数指针指向没有参数并返回background_task对象的函数),返回一个std::thread对象的函数,而非启动了一个线程
  • 使用在前面命名函数对象的方式,或使用多组括号,或使用新统一的初始化语法,或者lambda表达式可以避免这个问题


是否需要等待线程完成

等待线程完成

如果需要阻塞等待线程完成,那么需要调用join。比如mythread.join()

join()会在线程完成后,清理线程相关的存储部分,因此:

  1. 只能对一个未销毁的线程进行join
  2. 每个线程只能执行一次join
  3. 如果在调用join之前,其入口函数就已经退出或者抛出异常,那么压根就不会执行这个join
  4. 因此,如果入口函数可能会抛出异常,那么建议用RAII的方式来保障资源全部被销毁
#include <thread>

class thread_guard
{
  
    std::thread& t;
public:
    explicit thread_guard(std::thread& t_):
        t(t_)
    {
  }
    ~thread_guard()
    {
  
        if(t.joinable())
        {
  
            t.join();
        }
    }
    thread_guard(thread_guard const&)=delete;
    thread_guard& operator=(thread_guard const&)=delete;
};

不等待线程完成

如果不想等待线程结束(因为需要它们运行在后台),可以分离_(_detaching)线程,从而避免异常安全(exception-safety)问题。不过,这就打破了线程与std::thread对象的对象的联系,即使线程仍然在后台运行着,分离操作也能确保std::terminate()在std::thread对象销毁才被调用。

  • 使用detach()会让线程在后台运行,这就意味着主线程不能与之直接交互。也就是说,不会等待这个线程结束;如果线程分离,那么就不可能会有std::thread对象能够引用它,分离线程的确在后台运行,所以分离线程不能被加入。不过c++运行库保障,当线程退出时,相关资源能够正确回收,后台线程的归属和控制c++运行库都会处理。
  • 通常称分离线程为守护线程(发后既忘的任务可以被设置为分离线程)
  • 当detach分离了一个线程,那么之后相应的std::thread对象就与实际执行的线程无关了,并且这个线程也无法join

其他

判断线程是否能够join,可以用joinable

#include <iostream>
#include <thread>
#include <chrono>
using namespace std::chrono_literals;
 
void foo()
{
  
    std::this_thread::sleep_for(500ms);
}
 
int main()
{
  
    std::cout << std::boolalpha;
 
    std::thread t;
    std::cout << "before starting, joinable: " << t.joinable() << '\n';
 
    t = std::thread{
  foo};
    std::cout << "after starting, joinable: " << t.joinable() << '\n';
 
    t.join();
    std::cout << "after joining, joinable: " << t.joinable() << '\n';
 
    t = std::thread{
  foo};
    t.detach();
    std::cout << "after detaching, joinable: " << t.joinable() << '\n';
    std::this_thread::sleep_for(1500ms);
}

线程的移动语义

转移线程所有权

std::thread的语义类型std::unique_ptr,可以移动但是不可以拷贝。

  • 虽然,std::thread实例不像std::unique_ptr那样能占有一个动态对象的所有权,但是它能够占有其他资源:每个实例都负责管理一个执行线程
  • 执行线程的所有权可以在多个std::thread实例中互相转移,这依赖了std::thread可以移动但是不可以复制的特性。
    • 不可复制保证了在同一时间点,一个std::thread实例只能关联一个执行线程
    • 可移动性使得开发者可以自己决定,哪个实例拥有实际执行线程的所有权
  • 首先,新线程开始与t1相关联1。
  • 当显式使用创建t2后2,t1的所有权就转移给了t2。之后,t1和执行std::move()行线程已经没有关联了,执行some_function的函数线程与t2关联。
  • 然后,一个临时std::thread对象相关的线程启动了3。为什么不显式调用
    std::move()转移所有权呢?因为,所有者是一个临时对象——移动操作将会隐式的调用。
  • t3使用默认构造方式创建4,与任何执行线程都没有关联。
  • 最后一个移动操作,将some_function线程的所有权转移6给t1。不过,t1已经有了一个关联的线程(执行some_other_function的线程),所以这里系统直接调用std::terminate()终止程序运行

除了上面这种方式,线程的所有权还可以在函数外进行转移。比如:

std::thread f()
{
  
    void some_function();
    return std::thread(some_function);
}
std::thread g()
{
  
    void some_other_function(int);
    std::thread t(some_other_function,42);
    return t;
}

int test_thread()
{
  
    std::thread t1=f();
    t1.join();
    std::thread t2=g();
    t2.join();
}

std::thread可以返回,同样的,std::thread也可以作为参数

我们也可以将std::thread放入std::vector中,将这些线程当做一个组:

#include <vector>
#include <thread>
#include <algorithm>
#include <functional>

void do_work(unsigned id)
{
  }

void f()
{
  
    std::vector<std::thread> threads;
    for(unsigned i=0;i<20;++i)
    {
  
        threads.push_back(std::thread(do_work,i));
    }
    std::for_each(threads.begin(),threads.end(),
        std::mem_fn(&std::thread::join));
}

int main()
{
  
    f();
}
#include <stdio.h>
#include <stdlib.h>

#include <chrono>    // std::chrono::seconds
#include <iostream>  // std::cout
#include <thread>    // std::thread, std::this_thread::sleep_for

void thread_task(int n) {
  
    std::this_thread::sleep_for(std::chrono::seconds(n));
    std::cout << "hello thread "
              << std::this_thread::get_id()
              << " paused " << n << " seconds" << std::endl;
}

/*
 * ===  FUNCTION  =========================================================
 *         Name:  main
 *  Description:  program entry routine.
 * ========================================================================
 */
int main(int argc, const char *argv[])
{
  
    std::thread threads[5];
    std::cout << "Spawning 5 threads...\n";
    for (int i = 0; i < 5; i++) {
  
        threads[i] = std::thread(thread_task, i + 1);
    }
    std::cout << "Done spawning threads! Now wait for them to join\n";
    for (auto& t: threads) {
  
        t.join();
    }
    std::cout << "All threads joined.\n";

    return EXIT_SUCCESS;
}  /* ----------  end of function main  ---------- */

线程id

std::thread可以看成是线程的一个容器,一个线程同时只能放在一个容器中。问题是如何知道这个容器中装了那个线程呢?有两种方法:

  • 第一种,可以通过调用std::thread对象的成员函数get_id()来直接获取。如果std::thread没有和任何执行线程管理,get_id()将返回std::thread::type默认构造值,这个值表示“无线程”。
  • 第二种,当前线程中调用std::this_thread::get_id()也可以获得线程标识。

线程标识类型为std::thread::idstd::thread::id对象可以自由的拷贝和对比

  • 如果两个对象的std::thread::id相等,那它们就是同一个线程,或者都“无线程”。
  • 如果不等,那么就代表了两个不同线程,或者一个有线程,另一没有线程.

std::thread::id使用场景:

  • 常用作检测线程是否需要进行一些操作
  • 比如:当用线程来分割一项工作主线程可能要做一些与其他线程不同的工作。这种情况下,启动其他线程前,它可以将自己的线程ID通std::this_thread::get_id()得到,并进行存储。每个线程都要检查一下,其拥有的线程ID是否与初始线程的ID相同

运行时决定线程数量

  • std::thread::hardware_concurrency()将会返回当前机器上最多能够跑的线程数量。(注意,如果不能够获取系统信息,那么将会返回0)
  • 一般我们不要让并发线程数量超过std::thread::hardware_concurrency(),因为上下文频繁的切换会降低线程的性能。

小结

std::thread用于创建一个执行的线程实例,它是一切并发变成的基础,使用时需要包含<thread>头文件,它提供了很多基本的线程操作:

成员函数名 作用
join 阻塞等待到该线程结束。
detach 将线程从父进程分离,无法再通过 thread 对象对其进行操作,生命周期也脱离父进程,最终由操作系统进行资源回收。
joinable 检查线程是否可被阻塞等待。
get_id 获取该线程的唯一标识符。
swap 与指定 thread 对象进行互换操作。
native_handle 获取该线程的句柄。
hardware_concurrency [static] 返回逻辑处理器数量。