C++并发编程-1. 线程基础

参考:https://llfc.club/articlepage?id=2TayNx5QxbGTaWW5s48vMjtuvCB

1. 简介

主要介绍线程的基本管控,包括线程的发起,等待,异常条件下如何等待以及后台运行等基础操作。

2. 线程的发起

线程发起顾名思义就是启动一个线程,C++11标准统一了线程操作,可以在定义一个线程变量后,该变量启动线程执行回调逻辑。如下即可发起一个线程

在 C++ 中,std::thread 的构造函数用于创建和初始化线程对象。以下是 std::thread 的主要构造函数及其用法示例,结合代码说明如何初始化线程。所有示例都基于 C++11 及以上标准。

std::thread 的构造函数

根据 C++ 标准,std::thread 提供了以下几种构造函数:

  1. 默认构造函数

    std::thread();
    
    • 创建一个空的线程对象,不关联任何可执行函数,线程未启动。
    • 用途:可以稍后通过赋值或移动操作关联线程。
  2. 带函数和参数的构造函数

    template <class Function, class... Args>
    explicit std::thread(Function&& f, Args&&... args);    // 注意这里的参数为右值
    
    • 创建一个线程并立即启动,执行函数 f,并将 args... 作为参数传递。
    • Function 可以是函数指针、函数对象、lambda 表达式或可调用对象。
    • 参数 args 会按值传递(若需传递引用,需用 std::ref)。
  3. 移动构造函数(C++11 起):

    std::thread(std::thread&& other) noexcept;
    
    • 通过移动语义转移线程对象的所有权,other 变为空。
  4. 禁用拷贝构造函数

    std::thread(const std::thread&) = delete;
    
    • std::thread 不可拷贝,只能移动。

初始化线程的示例

以下是通过不同方式使用构造函数初始化 std::thread 的示例代码:

  • 说明t5 的所有权通过移动转移到 t6t5 变为空。

注意事项

  1. 线程生命周期

    • 线程对象销毁前必须调用 join()detach(),否则程序会调用 std::terminate() 终止。
    • join() 等待线程完成,detach() 让线程在后台运行。
  2. 参数传递

    • 参数默认按值拷贝到线程的内部存储。若需修改原对象,需用 std::ref 或指针。
    • 临时对象(如字符串字面量)会自动转换为 std::string(如示例 2)。
  3. 异常安全

    • 如果线程启动后抛出异常,确保在 join()detach() 前捕获,否则程序可能终止。
  4. 性能考虑

    • 线程创建和销毁有开销,考虑使用线程池(如 C++17 的并行算法或第三方库)来管理多线程任务。

使用thread启动一个线程,都会将传入的参数转化为右值存起来


#include <iostream>
#include <thread>
#include <string>

void thread_work1(std::string str)
{
    std::cout << "str is " << str << std::endl;
}

int main()
{
    std::string hellostr = "Hello!";
    // 通过()初始化并启动一个线程
    std::thread t1(thread_work1, hellostr);
    // 等待线程完成
    t1.join();
    return 0;
}

3. 线程的等待

当我们启动一个线程后,线程可能没有立即执行,如果在局部作用域启动了一个线程,或者main函数中,很可能子线程没运行就被回收了,回收时会调用线程的析构函数,执行terminate操作。所以为了防止主线程退出或者局部作用域结束导致子线程被析构的情况,我们可以通过join,让主线程等待子线程启动运行,子线程运行结束后主线程再运行。

4. 仿函数作为参数

当我们用仿函数作为参数传递给线程时,也可以达到启动线程执行某种操作的含义

  • 歧义
#include <iostream>
#include <thread>
#include <string>

class background_task
{
public:
    void operator()(std::string str)
    {
        std::cout << "str is " << str << std::endl;
    }
};
int main()
{
    // error  以前歧义:当作函数对象的声明
    std::thread t2(background_task());
    /*
    因为编译器会将t2当成一个函数对象, 返回一个std::thread类型的值,
    函数的参数为一个函数指针,该函数指针返回值为background_task,
    参数为void。可以理解为如下  类似于:"std::thread (*)(background_task (*)())"
    */
    t2.join();
    return 0;
}

5. lambda表达式

std::thread t4([](std::string  str) {
    std::cout << "str is " << str << std::endl;
},  hellostr);
t4.join();

6. 线程detach

允许采用分离的方式在后台独自运行,C++ concurrent programing书中称其为守护线程。


#include <iostream>
#include <thread>
#include <string>

struct func
{
    int &_i;
    func(int &i) : _i(i) {}
    void operator()()
    {
        for (int i = 0; i < 3; i++)
        {
            _i = i;
            std::cout << "_i is " << _i << std::endl;                 
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }
    }
};
void oops()
{
    int some_local_state = 0;
    func myfunc(some_local_state);
    std::thread functhread(myfunc);
    // 隐患,访问局部变量,局部变量可能会随着}结束而回收或随着主线程退出而回收
    functhread.detach();
}

int main()
{
    // detach 注意事项
    oops();
    // 防止主线程退出过快,需要停顿一下,让子线程跑起来detach
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return 0;
}

上面的程序输出不定:有时候是-0,1有时候是0
在这里插入图片描述

  • detach线程的时候一定要注意,子线程是否用到主线程的局部变量

