c++11并发与多线程

本文介绍了C++11中创建、启动、结束线程的方法,包括线程传参、成员函数作为线程函数,以及数据共享、unique_lock和mutex的使用。还涵盖了跨平台线程库的使用、条件变量、async/future/promise和线程池的实践。
摘要由CSDN通过智能技术生成


前言

写多线程程序时,Windows系统下用CreateThread函数创建线程,Linux系统下用pthread_create创建线程,所以这些代码不能跨平台使用。当然,如果使用一些跨平台的多线程库如POSIX_thread(pthread)是可以跨平台的。但是使用pthread,在Windows和Linux下还是要分别配置一番,两个系统总有一些不同的地方,所以还是不够方便。从C++11新标准开始,C++语言本身增加了针对多线程的支持。


一、创建、启动、结束线程

主线程是从main函数开始执行的,自己创建的线程也得从一个函数(初始函数)开始执行,函数执行完,线程也就退出了。一般来讲,整个进程(程序)是否执行完毕的标志是主线程是否执行完,主线程一旦结束,子线程就会被操作系统强制终止(detach会打破这个规律)。

#include <iostream>
#include <thread>

using namespace std;

void myprintf() {
    cout << "I'm a child thread." << endl;
}

int main() {
    cout << "I'm the main thread." << endl;
    thread threadObj(myprintf);  // 创建一个线程
    threadObj.join();
    return 0;
}

thread是一个类,这个类就是用来创建线程的。它创建了一个对象threadObj,线程创建出来后,会执行myprintf函数。join使主线程阻塞在这里,等待子线程执行完毕。有时并不需要主线程等待子线程退出,可以使用detach。有资料解释称,线程detach后,与这个线程关联的thread对象就会失去与这个线程的关联(因为thread对象是在主线程中定义的),此时这个线程就会驻留在后台运行,相当于被C++运行时库接管了。这种分离的线程在Linux叫守护线程(守护进程)。针对一个线程,一旦detach(join)后就不能join(detach)了。joinable()可以判断一个线程是否可以join或detach,可以返回true,不能返回false。thread接受的是一个可调用对象作为参数来创建线程,下面换种写法:

#include <iostream>
#include <thread>

using namespace std;

class TA {
   public:
    // 可调用对象,重载圆括号
    void operator()() {  // 不带参数
        cout << "TA()" << endl;
    }
};

int main() {
    cout << "I'm the main thread." << endl;
    TA ta;
    thread threadObj(ta);
    // thread threadObj(TA());  // 不能使用临时对象,编译无法通过
    threadObj.join();
    return 0;
}

类与detach结合使用,可能会带来问题:

#include <iostream>
#include <thread>

using namespace std;

class TA {
   public:
    TA(int &i) : m_i(i) {}
    // 可调用对象,重载圆括号
    void operator()() {  // 不带参数
        cout << "TA()" << endl;
    }

    int &m_i;
};

int main() {
    cout << "I'm the main thread." << endl;
    int i = 6;
    TA ta(i);
    thread threadObj(ta);
    threadObj.detach();
    return 0;
}

成员变量m_i是一个引用,绑定的是main函数里的i变量,当主线程执行结束时,i变量被销毁,子线程可能在后台继续运行,继续使用m_i,就会产生不可预料的后果。还有,当主线程结束后,ta对象被销毁,而子线程好像正在使用这个ta对象,这样是否会出现问题?其实ta对象是会被复制到子线程中的,但是这个对象如果有引用或指针,那就可能出现问题。

#include <iostream>
#include <thread>

using namespace std;

class TA {
   public:
    TA(int i) : m_i(i) {
        printf("TA构造函数执行,m_i = %d,this = %p\n", m_i, this);
    }
    ~TA() {
        printf("TA析构函数执行,m_i = %d,this = %p\n", m_i, this);
    }
    TA(const TA& ta) : m_i(ta.m_i) {
        printf("TA拷贝构造函数执行,m_i = %d,this = %p\n", m_i, this);
    }
    // 可调用对象,重载圆括号
    void operator()() {  // 不带参数
        cout << "TA()" << endl;
    }

    int m_i;
};

int main() {
    int i = 6;
    TA ta(i);
    thread threadObj(ta);
    threadObj.detach();
    return 0;
}

执行结果:
执行结果
拷贝构造函数执行了一遍,说明ta对象是被复制到子线程中去了。主线程结束后,子线程还没结束,所以只执行了一次析构函数。子线程跑到后台运行,后续析构函数的打印也不会显示到屏幕上。用lambda表达式创建线程:

#include <iostream>
#include <thread>

using namespace std;
int main() {
    auto mylambda = [] {
        cout << "my lambda" << endl;
    };
    thread threadObj(mylambda);
    threadObj.join();
    return 0;
}

二、线程传参、成员函数作为线程函数

实际工作中可能需要创建不止一个线程,例如创建10个线程,0号线程加工前10个零件,1号线程加工第11到第20个零件,以此类推,这说明每个线程都需要知道自己需要加工的编号,这就需要给线程传递参数。先看看传递临时对象作为线程参数:

#include <iostream>
#include <thread>

using namespace std;

void myprintf(const int &i, char *pBuf) {
    cout << i << endl;
    cout << pBuf << endl;
}

int main() {
    int var = 1;
    int &vary = var;
    char buf[] = "this is a test.";
    thread threadObj(myprintf, var, buf);
    threadObj.join();
    return 0;
}

运行结果:
运行结果
根据观察(跟踪调试),函数myprintf中,形参i的地址和原来main函数中var地址不同(虽然是引用类型),也就是说thread类的构造函数实际是复制了这个参数。而myprintf函数中的pBuf指向的内存就是main中buf的内存。C++语言只会为const引用产生临时对象。

void myprintf(int i, const string &pBuf) {  // 将第二个参数也改成const引用
    cout << i << endl;
    cout << pBuf.c_str() << endl;
}

