[C++并发编程] 线程基础

线程发起

最简单的发起一个线程。

cpp 复制代码
void thread_work(std::string str) {
    std::cout << "str: " << std << std::endl; 
}
//初始化并启动一个线程
std::thread t1(thread, wangzn2016);

线程等待:

线程发起后,可能新的线程还没立即执行,或者还没执行完成,主线程就执行完了,此时子线程就会被回收,回收时会调用线程的析构函数,执行terminate操作。

因此,为了防止主线程退出或者局部作用域结束导致子线程被回收析构,我们可以通过join的方式,让主线程等待子线程执行完成并回收。

cpp 复制代码
void thread_work(std::string str) {
    std::cout << "str: " << std << std::endl; 
}
//初始化并启动一个线程
std::thread t1(thread, wangzn2016);
//主线程等待子线程执行完成
t1.join();

仿函数作为参数

也可以使用仿函数作为参数发起线程

cpp 复制代码
class background_task {
public:
    void operator()() {
        std::cout << "wangzn2016" << str << std::endl;
    }
};
//这种方式会被编译器识别成函数"std::stread (*)(background_task) (*)()"
//std::thread t1(background_task());

//其中一个解决办法就是,实例化一个对象后,再传入
//这里提供另外两种种解决办法
// 1.多加一层()
std::thread t2((background_task()));
t2.join();

// 2.使用{}来初始化
std::thread t3{ background_task() };
t3.join();

Lambda表达式

lambda表达式也可以作为线程的参数传递给thread。

cpp 复制代码
std::thread t1([](std::string  str) {
    std::cout << "str: " << str << std::endl;
},  hellostr);

t1.join();

线程detach

线程分离,也可以被称为守护线程。线程允许采用分离的方式在后台独自运行。

cpp 复制代码
struct func {
    int& _i;
    func(int& i): _i(i){}
    void operator()() {
        for (int i = 0; i < 3; i++) {
            _i = i;
            std::cout << "_i: " << _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();    
}

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

上面的例子存在隐患,因为some_local_state是局部变量, 当oops调用结束后局部变量some_local_state就可能被释放了,而线程还在detach后台运行,容易出现崩溃。

解决办法:

  • 通过智能指针传递参数,因为引用计数会随着赋值增加,可保证局部变量在使用期间不被释放,这也就是我们之前提到的伪闭包策略。
  • 将局部变量的值作为参数传递,这么做需要局部变量有拷贝复制的功能,而且拷贝耗费空间和效率。
  • 将线程运行的方式修改为join,这样能保证局部变量被释放前线程已经运行结束。但是这么做可能会影响运行逻辑。 比如下面的修改
cpp 复制代码
void use_join() 
{ 
    int some_local_state = 0; 
    func myfunc(some_local_state); 
    std::thread functhread(myfunc); 
    functhread.join(); 
}      

异常处理

cpp 复制代码
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();
}

但是用这种方式编码,会显得臃肿,可以采用RAII技术,保证线程对象析构的时候等待线程运行结束,回收资源。

cpp 复制代码
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);
    thread_guard g(t);
    //本线程做一些事情
    std::cout << "auto guard finished " << std::endl;
}

auto_guard();

慎用隐式类型转换

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

cpp 复制代码
void print_str(int i, std::string str)
{
    std::cout << "i: " << i << " std: " << str << std::endl;
}
void danger_oops(int som_param) {
    char buffer[1024];
    sprintf(buffer, "%i", som_param);
    //在线程内部将char const* 转化为std::string
    
    std::thread t(print_str, 3, buffer);
    t.detach();
    std::cout << "danger oops finished " << std::endl;
}

我们定义一个线程变量thread t时,传递给这个线程的参数buffer会被保存到thread的成员变量中。 而在线程对象t内部启动并运行线程时,参数才会被传递给调用函数print_str。 而此时buffer可能随着}运行结束而释放了。

解决的方式很简单,我们将参数传递给thread时显示转换为string就可以了, 这样thread内部保存的是string类型。

cpp 复制代码
void safe_oops(int some_param) {
    char buffer[1024];
    sprintf(buffer, "%i", some_param);
    std::thread t(print_str, 3, std::string(buffer));
    t.detach();
}

引用参数

当我们创建的线程要调用的回调函数的参数为引用类型的时候,需要将参数显示转化为引用对象传递给线程的构造函数。

cpp 复制代码
void change_param(int& param) {
    param++;
}

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

因为线程的启动在底层是模板实现,并且嵌套多个函数,引用类型在传参的时候涉及到引用降级(引用折叠),最后传递给change_param函数的参数是个右值,因此会报错,所以我们需要使用引用显示转换, std::ref()来传参。

thread原理

cpp 复制代码
template <class _Fn, class... _Args, enable_if_t<!is_same_v<_Remove_cvref_t<_Fn>, thread>, int> = 0>
_NODISCARD_CTOR explicit thread(_Fn&& _Fx, _Args&&... _Ax) {
    _Start(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);
}

thread构造函数内部通过forward原样转换传递给_Start函数。关于原样转换的知识可以看我之前写的文章。 _Start 函数内部就是启动了一个线程_beginthreadex执行回调函数。

cpp 复制代码
template <class _Fn, class... _Args>
    void _Start(_Fn&& _Fx, _Args&&... _Ax) {
        using _Tuple                 = tuple<decay_t<_Fn>, decay_t<_Args>...>;
        auto _Decay_copied           = _STD make_unique<_Tuple>(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);
        constexpr auto _Invoker_proc = _Get_invoke<_Tuple>(make_index_sequence<1 + sizeof...(_Args)>{});

#pragma warning(push)
#pragma warning(disable : 5039) // pointer or reference to potentially throwing function passed to
                                // extern C function under -EHc. Undefined behavior may occur
                                // if this function throws an exception. (/Wall)
        _Thr._Hnd =
            reinterpret_cast<void*>(_CSTD _beginthreadex(nullptr, 0, _Invoker_proc, _Decay_copied.get(), 0, &_Thr._Id));
#pragma warning(pop)

        if (_Thr._Hnd) { // ownership transferred to the thread
            (void) _Decay_copied.release();
        } else { // failed to start thread
            _Thr._Id = 0;
            _Throw_Cpp_error(_RESOURCE_UNAVAILABLE_TRY_AGAIN);
        }
    }
相关推荐
高山我梦口香糖35 分钟前
[react]searchParams转普通对象
开发语言·前端·javascript
冷眼看人间恩怨1 小时前
【Qt笔记】QDockWidget控件详解
c++·笔记·qt·qdockwidget
信号处理学渣1 小时前
matlab画图,选择性显示legend标签
开发语言·matlab
红龙创客1 小时前
某狐畅游24校招-C++开发岗笔试(单选题)
开发语言·c++
Lenyiin1 小时前
第146场双周赛:统计符合条件长度为3的子数组数目、统计异或值为给定值的路径数目、判断网格图能否被切割成块、唯一中间众数子序列 Ⅰ
c++·算法·leetcode·周赛·lenyiin
jasmine s1 小时前
Pandas
开发语言·python
biomooc1 小时前
R 语言 | 绘图的文字格式(绘制上标、下标、斜体、文字标注等)
开发语言·r语言
骇客野人1 小时前
【JAVA】JAVA接口公共返回体ResponseData封装
java·开发语言
black^sugar2 小时前
纯前端实现更新检测
开发语言·前端·javascript
404NooFound2 小时前
Python轻量级NoSQL数据库TinyDB
开发语言·python·nosql