7. 异常处理

  • 当我们启动一个线程后,如果主线程产生崩溃,会导致子线程也会异常退出,就是调用terminate,如果子线程在进行一些重要的操作比如将充值信息入库等,丢失这些信息是很危险的。所以常用的做法是捕获异常,并且在异常情况下保证子线程稳定运行结束后,主线程抛出异常结束运行。如下面的逻辑
#include <iostream>
#include <thread>
#include <string>

struct func
{
    int &_i;
    func(int &i) : _i(i) {}
    void operator()()
    {
        for (int i = 0; i < 3; i++)
        {
            _i = i;
            std::cout << "_i is " << _i << std::endl;
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }
    }
};
void oops()
{
    int some_local_state = 0;
    func myfunc(some_local_state);
    std::thread functhread(myfunc);
    // 隐患,访问局部变量,局部变量可能会随着}结束而回收或随着主线程退出而回收
    functhread.detach();
}

void catch_exception()
{
    int some_local_state = 0;
    func myfunc(some_local_state);
    std::thread functhread{myfunc};
    try
    {
        // 本线程做一些事情,可能引发崩溃
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    catch (std::exception &e)
    {
        functhread.join();
        throw;
    }
    functhread.join();
}
int main()
{
    // detach 注意事项
    oops();
    // 防止主线程退出过快,需要停顿一下,让子线程跑起来detach
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return 0;
}

但是用这种方式编码,会显得臃肿,可以采用RAII技术,保证线程对象析构的时候等待线程运行结束,回收资源。如果大家还记得我基于asio实现异步服务时,逻辑处理类LogicSystem的析构函数里等待线程退出。那我们写一个简单的线程守卫


#include <iostream>
#include <thread>
#include <string>

struct func
{
    int &_i;
    func(int &i) : _i(i) {}
    void operator()()
    {
        for (int i = 0; i < 3; i++)
        {
            _i = i;
            std::cout << "_i is " << _i << std::endl;
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }
    }
};



class thread_guard
{
private:
    std::thread &_t;

public:
    explicit thread_guard(std::thread &t) : _t(t) {}
    ~thread_guard()
    {
        // join只能调用一次
        if (_t.joinable())
        {
            _t.join();
        }
    }
    thread_guard(thread_guard const &) = delete;
    thread_guard &operator=(thread_guard const &) = delete;
};

void auto_guard()
{
    int some_local_state = 0;
    func my_func(some_local_state);
    std::thread t(my_func);
    1 / 0;
    thread_guard g(t);
    // 本线程做一些事情
    std::cout << "auto guard finished " << std::endl;
}

int main()
{
    auto_guard();
    return 0;
}

在这里插入图片描述

在这里插入图片描述

8. 慎用隐式转换

C++中会有一些隐式转换,比如char* 转换为string等。这些隐式转换在线程的调用上可能会造成崩溃问题


#include <iostream>
#include <thread>
#include <string>

void print_str(int n, const char* str) {
    std::string s(str); // 在线程内部将 const char* 转换为 std::string
    std::cout << "print_str: " << s << " (n=" << n << ")" << std::endl;
}

void danger_oops(int som_param) {
    char buffer[1024];
    sprintf(buffer, "%i", som_param); // 将 som_param 格式化为字符串
    std::thread t(print_str, n, buffer); // 创建线程,传递 n 和 buffer
    t.detach(); // 分离线程
    std::cout << "danger oops finished " << std::endl;
}

当我们定义一个线程变量thread t时,传递给这个线程的参数buffer会被保存到thread的成员变量中。
而在线程对象t内部启动并运行线程时,参数才会被传递给调用函数print_str。
而此时buffer可能随着}运行结束而释放了。
改进的方式很简单,我们将参数传递给thread时显示转换为string就可以了,
这样thread内部保存的是string类型。
在这里插入图片描述


#include <iostream>
#include <thread>
#include <string>

void print_str(int n, std::string s) { // 直接接受 std::string
    std::cout << "print_str: " << s << " (n=" << n << ")" << std::endl;
}

void safe_oops(int som_param) {
    char buffer[1024];
    sprintf(buffer, "%i", som_param);
    std::thread t(print_str, 3, std::string(buffer)); // 显式转换为 std::string
    t.detach();
    std::cout << "safe oops finished " << std::endl;
}

int main() {
    safe_oops(42);
    return 0;
}

在这里插入图片描述

9. 引用参数

·当线程要调用的回调函数参数为引用类型时,需要将参数显示转化为引用对象传递给线程的构造函数,
如果采用如下调用会编译失败


#include <iostream>
#include <thread>
#include <string>

void change_param(int &param)
{
    param++;
}
void ref_oops(int some_param)
{
    std::cout << "before change , param is " << some_param << std::endl;
    // 需使用引用显示转换
    std::thread t2(change_param, some_param);
    t2.join();
    std::cout << "after change , param is " << some_param << std::endl;
}

int main()
{
    ref_oops(2);
    return 0;
}

在这里插入图片描述
改为如下调用就可以了


void ref_oops(int some_param)
{
    std::cout << "before change , param is " << some_param << std::endl;
    // 需使用引用显示转换
    std::thread t2(change_param, std::ref(some_param));
    t2.join();
    std::cout << "after change , param is " << some_param << std::endl;
}

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

10. thread原理

11. 绑定类成员函数

在这里插入图片描述

12. 使用move操作

在这里插入图片描述

13. 总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值