当前位置:   article > 正文

c++ thread创建与多线程同步详解_std::thread同步

std::thread同步

1.thread的构造函数

c++11以后,引入了新的线程库thread,这样可以方便多线程操作。

std::thread中的构造函数如下
默认构造函数 thread() noexcept
初始化构造函数 template <class F, class …Args> explicit thread(F&& f, Args&&… args);
复制[delete] thread(const thread&) = delete;
移动 thread(thread&& t) noexcept;

默认构造函数创建一个空的thread对象。
初始化构造函数,会创建一个thread对象。该对象joinable,产生的新线程将调用f,参数为args。
复制构造函数被delete,说明thread不可以用拷贝构造。
move构造函数,调用成功之后 x 不代表任何 thread 执行对象。

2.thread构造实例

创建thread有意义的对象整体可以表示为
std::thread threadObj(<CallBack Function>)
我们通过给thread对象传入回调函数,该回调函数将会在新线程中启动。而回调函数的形式可以为:
1.函数指针
2.函数对象
3.lambda表达式

2.1 通过普通函数(函数指针)构造

void thread_print_func() {
    for(int i=0; i<10; i++) {
        cout<<"this is thread print func!"<<endl;
    }
}

void f1() {
    thread obj(thread_print_func);
    obj.join();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

2.2 通过函数对象构造

class PrintThread {
    public:
        void operator()() {
            for(int i=0; i<10; i++) {
                cout<<"PrintThread running!"<<endl;
            }
        }
};

void f2() {
    thread obj((PrintThread()));
    obj.join();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

2.3 通过lambda表达式构造

void f3() {
    thread obj([] {
        for(int i=0; i<10; i++) {
            cout<<"lambda func ruuning!"<<endl;
        }
    });
    obj.join();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

以上三种方式均可实现创建thread线程。

3.获取不同线程id

每个thread对象都有一个id,可以通过get_id方法获取

void print_thread_id() {
    cout<<"thread id is: "<<this_thread::get_id()<<endl;
}

void f4() {
    thread obj1(print_thread_id);
    cout<<"in f4, obj1 thread_id is: "<<obj1.get_id()<<endl;
    obj1.join();
}

int main(int argc, char const *argv[])
{
    f4();
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

上面代码运行以后结果为

in f4, obj1 thread_id is: 0x700007245000
thread id is: 0x700007245000
  • 1
  • 2

4.创建多线程

thread实现多线程也不麻烦。看一个简单的例子

void unblock_func(char c) {
    for(int i=0; i<100; i++) cout<<c;
    cout<<endl;
}

void f5() {
    thread t1(unblock_func, '*');
    thread t2(unblock_func, '%');
    t1.join();
    t2.join();
    for(int i=0; i<100; i++) {
        cout<<'$';
    }
    cout<<endl;
}

int main(int argc, char const *argv[])
{
    f5();
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

上面代码运行以后,某一次结果为

*****************************************************************%%%%%%%%%%%%%%%%%%%****%%%%%%%%********%%%%%%*******%%%%%%%*********%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%*******%%%%%%%


$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
  • 1
  • 2
  • 3
  • 4

线程可以join,也可以detach(分离)。线程一旦开始运行,需要显式决定其是要等待完成(join),还是分离让他自行运行(detach)。当thread::join函数被调用以后,调用他的线程会阻塞,直到线程的执行被完成。基本上,这是一种可以用来知道一个线程已结束的机制。当thread::join()返回时,OS的执行的线程已经完成,C++线程对象可以被销毁。

比如上面的代码,有两个线程执行了join方法,可以看出,线程t1,t2交替执行。当这两个线程执行完毕以后,再执行main方法中的逻辑。
如果使用的是detach方法,我们看看下面的实验

#include <unistd.h>

void unblock_func(char c) {
    for(int i=0; i<50; i++) cout<<c;
    cout<<endl;
}

void f5() {
    thread t1(unblock_func, '*');
    thread t2(unblock_func, '%');
    //t1.join();
    //t2.join();
    t1.detach();
    t2.detach();
    for(int i=0; i<50; i++) {
        cout<<'#';
    }
    sleep(1);
    cout<<endl;
    
}

int main(int argc, char const *argv[])
{
    f5();
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

某一次运行的结果为

#%#%%%%%%%%%%%%%%%%%%%%%%####%****************%*%*############################################**************************%%%%%%%%%%%%%%%%%%%%%%%%
******


  • 1
  • 2
  • 3
  • 4

detach被调用以后,目标线程变成了守护线程,此时会驻留后台运行,与之关联的std::thread对象失去对目标线程的关联,无法再通过std::thread对象取得该线程的控制权。当线程主函数执行完之后,线程就结束了,运行时库负责清理与该线程相关的资源。

detach是使主线程不用等待子线程可以继续往下执行,但即使主线程终止了,子线程也不一定终止。

我们观察上面的代码,很明显t1, t2, main方法中的逻辑不是顺序执行的。我们加上sleep(1)的目的,是为了观察到线程t1, t2执行完毕,要不很有可能main方法执行完毕以后,t1,t2还未执行完,此时打印出来的符号没有那么多。

5.通过锁线程同步

5.1 mutex

mutex实现的方式比较简单,在进入临界区之前调用该变量的lock函数,出临界区之前调用该变量的的unlock函数。

将第四部分代码简单进行修改

mutex mtx;

void block_func(char c) {
    mtx.lock();
    for(int i=0; i<100; i++) cout<<c;
    cout<<endl;
    mtx.unlock();
}

void f6() {
    thread t1(block_func, '*');
    thread t2(block_func, '%');
    t1.join();
    t2.join();
}

int main(int argc, char const *argv[])
{
    f6();
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

代码输出如下

****************************************************************************************************
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  • 1
  • 2

从上面结果可以看出,此时t1,t2就不会交替输出。
但是有个问题:cout<<c,这部分逻辑如果发生了异常咋办?抛出异常后,意味着mtx.unlock()不会被执行,即锁没有被释放,整个程序进入不了临界区,该程序往往会挂死。

5.2 Lock_guard

同样首先在线程之外声明mutex变量,在线程进入临界区之前声明lock_guard变量,将mutex变量作为变量传入,在构造函数中会调用该变量的lock(),在析构函数中调用unlock(),如此无论是正常运行结束还是临界区中出现异常都会正常执行锁操作。

void block_func(char c) {
    lock_guard<mutex> l(mtx);
    for(int i=0; i<100; i++) cout<<c;
    cout<<endl;
}

void f6() {
    thread t1(block_func, '*');
    thread t2(block_func, '%');
    t1.join();
    t2.join();
}

int main(int argc, char const *argv[])
{
    f6();
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

上面代码输出:

****************************************************************************************************
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  • 1
  • 2

输出是不会乱序的。

lock_guard实现简单,使用方便,适用于大多数场景。但存在的问题是使用场景过于简单,无法处理一些精细操作。此时便需要使用unique_lock。

5.3 unique_lock

unique_lock用法与lock_guard基本相同,不一样的在于它提供了非常多构造函数。

public:
    _LIBCPP_INLINE_VISIBILITY
    unique_lock() _NOEXCEPT : __m_(nullptr), __owns_(false) {}
    _LIBCPP_INLINE_VISIBILITY
    explicit unique_lock(mutex_type& __m)
        : __m_(_VSTD::addressof(__m)), __owns_(true) {__m_->lock();}
    _LIBCPP_INLINE_VISIBILITY
    unique_lock(mutex_type& __m, defer_lock_t) _NOEXCEPT
        : __m_(_VSTD::addressof(__m)), __owns_(false) {}
    _LIBCPP_INLINE_VISIBILITY
    unique_lock(mutex_type& __m, try_to_lock_t)
        : __m_(_VSTD::addressof(__m)), __owns_(__m.try_lock()) {}
    _LIBCPP_INLINE_VISIBILITY
    unique_lock(mutex_type& __m, adopt_lock_t)
        : __m_(_VSTD::addressof(__m)), __owns_(true) {}
    template <class _Clock, class _Duration>
    _LIBCPP_INLINE_VISIBILITY
        unique_lock(mutex_type& __m, const chrono::time_point<_Clock, _Duration>& __t)
            : __m_(_VSTD::addressof(__m)), __owns_(__m.try_lock_until(__t)) {}
    template <class _Rep, class _Period>
    _LIBCPP_INLINE_VISIBILITY
        unique_lock(mutex_type& __m, const chrono::duration<_Rep, _Period>& __d)
            : __m_(_VSTD::addressof(__m)), __owns_(__m.try_lock_for(__d)) {}
    _LIBCPP_INLINE_VISIBILITY
    ~unique_lock()
    {
        if (__owns_)
            __m_->unlock();
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

我们将上面的block_func稍作修改

void block_func(char c) {
    unique_lock<mutex> l(mtx);
    for(int i=0; i<100; i++) cout<<c;
    cout<<endl;
}
  • 1
  • 2
  • 3
  • 4
  • 5

这样的话最终输出也是不会乱序的。

本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号