第二个参数使用了const引用,所以实际上发生了对象复制,这个与系统内部工作机理有关。

thread threadObj(myprintf, var, buf);

这行代码的本意是希望系统帮助我们把buf隐式转换成string,但如果是detach,等main函数都执行完了,才把buf往string转,就会出现问题,所以把这行代码写成以下的形式,可以保证在线程myprintf中所用的pBuf肯定是有效的:

thread threadObj(myprintf, var, string(buf));

结论就是:如果传递int这种简单类型参数,建议使用值传递,不用引用;如果传递类对象作为参数,则避免隐式类型转换(例如把一个char*转成string,把一个int转成类A对象),全部在创建线程这一行就构建出临时对象来,线程入口函数的形参位置使用引用来作为形参(这里如果不使用引用可能在某种情况下会导致多构造一次临时类对象);建议使用join,不使用detach,就不存在局部变量失效导致线程对内存非法引用的问题。所以在子线程中通过参数传递给线程入口函数的形参(对象)实际是实参对象的复制,这就意味着即便修改了线程入口函数中的对象的内容,也无法反馈到外面,这时就需要用到std::ref了,这是一个函数模板。接下来看看传递类对象与智能指针作为线程参数,如下代码:

#include <iostream>
#include <vector>
#include <thread>

using namespace std;

class A {
   public:
    A(int a) : m_i(a) {
        cout << "A::A(int a)构造函数执行,this = " << this << ",threadid = " << std::this_thread::get_id() << endl;
    }
    A(const A& a) {
        cout << "A::A(const A)拷贝构造函数执行,this = " << this << ",threadid = " << std::this_thread::get_id() << endl;
    }
    ~A() {
        cout << "~A::A()析构函数执行,this = " << this << ",threadid = " << std::this_thread::get_id() << endl;
    }

  public:
    void operator()(int num) {
        cout << "子线程()执行,this = " << this << "threadid = " << std::this_thread::get_id() << endl;
    }
    int m_i;
};

void myprint2(const A& pmybuf) {
    cout << "子线程myprint2的参数pmybuf的地址是:" << &pmybuf << ",threadid = " << std::this_thread::get_id() << endl;
}

int main() {
    A myobj(10);  // 生成一个类对象
    std::thread mytobj(myprint2, std::ref(myobj));
    mytobj.join();
    return 0;
}

用std::ref(myobj),这样传递的参数真的是一个引用而不是复制出一个临时对象作为形参了,所以myprint2形参也可以去掉const修饰了。将智能指针作为形参传递到线程入口函数:

#include <iostream>
#include <vector>
#include <thread>

using namespace std;

void myprint3(unique_ptr<int> pzn) {
    return;
}

int main() {
    unique_ptr<int> myp(new int(100));
    std::thread mytobj(myprint3, std::move(myp));
    mytobj.join();
    return 0;
}

用std::move将一个unique_ptr转移到另一个unique_ptr上,代码相当于将myp转移到了形参pzn中,myp变为空。最后看看用成员函数作为线程入口函数,在类A中增加一个成员函数:

public:
    void thread_work(int num) {  // 带一个参数
        cout << "子线程thread_work执行,this = " << this << ",threadid = " << std::this_thread::get_id() << endl;
    }

main函数调整为:

A myobj(10);
std::thread mytobj(&A::thread_work, myobj, 15);  // 1
//std::thread mytobj(&A::thread_work, &myobj, 15);  // 2
//std::thread mytobj(&A::thread_work, std::ref(myobj), 15);  // 3
mytobj.join();

如1那样,会调用一次A的拷贝构造函数,也可以写成2、3那样,不会调用A的拷贝构造函数。A中有对圆括号重载,main函数也可以写成:

A myobj(10);
thread mytobj(myobj, 15);  // 1
// thread mytobj(std::ref(myobj), 15); // 2 第二个参数无法修改为&myobj,编译会报错
mytobj.join();

如1那样,会调用一次A的拷贝构造函数,也可以如2使用std::ref,不会调用类A的拷贝构造函数,但不可以使用&myobj。

三、数据共享问题

多线程同时操作数据时,需要对数据加锁,可以使用mutex:

#include <iostream>
#include <mutex>
#include <list>
#include <vector>

using namespace std;

std::list<int>  msgRecvQueue;  // 容器
std::mutex my_mutex;           // 创建互斥量

void myFun() {
    my_mutex.lock();
    // 操作容器msgRecvQueue
    my_mutex.unlock();
}

int main() {
    vector<thread> mythreads;
    for (int i = 0; i < 5; i++) {
        mythreads.push_back(thread(myFun));  // 创建并开始执行线程
    }
    for (auto iter = mythreads.begin(); iter != mythreads.end(); ++iter) {
        iter->join();  // 等待5个线程都返回
    }
    return 0;
}

可以使用std::lock_guard替代lock和unlock:

#include <mutex>
#include <thread>
#include <list>
using namespace std;

void outMsgLULProc(int& command) {
    std::lock_guard<std::mutex> sbguard(my_mutex);  // sbguard是随便起的变量名 
    if (!msgRecvQueue.empty()) {
        command = msgRecvQueue.front();  // 返回第一个元素但不检查元素存在与否
        msgRecvQueue.pop_front();
   }
}

lock_guard<std::mutex>的工作原理是:在lock_guard类模板的构造函数里,调用了mutex的lock函数,而在析构函数里调用了mutex的unlock函数。所以当离开sbguard的作用域时,就自动解锁了。当有两个互斥量要上锁时,可能会造成死锁:

void inMsgRecvQueue() {
    for (int i = 0; i < 100000; i++) {
        my_mutex.lock();  // 两行lock()代码不一定紧张挨着,可能它们要保护不同的数据共享块
        //......需要保护的一些共享数据
        my_mutex2.lock();
        msgRecvQueue.push_back(i); 
        my_mutex2.unlock();
        my_mutex.unlock();
    }
}

