[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);
        }
    }
相关推荐
攻城狮7号20 分钟前
【第四节】C++设计模式(创建型模式)-Builder(建造者)模式
c++·设计模式·建造者模式
fpcc24 分钟前
设计心得——解耦的实现技术
c++·软件工程
水瓶丫头站住33 分钟前
Qt中QRadioButton的样式设置
开发语言·qt
东方芷兰1 小时前
算法笔记 04 —— 算法初步(下)
c++·笔记·算法
xinghuitunan1 小时前
时间转换(acwing)c/c++/java/python
java·c语言·c++·python
TechNomad1 小时前
C++访问MySQL数据库
数据库·c++·mysql
Emplace2 小时前
ABC381E题解
c++·算法
关关钧2 小时前
【R语言】绘图
开发语言·python·r语言