创建线程

  • 也就是创建一个thread对象,创建thread需要给这一个对象传递任务,并且需要传递任务的参数,对象创建完成之后就会开始执行任务了,实例程序如下:
/* 梦开始的地方: Hello Corrency! */
#include<iostream>
#include<thread>

void hello();
int main() {
    std::thread t(hello);
    t.join();
    return 0;
}

void hello()
{
    std::cout << "hello corrency world!" << std::endl;
}

当前环境支持并发线程数量

  • 一般可以使用 std::thread::hardware_concurrency() 可以获取到硬件支持的并发线程数量,是std::thread的静态成员函数
  • 超线程技术: 一个核心上可以运行多个线程,可以并行完成更多任务

线程管理

启动线程

  • 启动新线程, 利用C++线程库启动线程就是构造 std::thread 对象,构造函数的参数是可调用对象以及相应的参数并且提供对应的移动构造函数:
thread t{ callable obj };

这里最好使用初始化列表声明,如果利用小括号声明可能使得语句被识别为函数的声明比如:

thread t{ Task() }
  • 启动线程之后必须在线程对象的声明周期结束之前,也就是thread::~thread调用之前,决定它的执行策略,是join还是detach,但是前提是线程对象是 joinable
  • 执行策略为 detach 的时候注意线程中使用的变量在其所在的函数退出的时候会被销毁造成指针悬空的问题
  • 另外对于线程的异常处理,一定需要保证不要使得线程对象调用两次 join 或者 detach 否则就会报错,正确的处理方法如下:
void f(){
    int n = 0;
    std::thread t{ func{n},10 };
    try{
        // todo.. 一些当前线程可能抛出异常的代码
        f2();
        t.join(); // try 最后一行调用 join()
    }
    catch (...){
        t.join(); // 如果抛出异常,就在 这里调用 join()
    }
}

RAII管理线程

  • RAII(资源获取即初始化): 构造函数申请资源,析构函数释放资源,让对象的生命周期和资源绑定,函数执行结束或者异常抛出的时候C++会自定调用对象的分析构函数,利用RAII管理的一般叫做 XXX_guard 对象,比如thread_guard 的是实现方法如下:
class thread_guard{
    std::thread& m_t;
public:
    explicit thread_guard(std::thread& t) :m_t{ t } {}
    ~thread_guard(){
        std::puts("析构");     // 打印日志 不用在乎
        if (m_t.joinable()) { // 线程对象当前关联了活跃线程
            m_t.join();
        }
    }
    thread_guard(const thread_guard&) = delete;
    thread_guard& operator=(const thread_guard&) = delete;
};
void f(){
    int n = 0;
    std::thread t{ func{n},10 };
    thread_guard g(t);
    f2(); // 可能抛出异常
}

参数的传递

  • 向可调用对象传递参数,这些参数作为:std::thread的构造参数即可,注意这些参数会赋值到新线程的内存空间中,即使函数中的参数是引用,实际依然是复制 ,所以如果需要传递引用可以使用标准库中的 std::refstd::cref ,底层原理如下: https://blog.csdn.net/haokan123456789/article/details/138747950
  • 传递值: 直接把参数放在 std::thread 的构造函数中即可
  • 传递引用: 使用 std::ref 或者 std::cref 即可(注意如果函数需要传递引用,但是直接传递值的情况下,由于参数副本转换为右值表达时进行传递,但是左值表达时没有办法引用右值表达时,所以会产生编译错误)
void f(int, int& a) {
    std::cout << &a << '\n'; 
}

int main() {
    int n = 1;
    std::cout << &n << '\n';
    std::thread t { f, 3, std::ref(n) };
    t.join();
}
  • 利用成员函数指针作为调用对象,需要写成&类名::非静态成员的形式,并且第一个参数需要传递 &类对象(参考 C++ 对象模型) , 甚至可以成员变量当成成员函数使用,可调用函数的类型为: function<类型&(void)>类型(这是std::functionstd::bind) 的内容
  • 传递只可以移动的对象可以使用 std::move
  • 同时创建std::thread对象的函数可以和std::bind一起使用,比如::
std::thread t{ std::bind(&X::task_run,&x,std::ref(n)) };
  • 同时注意如果传入的指针对象,那么一定需要注意如果在 f(参数)调用之前指向的对象已经被销毁了,那么此时就会造成悬空指针,从而出错,比如如下情况:
void f(const std::string&);
void test(){
    char buffer[1024]{};
    //todo.. code
    std::thread t{ f,buffer };
    t.detach();
}

解决方法: 使用join() ,或者提前把buffer 转换为 string 类型,比如string(buffer)

std::this_thread

  • std::this_thread命名空间中包含如下函数:
    • yield: 建议实现重新调度各执行线程
    • get_id: 返回当前线程id(提供一个线程的标识符号)
    • sleep_for: 使当前线程停止执行指定时间
    • sleep_until: 使当前线程执行停止到指定的时间点
  • yield的使用方法如下,使得线程在busy-loop 的情况下交出 CPU :
while(!isDone) {
	std::this_thread::yield();
}

std::thread 转移所有权

  • thread 对象是一种 move-only的对象,只可以使用移动构造函数或者移动运算符号进行资源的转移,同时利用这一个特性,可以把函数返回的thread对象利用起来构建新的thread对象
std::thread f(){
    std::thread t{ [] {} };
    return t;
}

int main(){
    std::thread rt = f();
    rt.join();
}

C++20 的 std::jthread

  • std::jthread中相比于 C++11 中引入的std::thread,只是多了两个功能:

    • RAII管理: 在析构的时候自动调用join()
    • 线程停止功能: 线程的取消/停止
  • 提供了停止线程的方法,主要是提供了两种类型:

    • td::stop_source:这是一个可以发出停止请求的类型。当你调用 stop_source 的 request_stop() 方法时,它会设置内部的停止状态为“已请求停止”。 任何持有与这个 stop_source 关联的 std::stop_token 对象都能检查到这个停止请求。
    • std::stop_token: 这是一个可以检查停止请求的类型。线程内部可以定期检查 stop_token 是否收到了停止请求。 通过调用 stop_token.stop_requested(),线程可以检测到停止状态是否已被设置为“已请求停止”。
  • 同时std::jthread 中提供三个成员函数进行线程停止:

    • get_stop_source:返回与 jthread 对象关联的 std::stop_source,允许从外部请求线程停止。
    • get_stop_token:返回与 jthread 对象停止状态关联的 std::stop_token,允许检查是否有停止请求。
    • request_stop:请求线程停止。