void outMsgLULProc(int& command) {
    my_mutex2.lock();
    my_mutex.lock();
    if (!msgRecvQueue.empty()) {
        command = msgRecvQueue.front();
        msgRecvQueue.pop_front();
    }
    my_mutex2.unlock();
    my_mutex.unlock();
}

第一条线程先对my_mutex进行lock,第二条先对my_mutex2进行lock,造成死锁,程序无法继续往下运行。当需要锁住两个互斥量时,可以使用std::lock函数模板,它能一次锁住两个或以上的互斥量,如果它先锁住第一个互斥量,但锁第二个的时候失败了,它会把第一个解锁,然后卡在那里不断地尝试锁这两个互斥量。

void inMsgRecvQueue() {
    for (int i = 0; i < 100000; i++) {
        std::lock(my_mutex, my_mutex2);  // 相当于每个互斥量都调用了lock
        msgRecvQueue.push_back(i);
        my_mutex2.unlock();  // 前面锁住2个,后面就得解锁2个
        my_mutex.unlock();
    }
}

结合std::lock_guard使用,用上std::lock_guard的std::adopt_lock参数:

void inMsgRecvQueue() {
    for (int i = 0; i < 100000; i++) {
        std::lock(my_mutex, my_mutex2);
        std::lock_guard<std::mutex> sbguard1(my_mutex, std::adopt_lock);
        std::lock_guard<std::mutex> sbguard2(my_mutex2, std::adopt_lock);			
        msgRecvQueue.push_back(i);
    }
}

使用std::adopt_lock后,在构造lock_guard<std::mutex>对象时,它的构造函数不会调用my_mutex和my_mutex2的lock函数,析构函数仍然会unlock,这样就可以自动解锁了。

四、unique_lock类模版

unique_lock也是用来对mutex(互斥量)进行加锁和解锁管理,它比lock_guard更加灵活,代价是执行效率差一点,内存占用的也稍微多一点。

bool outMsgLULProc(int& command) {
    std::unique_lock<std::mutex> sbguard1(my_mutex);
    if (!msgRecvQueue.empty()) {
        // 消息不为空			
        command = msgRecvQueue.front();  // 返回第一个元素,但不检查元素是否存在;
        msgRecvQueue.pop_front();        // 移除第一个元素,但不返回;
        return true;
    }
    return false;
}

unique_lock也有std::adopt_lock参数,使用这个参数表示互斥量mutex已经被lock过了,不需要再在std::unique_lock<std::mutex>对象的构造函数中lock了,使用这个参数一定要保证互斥量已经lock过了,否则会出现异常。

my_mutex.lock();
std::unique_lock<std::mutex> sbguard1(my_mutex, std::adopt_lock);

unique_lock的另一个参数std::try_to_lock,使用这个参数,系统会尝试去锁住mutex,如果没锁住,也会立即返回,不会阻塞在那里。使用std::try_to_lock的前提是开发者不能自己把互斥量lock上。

void inMsgRecvQueue() {
    for (int i = 0; i < 100000; ++i) {
        std::unique_lock<std::mutex> sbguard1(my_mutex, std::try_to_lock);
        if (sbguard1.owns_lock()) {  // 条件成立表示拿到了锁头
            // 拿到了锁头,离开sbguard1作用域锁头会自动释放
            msgRecvQueue.push_back(i);  // 假设这个数字就是收到的命令,直接放到消息队列里来
            // 其他处理代码
        } else {
            // 没拿到锁
            cout << "inMsgRecvQueue()执行,但没拿到锁,只能干点别的事" << i << endl;
       }
    }
}

unique_lock所支持的另一个参数std::defer_lock,用这个参数的前提是开发者不能自己先去把互斥量lock上,否则会报异常。std::defer_lock的意思是初始化这个mutex,但并没有给这个mutex加锁。这个参数可以结合unique_lock这个类模板的一些重要的成员函数使用,比如lock函数,像下面的例子,可以随时加锁,函数退出的时候,会自动解锁:

void inMsgRecvQueue() {
    for (int i = 0; i < 100000; ++i) {
        std::unique_lock<std::mutex> sbguard1(my_mutex, std::defer_lock);
        sbguard1.lock();  // 反正unique_lock能自动解锁,不用自己解,所以这里只管加锁
        msgRecvQueue.push_back(i);
    }
}

unique_lock有unlock函数,可以给已经加锁的互斥量解锁,虽然unique_lock能够自动解锁,但是也可以用该函数手工解锁,这就是它灵活的地方。unique_lock还有try_lock函数,尝试给互斥量加锁,如果拿不到锁,则返回false,拿到则返回true,这个函数不阻塞。

void inMsgRecvQueue() {
    for (int i = 0; i < 100000; ++i) {
        std::unique_lock<std::mutex> sbguard1(my_mutex, std::defer_lock);
        if (sbguard1.try_lock() == true) {  // 返回true表示拿到了锁,自己不用管unlock问题
            msgRecvQueue.push_back(i);
        } else {
            cout << "抱歉,没拿到锁,做点别的事情吧!" << endl;
        }
    }
}

unique_lock还有一个release函数,返回它管理的mutex对象指针,并释放所有权,也就是这个unique_lock和mutex不再有关系。一旦解除unique_lock和所管理的mutex的关联关系,如果原来的mutex对象处于加锁状态,则开发者有责任负责解锁。

std::unique_lock<std::mutex> sbguard1(my_mutex);  // mutex锁定
std::mutex* p_mtx = sbguard1.release();  // 现在关联关系解除,程序员有责任自己解锁了,其实这个就是my_mutex,现在sbguard1已经不和my_mutex关联了
msgRecvQueue.push_back(i);
p_mtx->unlock();  // 因为前面已经加锁,所以这里要自己解锁了

