【C/C++】多线程编程

Part1.【 thread 】(#include <thread>)

C++中的多线程,常通过thread类来定义一个thread对象(子线程)来实现。

thread t1 (func, arg1, arg2...);

其中func可以是一个函数名,或者函数对象;后边跟这个对象的参数;

 

在定义一个子线程以后,要确定他是join()或者detach()。

  * t1.join():表示当前线程将在此处等待t1执行完相应操作后继续执行下面的程序(已经在运行状态的程序部分不会停止)。

  * t1.detach(): 表示当前程序将不会等待以及管理t1子程序的运行。

  * 一个子线程只能被join()或者detach()一次;一个子线程被detach后不可以再被join

  *必须规定子线程是join或者detach,否则程序会终止(terminate)

 

来看一个简单的代码示例:

#include <iostream>
#include <thread>

using namespace std;

void func() {
    for(int i = 0; i < 10; ++i) {
        cout << "From sub thread" << i << endl;
    }
}

int main() {
    thread t1(func);
    for(int j = 0; j < 10; ++j) {
        cout << "From main thread" << j << endl;
    }
    t1.join();
}

***************************************************************************************************************************************

 

Part2.【 mutex/locker/condition 】(#include <mutex>; #include <conditional_variable>)

当多个线程同时使用或操作相同资源是,常会造成资源的混乱,例如两个线程同时在同一个队列或文件中进行写操作。

这种情况下我们可以用mutex类来生成一个对象,用来确保同一资源在同一时间内只被一个线程访问。

具体原理是,在使用某一资源时,如果前边有某个mutex对象mu.lock()的操作,则需先判断mu是否处于unlock的状态,如果没有的话说明被mu保护的该资源在被其他线程使用,则需要等待。

此外,由于一些特殊状况,例如异常,mu没有执行unlock, 会使被mutex对象保护的资源一直处于不能访问状态,我们经常把mutex与一些locker类结合使用,当locker对象生命周期结束时,无论通过任何方式结束,mutex对象都会被自动析构,资源便可释放。

常用的locker有:locker_guard和unique_locker。前一种效率较高,后一种功能较多更灵活,可控制locker的状态,也可与conditional_varialble结合使用。

 

* 死锁现象:

  使用mutex和locker时要注意避免死锁现象,例如:线程A拥有mu1保护的资源,在等待mu2保护的资源;而线程B拥有mu2保护的资源,在等待mu1保护的资源

  避免的方法有:

  1. 一个mutex对象尽量只对应一个资源;

  2. 若不得不安排多个mu对应一个资源,保持这些mu对象的顺序一致;最好使用std::lock(),包含一些避免死锁的机制

  3. 不要在有对象被mutex保护时去call一个用户function/未知的function;

 

* conditional_variable: (线程间交互)

在一些情况下,两个线程需要交互,其中一个线程需要等待另一个线程执行完一些操作才应该执行。

例如线程A负责往队列里放东西,线程B负责从队尾拿东西;如果A中push的操作还没有完成,B会一直查看队列中是否有元素,会导致性能的损耗。

这时会使用conditional_variable来建立一个A与B交流的方式。

比如下面一个列子:

#include <iostream>
#include <thread>
#include <mutex>
#include <deque>
#include <condition_variable>

using namespace std;

deque<int> dq;
mutex mu;
condition_variable cond;

void func1() {
    int data = 1;
    while(data <= 3) {
        unique_lock<mutex> locker(mu);
        dq.push_front(data++);
        locker.unlock();
        cond.notify_one();   //通知线程t2
        this_thread::sleep_for(chrono::seconds(1));
    }
}

void func2() {
    int data = 0;
    while(data != 3) {
        unique_lock<mutex> locker(mu);
        cond.wait(locker, [](){ return !dq.empty();});
        
        //当获得资源访问权时cond会先让出使用权,即将locker中mutex对象处于unlock状态
        //所以被其保护的资源dq可以先被其他线程使用,直到接收到cond的notify,
        //才会再次把locker处于lock的状态
       
        data = dq.back();
        dq.pop_back();
        cout << data << endl;
        locker.unlock();
    }
}

int main() {
    thread t1(func1);
    thread t2(func2);
    t1.join();
    t2.join();
}
                    

t1线程向dq中放入元素,t2从dq中取出元素;

输出结果为:

1

2

3

*********************************************************************************************************************************** 

Part3. 【 future/promise/async 】(#include <future> ) 

有时不同线程或程序的不同地方之间通信会有比较复杂的情况,

Case1. 程序块A需要程序块B执行的函数的返回值。但程序块A不能控制程序块B什么时候结束,则可通过future对象(可以看做一个channel)来获取到将来某时刻B响应函数的返回值。

Eg. future<int> fu = async(myfunc, args...);

  int x = fu.get()   //可获得函数myfunc的返回值

* async函数可理解为一个会并发进行的操作,与thread不同的是可以指定async中的操作是与当前线程同时进行还是需要时再触发,

* async函数返回一个future对象

* 同一个future对象只可以使用一次get()

 

Case2. 程序块A需要程序块B指定一些参数,但是在定义A时B还未设置该参数,则可传入一个future对象,并将其值等于一个promise对象,然后通过promise对象之后再设置具体的值。

Eg. future<int> fu;

  promise<int> pr;

  async(myfunc, ref(fu));...

  pr.set_value(...);

要注意的是promise对象使用后,一定要设置相应的值,不然会报错。

 

来看一个完整示例:

#include <iostream>
#include <thread>
#include <future>
#include <utility>

using namespace std;

int myPow(future<pair<int,int>>& fp) {
    auto p = fp.get();
    int n = p.first, x = p.second;
    int res = 1;
    for(int i = 1; i <= x; ++i)
        res *= n;
    return res;
}

int main() {
    promise<pair<int, int>> pr;
    future<pair<int, int>> fp = pr.get_future();
    future<int> fu = async(myPow, std::ref(fp));
    pr.set_value(make_pair(2,4));
    int res = fu.get();
    cout << res << endl;
」

myPow来计算一个数字x的n次方;通过future对象fp, 把参数x和n传入。而fp是通过promise对象pr来设置的。

此外myPow计算的结果又通过future对象fu传回。

 

运行结果为:16

************************************************************************************************************

Part4【 packaged_task 】

另外一个callable 类型,基本定义为:

packaged_task<T1(T2...)> pt (myFunc);

其中T1是函数myFunc返回类型,T2..是myFunc的各函数类型;运行函数时应该使用:pt(arg1, arg2...);

一个packaged_task类型对象可以通过调用get_future()返回一个future对象

 

* 返回future类型的途径:

1. async(),函数直接返回类型;

2. promise对象,调用get_future();

3. packaged_task对象,调用get_future();

 

一个代码示例:

#include <iostream>
#include <thread>
#include <future>
#include <queue>

using namespace std;

int factorial(int n) {
    int res = 1;
    for(int i = n; i >= 1; --i)
        res *= n;
    return n;
}

queue<packaged_task<int()> q;
mutex mu;
conditional_variable cond;

void thread_1() {
    unique_lock<mutex> locker(mu);
    cond.wait(locker, [](){return !q.empty();});
    packaged_task<int()> t = move(q.front());
    q.pop();
    t();
}

int main(){
    thread t1(thread_1);
    packaged_task<int()> t(bind(factorial,6));
    unique_lock<mutex> locker(mu);
    q.push(t);
    cond.notify_one();
    t1.join();
    int x = t.get_future().get();
    cout << x << endl;
}

 

主线程中:

  定义要执行thread_1的子线程t1,

  定义要执行factorial的packaged_task t,并把他push到任务队列中,

  通知t1:

t1中:

  t1从任务队列中拿出t并执行factorial,执行结果被保存在t中(future channel中)

主线程中:

  t1结束后,从t中拿到结果

运行结果为:16

 

**以上内容参考相关视频: https://www.bilibili.com/video/BV1ut411y7u5**

上一篇:Dart3_异步编程


下一篇:python3.6/site-packages/h5py/__init__.py:36: FutureWarning: Conversion of the second argument解决办法