可以看出unique_lock要发挥作用,需要和一个mutex互斥量绑定到一起,这样才是一个完整的能发挥作用的unique_lock,也就是说unique_lock需要管理一个mutex指针。一个mutex应该只和一个unique_lock绑定,如下代码会报异常:

std::unique_lock<std::mutex> sbguard1(my_mutex);
std::unique_lock<std::mutex> sbguard10(my_mutex);

unique_lock可以把它所拥有的这个mutex传递给其他的unique_lock,unique_lock对这个mutex的所有权是属于可以移动但不可以复制的,这个所有权的传递跟unique_ptr智能指针的所有权传递非常类似。

std::unique_lock<std::mutex> sbguard1(my_mutex);
//std::unique_lock<std::mutex> sbguard10(sbguard1);  // 复制所有权,不可以
std::unique_lock<std::mutex> sbguard10(std::move(sbguard1));  // 移动语义,这可以现在my_mutex和sbguard10绑定到一起了。移动后sbguard1指向空,sbguard10指向了该my_mutex	
msgRecvQueue.push_back(i);

另外返回unique_lock类型,也是一种用法(程序写法):

std::unique_lock<std::mutex> rtn_unique_lock() {
    std::unique_lock<std::mutex> tmpguard(my_mutex);
    // 从函数返回一个局部unique_lock对象是可以的,返回这种局部对象tmpguard
    // 会导致系统生成临时unique_lock对象,并调用unique_lock的移动构造函数
    return tmpguard;
}
void inMsgRecvQueue() {
    for (int i = 0; i < 100000; ++i) {
        std::unique_lock<std::mutex> sbguard1 = rtn_unique_lock();
        msgRecvQueue.push_back(i);
   }
}

五、多线程创建单例类对象

单例类对象只能创建一个,如果多线程创建单例类对象,要注意共享数据的保护:

class MyCAS {  // 这是一个单例类
   private:
    MyCAS() {}  // 构造函数是私有的

   public:
    static MyCAS* GetInstance() {
        if (m_instance == NULL) {
            std::unique_lock<std::mutex> mymutex(resource_mutex);  // 自动加锁
            if (m_instance == NULL) {
               m_instance = new MyCAS();
               static CGarhuishou cl;  // 生命周期一直到程序退出
           }			
       }
       return m_instance;
   }
       
   class CGarhuishou {  // 类中套类,用于释放对象
      public:
      ~CGarhuishou() {
          if (MyCAS::m_instance) {
              delete MyCAS::m_instance;
              MyCAS::m_instance = NULL;
          }
      }
	};
       
   private:
    static MyCAS* m_instance;
};

上面代码包含了两句if (m_instance == NULL),这种写法叫“双重锁定”或者“双重检查”,当m_instance不为空时,说明m_instance一定被new过了,也就不用加锁了,可以提高效率。c++11引入了std::call_once函数,能保证函数只被调用一次:

std::once_flag g_flag;  // 这是个系统定义的标记

static void CreateInstance() {
    m_instance = new MyCAS();
    static CGarhuishou cl;
}

static MyCAS* GetInstance() {
    if (m_instance == NULL) {  // 同样为提高效率。
        // 两个线程同时执到这里时,其中一个线程卡在这行等另外一个线程的该行执行完毕(所以可以把g_flag看成一把锁)。	
        std::call_once(g_flag, CreateInstance);
   }
   return m_instance;
}

这样保证函数CreateInstance()只被调用一次。

六、条件变量

std::condition_variable是一个类,一个和条件相关的类,用于等待一个条件达成,需要和互斥量配合工作:

class A {
   public:
    void outMsgRecvQueue() {
        int command = 0;
        while (true) {
            std::unique_lock<std::mutex> sbguard1(my_mutex);  // 临界进去
            // wait()用于等一个东西
            // 如果wait()第二个参数的lambda表达式返回的是true,wait就直接返回
            // 如果wait()第二个参数的lambda表达式返回的是false,那么wait()将解锁互斥量,并堵塞到这行,堵到其他某个线程调用notify_one()通知为止
            // 如果wait()不用第二个参数,那跟第二个参数为lambda表达式并且返回false效果一样(解锁互斥量,并堵塞到这行,堵到其他某个线程调用notify_one()通知为止)
            my_cond.wait(sbguard1, [this] {
                                      if (!msgRecvQueue.empty())
                                          return true;
                                      return false;
                                      });

            // 现在互斥量是锁着的,流程走下来意味着msgRecvQueue队列里必然有数据
            command = msgRecvQueue.front();  // 返回第一个元素,但不检查元素是否存在
            msgRecvQueue.pop_front();  // 移除第一个元素,但不返回
            sbguard1.unlock();  // 因为unique_lock的灵活性,可以随时unlock解锁,以免锁住太长时间
            cout << "outMsgRecvQueue()执行,取出一个元素" << command << " threadid = " << std::this_thread::get_id() << endl;
        }  // end while
    }
    
    void inMsgRecvQueue() {
        for (int i = 0; i < 100000; ++i) {
            cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
            std::unique_lock<std::mutex> sbguard1(my_mutex);
            msgRecvQueue.push_back(i);  // 假设这个数字就是我收到的命令,直接放到消息队列里来
            my_cond.notify_one();  // 尝试把卡(堵塞)在wait()的线程唤醒,但光唤醒了还不够,这里必须把互斥量解锁,另外一个线程的wait()才会继续正常工作
        }
        return;
	}
    
   private: 
    std::condition_variable my_cond;  // 生成一个条件对象
};

这样outMsgRecvQueue()函数只要等待inMsgRecvQueue()唤醒就可以取数据了。notify_one()可以通知一条线程,当有两条线程在执行outMsgRecvQueue()时,notify_one()只能通知到其中一条,要想通知全部,就要用到notify_all:

void inMsgRecvQueue() {
    for (int i = 0; i < 100000; ++i) {
        cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
        std::unique_lock<std::mutex> sbguard1(my_mutex);
        msgRecvQueue.push_back(i);  // 假设这个数字就是我收到的命令,直接放到消息队列里来
        my_cond.notify_all();
    }
    return;
}

int main() {		
	A myobja;
	std::thread myOutnMsgObj(&A::outMsgRecvQueue, &myobja);  // 第二个参数是引用,才能保证线程里用的是同一个对象
	std::thread myOutnMsgObj2(&A::outMsgRecvQueue, &myobja);
	std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
	myInMsgObj.join();
	myOutnMsgObj2.join();
	myOutnMsgObj.join();
	cout << "main主函数执行结束!" << endl;	
	return 0;
}

notify_all通知两个outMsgRecvQueue线程,当这两个线程都被唤醒后,这两个线程中的每一个也需要尝试重新获取锁,结果还是只有一个线程能获取到锁往下走,另外一个获取不到锁会继续卡在wait那里等待。所以这里用notify_all和notify_one的结果相同。这里提及一个概念,叫作“虚假唤醒”,就是wait代码行被唤醒了,但是不排除msgRecvQueue(消息队列)里面没有数据的情形。虚假唤醒产生的情况很多,例如push_back一条数据,调用多次notify_one,或者是有多个outMsgRecvQueue线程取数据,但是inMsgRecvQueue线程只push_back了一条数据,然后用notify_all把所有的outMsgRecvQueue线程都通知到了,就总有某个outMsgRecvQueue线程被唤醒,但是队列中并没有它要处理的数据。代码里处理虚假唤醒的做法:

my_cond.wait(sbguard1, [this] {
    if (!msgRecvQueue.empty())
        return true;  // 该lambda表达式返回true,则wait就返回,流程走下来,互斥锁被本线程拿到
    return false;     // 解锁并休眠,卡在wait等待被再次唤醒
});

if语句来应付虚假唤醒,因为wait被唤醒后,要先拿锁,拿到锁后才会执行这个lambda表达式中的判断语句,所以此时这个lambda表达式里面的判断是安全的。

七、async、future、packaged_task与promise

std::async是一个函数模板,用来启动一个异步任务,会返回一个std::future对象(std::future是一个类模板)。就是说std::async会创建一个新线程(有时不会创建新线程,后面举例)并开始执行对应的线程入口函数,返回的future对象含有线程入口函数的返回结果,可以通过future对象的成员函数get来获取结果。

#include <future>

int mythread(int mypar) {
 	cout << "mythread() start" << " threadid = " << std::this_thread::get_id() << endl;
 	std::chrono::milliseconds dura(5000);  // 1秒 = 1000毫秒,所以5000毫秒 = 5秒
    std::this_thread::sleep_for(dura);     // 休息一定的时长
    cout << "mythread() end" << " threadid = " << std::this_thread::get_id() << endl;
    return 5;
}

int main() {		
    cout << "main" << " threadid = " << std::this_thread::get_id() << endl;
    std::future<int> result = std::async(mythread);  // 流程并不会卡在这里,注意如果线程入口函数需要参数,可以把参数放在async的第二个参数的位置
    cout << result.get() << endl;  // 卡在这里等待线程执行完,但是这种get因为一些内部特殊操作,不能get多次,只能get一次,否则执行会报异常	
    // result.wait();  // 流程卡在这里等待线程返回,但本身不返回结果
}

result.get()结果是5。也可以用result.wait(),但只是等待线程返回,不返回结果。std::async可以带参数:

class A {
   public:
    int mythread(int mypar) {
        cout << mypar << endl;
        cout << "mythread() start" << " threadid = " << std::this_thread::get_id() << endl; //新的线程id
        std::chrono::milliseconds dura(20000); //1秒 = 1000毫秒,所以20000毫秒 = 20秒
        std::this_thread::sleep_for(dura); //休息一定的时长
        cout << "mythread() end" << " threadid = " << std::this_thread::get_id() << endl;
        return 5;
   }
};
int main() {
    A a;
    int tmppar = 12;
    cout << "main" << " threadid = " << std::this_thread::get_id() << endl;
    std::future<int> result = std::async(&A::mythread, &a, tmppar);  // 这里第二个参数是对象地址,才能保证线程里面用的是同一个对象。
                                                                      // 第三个参数是线程入口函数的参数
    cout << "continue......!" << endl;
    cout << result.get() << endl;		
    cout << "main主函数执行结束!" << endl;
    return 0;
}

可以给std::async提供一个额外的参数,类型是std::launch枚举类型,看一看这个枚举类型可以取哪些值:

auto result = std::async(std::launch::deferred, &A::mythread, &a, tmppar);

使用std::launch::deferred,表示该线程入口函数的执行被延迟到std::future的wait或者get函数被调用时,如果一直不调用wait或get,则这个线程就不执行了。调用了wait或get后,发现线程入口函数被执行了,但发现是在主线程中调用的mythread线程入口函数,所以这种写法并没有创建出新线程。而使用另一个参数std::launch::async,表示调用std::async时就开始创建并执行线程,意味着系统必须要创建出新线程来执行。

auto result = std::async(std::launch::async, &A::mythread, &a, tmppar);

如果同时使用std::launch::deferred和std::launch::async,系统会根据一定因素(如硬件资源等)去评估是以std::launch::async方式还是std::launch::deferred方式去执行,两者选其一:

auto result = std::async(std::launch::async | std::launch::deferred, &A::mythread, &a, tmppar);  // “|”符号表示两个枚举值一起使用

如果不使用任何额外的参数,效果就相当于使用了std::launch::async | std::launch::deferred,由系统决定运行方式。相对于std::thread来说,std::async可以由系统根据当前资源,决定是否创建新线程,当系统资源很紧张的时候,创建新线程会导致程序崩溃。std::packaged_task是打包任务,或者说把任务包装起来的意思,是一个类模板,模板参数是各种可调用对象,将这些可调用对象包装起来,方便将来作为线程入口函数来调用:

int main() {
    cout << "main" << " threadid = " << std::this_thread::get_id() << endl;
    std::packaged_task<int(int)> mypt(mythread);  // 把函数mythread通过packaged_task包装起来
    std::thread t1(std::ref(mypt), 1);  // 线程直接开始执行,第二个参数作为线程入口函数的参数
    t1.join();  // 可以调用这个等待线程执行完毕,不调用这个不行,程序会崩溃
    std::future<int> result = mypt.get_future();  // std::future对象里含有线程入口函数的返回结果,这里用result保存mythread返回的结果
    cout << result.get() << endl;
    return 0;
}

包装lambda表达式:

std::packaged_task<int(int)> mypt([](int mypar) {
    cout << mypar << endl;
    cout << "lambda mythread() start" << " threadid = " << std::this_thread::get_id() << endl;
    std::chrono::milliseconds dura(5000);  // 1秒 = 1000毫秒,所以20000毫秒 = 20秒
    std::this_thread::sleep_for(dura);     //休息一定的时长
    cout << "lambda mythread() end" << " threadid = " << std::this_thread::get_id() << endl;
    return 15;
});

std::thread t1(std::ref(mypt), 1);
t1.join();  // 可以调用这个等待线程执行完毕,不调用这个不行,程序会崩溃
std::future<int> result = mypt.get_future();  // std::future对象里含有线程入口函数的返回结果,这里用result保存mythread返回的结果
cout << result.get() << endl;

std::packaged_task包装起来的对象也可以直接调用:

mypt(105);  // 可调用对象,直接调用。当然,这样写并没有创建什么新线程
std::future<int> result = mypt.get_future();

实际工作中,可能遇到std::packaged_task的各种用途,比如放到容器中,然后需要的时候取出来用:

vector<std::packaged_task<int(int)>> mytasks;
int main() {
    std::packaged_task<int(int)> mypt([](int mypar) {  // 创建或者叫包装一个任务
        cout << mypar << endl;
        cout << "lambda mythread() start" << " threadid = " << std::this_thread::get_id() << endl;
        std::chrono::milliseconds dura(5000);  // 1秒 = 1000毫秒,所以20000毫秒 = 20秒
        std::this_thread::sleep_for(dura);     // 休息一定的时长
        cout << "lambda mythread() end" << " threadid = " << std::this_thread::get_id() << endl;
        return 15;
    });
    // 入容器
    mytasks.push_back(std::move(mypt));  // 移动语义,这里要注意,入进去后mytp就empty了
    // 出容器
    std::packaged_task<int(int)> mypt2;
    auto iter = mytasks.begin();
    mypt2 = std::move(*iter);  // 用移动语义
    mytasks.erase(iter);  // 删除第一个元素,迭代器已经失效,不能再用
    mypt2(123);  // 直接调用。当然,这样写并没有创建什么新线程
    // 要取得结果,则还是要借助这个future
    std::future<int> result = mypt2.get_future();
    cout << result.get() << endl;
    return 0;
}

std::promise是一个类模板,这个类模板的作用是能够在某个线程中为其赋值,然后就可以在其它线程中把这个值取出来使用:

void mythread(std::promise<int>& tmpp, int calc) {  // 注意第一个参数
    cout << "mythread() start" << " threadid = " << std::this_thread::get_id() << endl;
    // 做一系列复杂操作
    calc++;
    calc *= 10;
    // 做其他运算,整个花费了5秒
    std::chrono::milliseconds dura(5000);
    std::this_thread::sleep_for(dura);

    // 终于计算出了结果
    int result = calc;  // 保存结果
    tmpp.set_value(result);  // 结果保存到了tmpp这个对象中
    cout << "mythread() end" << " threadid = " << std::this_thread::get_id() << endl;
}

int main() {
    cout << "main" << " threadid = " << std::this_thread::get_id() << endl;
    std::promise<int> myprom;  // 声明一个std::promise对象myprom,保存的值类型为int
    // 创建一个线程t1,将函数mythread及对象myprom作为参数放进去
    std::thread t1(mythread, std::ref(myprom), 180);
    t1.join();  // 等线程执行完毕,这个必须有,否则报异常,join放在.get后面也可以

    // 获取结果值
    std::future<int> fu1 = myprom.get_future();  // promise和future绑定用于获取线程返回值
    auto result = fu1.get();  // 获取值,但是这种get因为一些内部特殊操作,不能get多次,只能get一次	
    cout << "result = " << result << endl;	
    return 0;
}

就是说可以通过promise保存一个值,将来某个时刻通过一个future绑定到这个promise上来得到这个绑定的值。拿到这个值后,再将这个值传递到另一个线程:

void mythread2(std::future<int>& tmpf) {  // 注意参数
    auto result = tmpf.get();  // 获取值,只能get一次否则会报异常
    cout << "mythread2 result = " << result << endl;
    return;
}

std::thread t2(mythread2, std::ref(fu1));
t2.join();  // 等线程执行完毕

感觉就像是通过std::promise对象实现两个线程的数据传递。

八、future其他成员函数、shared_future与atomic

std::future还有很多方法,比如判断线程是否执行完毕,判断线程是否被延迟执行(而且是通过主线程而非创建子线程来执行):

int main() {
    cout << "main" << " threadid = " << std::this_thread::get_id() << endl;
    std::future<int> result = std::async(mythread);
    //std::future<int>  result = std::async(std::launch::deferred,mythread); //流程并不会卡在这里
    cout << "continue......!" << endl;
    //cout << result.get() << endl;  // 卡在这里等待线程执行完,但是这种get因为一些内部特殊操作(移动操作),不能get多次,只能get一次

    // future_status看成一个枚举类型
    std::future_status status = result.wait_for(std::chrono::seconds(1));  // 等待1秒,注意写法,但如果async的第一参数用了std::launch::deferred,则这里是不会做任何等待的,因为线程根本没启动(延迟)
    if (status == std::future_status::timeout) {
        // 超时线程还没执行完
        cout << "超时线程没执行完!" << endl;
        cout << result.get() << endl;  // 没执行完这里也要求卡在这里等线程返回
    } else if (status == std::future_status::ready) {
        // 线程成功返回
        cout << "线程成功执行完毕并返回!" << endl;
        cout << result.get() << endl;
    } else if (status == std::future_status::deferred) {
        // 如果async的第一个参数被设置为std::launch::deferred,则本条件成立
        cout << "线程被延迟执行!" << endl;
        cout << result.get() << endl;  // 上节说过,这会导致在主线程中执行了线程入口函数
    }
    return 0;
}

前面讲到,std::async不加额外参数或者额外参数是std::launch::async | std::launch::deferred,会让系统自行决定是否创建新线程从而会产生无法预知的潜在问题,问题的焦点在于如何确定异步任务到底有没有被推迟运行:

int main() {
    std::future<int> result = std::async(mythread);
    std::future_status status = result.wait_for(std::chrono::seconds(0));  // 可以写成0s,还支持ms(毫秒)写法
    if (status == std::future_status::deferred) {
        cout << "线程被延迟执行!" << endl;
        cout << result.get() << endl; //可以使用.get,.wait()来调用mythread(同步调用),会卡在这里等待完成
    } else {
        // 任务未被推迟,已经开始运行,但是否运行结束,则取决于任务执行时间
        if (status == std::future_status::ready) {
            // 线程运行完毕,可以获取结果
            cout << result.get() << endl;
        } else if (status == std::future_status::timeout) {
            // 线程还没运行完毕
            //......
        }
    }
    return 0;
}

上一节讲到packaged_task是将线程入口函数包装起来,然后创建线程mythread,用join等待线程结束,线程的执行结果其实就保存在result这个future对象中了。然后启动线程mythread2,在该线程中把future对象(也就是result)作为参数传递到线程中,而后在线程中调用future对象的get函数,拿到了线程mythread的返回结果。但需要说明的是,因为future对象的get函数被设计为移动语义,一旦调用get,就相当于把这个线程结果信息移动到result里面去了,所以再次调用get会报异常。如果多个线程都去调用get,程序肯定报异常。所以要用到std::shared_future,它的get函数是把数据进行复制(而不是转移):

void mythread2(std::shared_future<int>& tmpf) {  // 注意参数
    cout << "mythread2() start" << " threadid = " << std::this_thread::get_id() << endl;
    auto result = tmpf.get();  // 获取值,get多次没关系	
    cout << "mythread2 result = " << result << endl;
    return;
}

int main() {
    cout << "main" << " threadid = " << std::this_thread::get_id() << endl;
    std::packaged_task<int(int)> mypt(mythread);  // 把函数mythread通过packaged_task包装起来
    std::thread t1(std::ref(mypt), 1);  // 线程直接开始执行,第二个参数作为线程入口函数的参数
    t1.join();  // 调用这个等待线程执行完毕,不调用这个不行,程序会崩溃
    std::future<int> result = mypt.get_future();

    // valid,判断future对象里面的值是否有效
    bool ifcanget = result.valid();  // 没有被get过表示能通过get获取,则这里返回true
    // auto mythreadresult = result.get();  // 获取值,只能get一次否则会报异常
    // ifcanget = result.valid();  // future对象get过了,里边的值就没了,这个时候就返回false

    std::shared_future<int> result_s(std::move(result));  // std::move(result)也可以替换成result.share(),在没针对result调用get时,把result的内容弄到shared_future中来,此时future中空了
    ifcanget = result.valid();  // 因为result中空了,所以ifcanget为false了;这个时候不能再用result内容了
    ifcanget = result_s.valid();  // 因为result_s里有内容了,所以ifcanget为true了

    auto mythreadresult = result_s.get();
    mythreadresult = result_s.get();  // 可以调用多次,没有问题
    std::thread t2(mythread2, std::ref(result_s));
    t2.join();  // 等线程执行完毕	
    return 0;
}

可以在main函数中直接构造一个shared_future对象:

int main() {
    cout << "main" << " threadid = " << std::this_thread::get_id() << endl;
    std::packaged_task<int(int)> mypt(mythread);
    std::thread t1(std::ref(mypt), 1);
    t1.join();
    std::shared_future<int> result_s(mypt.get_future());  // 通过get_future返回值直接构造了一个shared_future对象
    auto mythreadresult = result_s.get();
    mythreadresult = result_s.get();  // 可以调用多次,没有问题
    std::thread t2(mythread2, std::ref(result_s));
    t2.join();  // 等线程执行完毕
    return 0;
}

在C++11中引入std::atomic类模板,叫做原子操作,可以理解成一种不需要用到互斥量加锁的多线程并发编程方式,一般指不可以分割的操作:

std::atomic<int> g_mycout = 0;  // 这是个原子整型类型变量;可以向使用整型变量一样使用
void mythread() {
    for (int i = 0; i < 10000000; i++) {  // 1千万
        // g_mycout++;   // 对应的操作就是原子操作,不会被打断
        // g_mycout+=1;  // 对应的操作就是原子操作,不会被打断
        g_mycout = g_mycout + 1;  // 这样写就不是原子操作了
    }
    return;
}

std::atomic并不是所有操作都是原子的,一般来讲,包含++、–、+=、-=、&=、|=、^=等简单运行算符的运算是原子的,其他的比较复杂的运算就不是原子的。实际工作中,原子操作可能比较多用来计数,比如累计发送出去多少个包等。下面这行代码,编译会出错:

atomic<int> atm;
atm = 0;
auto atm2 = atm;  // 不允许,编译时报语法错
atomic<int> atm3;
atm3 = atm;  // 不允许,编译时报语法错

这一行调用拷贝构造函数,应该是拷贝构造函数被delete了。可以用下面的方法实现:

atomic<int> atm5(atm.load());  // 这是可以的
atm5.store(12);

load是以原子的方式读atomic对象的值,store以原子的方式写入内容。

九、Windows临界区与其他各种mutex互斥量

Windows平台编程里面“临界区”的用法和互斥量完全相同:

#include <windows.h>
#define __WINDOWSLJQ__  // 宏定义

class A {
   public:
    A() {
#ifdef __WINDOWSLJQ__ 
        InitializeCriticalSection(&my_winsec);  // 初始化临界区
#endif
    }
    virtual ~A() {
#ifdef __WINDOWSLJQ__ 
        DeleteCriticalSection(&my_winsec);  // 释放临界区
#endif
    }
    void inMsgRecvQueue() {
        for (int i = 0; i < 100000; i++) {
            cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
#ifdef __WINDOWSLJQ__ 
            EnterCriticalSection(&my_winsec);  // 进入临界区
            msgRecvQueue.push_back(i);
            LeaveCriticalSection(&my_winsec);  // 离开临界区
#else
            my_mutex.lock();
            msgRecvQueue.push_back(i);
            my_mutex.unlock();
#endif
        }
    }
#ifdef __WINDOWSLJQ__ 
    // windows下叫临界区(类似于互斥量mutex)
    CRITICAL_SECTION my_winsec;
#endif	
};

同一线程,Windows临界区可以进入多次,就是说可以连续调用多次EnterCriticalSection(如果是不同线程,在上一条线程进入后还未退出时,会卡在进入临界区这行代码上),但退出时也要调用对应次数的LeaveCriticalSection,而互斥量则只能lock一次。可以写一个类,实现类似std::lock_guard<std::mutex>功能:

// 本类用于自动释放Windows下的临界区,防止忘记LeaveCriticalSection的情况发生,
// 类似于C++11中的std::lock_guard<std::mutex>功能
class CWinLock {
   public:
    CWinLock(CRITICAL_SECTION* pCritSect) {  // 构造函数
        m_pCritical = pCritSect;
        EnterCriticalSection(m_pCritical);
    }
    ~CWinLock() {  // 析构函数
       LeaveCriticalSection(m_pCritical);
    }
   private:
    CRITICAL_SECTION* m_pCritical;
};

CWinLock wlock(&my_winsec);
CWinLock wlock2(&my_winsec);  // 调用多次也没问题
msgRecvQueue.push_back(i);
// 离开作用域时不用手动LeaveCriticalSection

有人把CWinLock类相关的对象如上面的wlock、wlock2叫作RAII对象,CWinLock类也叫作RAII类,RAII翻译成中文是“资源获取即初始化”,这种技术的关键就是在构造函数中初始化资源,在析构函数中释放资源,典型的如智能指针、容器等都用到了这种技术。上面讲到c++11,一个互斥量不管是同一线程还是不同线程,只能lock一次,但有时候会遇到这种情况:

std::mutex my_mutex;
void testfunc1() {
    std::lock_guard<std::mutex> sbguard(my_mutex);
    testfunc2();  // 悲剧了,程序异常崩溃了
}
void testfunc2() {
    std::lock_guard<std::mutex> sbguard(my_mutex);
    //.......做另外一些事
}

这时候要用到recursive_mutex,叫作“递归的独占互斥量”,之前的std::mutex叫作“独占互斥量”。recursive_mutex允许同一线程多次调用同一个互斥量的lock成员函数:

std::recursive_mutex my_mutex;
void testfunc1() {
    std::lock_guard<std::recursive_mutex> sbguard(my_mutex);
    //.......做一些事
    testfunc2();
}
void testfunc2() {
    std::lock_guard<std::recursive_mutex> sbguard(my_mutex);
    //.......做另外一些事
}

std::timed_mutex是带超时功能的独占互斥量,std::recursive_timed_mutex是带超时功能的递归的独占互斥量。有了超时功能,就算拿不到锁头,也不会一直卡在拿锁那里。std::timed_mutex有两个独有的接口专门用来应对超时问题,一个是try_lock_for,一个是try_lock_until。try_lock_for是等待一段时间,如果拿到锁或者等待时间到了没拿到锁,流程都走下来:

std::timed_mutex my_mutex;
void inMsgRecvQueue() {
    for (int i = 0; i < 100000; i++) {
        std::chrono::milliseconds timeout(100);
        if (my_mutex.try_lock_for(timeout)) {  // 尝试获取锁,这里只等100毫秒
            // 在这100毫秒之内拿到了锁
            cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;			
            msgRecvQueue.push_back(i);
            // 用完了,还要解锁
            my_mutex.unlock();
        } else {
            // 这次没拿到锁就休息一下等待下次拿吧
            std::chrono::milliseconds sleeptime(100);
            std::this_thread::sleep_for(sleeptime);
        }
    }
}

try_lock_until的参数是一个时间点,代表一个未来的时间,在这个时间点没到来的时候,卡在那里等待拿锁,如果拿到了或者没拿到但到达了未来的那个时间,程序流程都走下来:

if (my_mutex.try_lock_until(chrono::steady_clock::now() + timeout))  // now:当前时间

std::recursive_timed_mutex就是允许同一个线程多次获取这个互斥量,它和std::timed_mutex两者的关系同上面讲解的std::mutex和std::recursive_mutex关系一样,可以直接把代码std::timed_mutex my_mutex改成std::recursive_timed_mutex my_mutex。

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值