C++11 多线程编程概述

目录

线程创建与参数传递

线程的创建

线程启动函数的参数传递

基本对象作为参数

类对象作为参数

类内成员函数作为线程起始函数

detach()函数的缺点

线程锁

基本互斥锁mutex

std::lock_guard类模板

死锁的发生以及解决办法

std::unique_lock类

条件变量

线程函数返回值的获取

std::packaged_task类模板

异步线程的执行状态std::future_status

std::shared_future模板类

原子操作std::atomic


 


线程创建与参数传递

线程的创建

C++11中开始携带标准线程库,便于跨平台程序的移植与编写。一般情况下线程由函数进入,基本的线程创建方式如下:

#include "pch.h"
#include <iostream>
#include <string>
#include <thread>	//C++11线程头文件

using namespace std;

void fun()
{
    cout << "In child thread: " << std::this_thread::get_id() << endl;
}

int main()
{
    thread my_thread(fun);
    my_thread.join();
    cout << "In main thread: " << std::this_thread::get_id()<< endl;
}

程序运行结果为:

In child thread: 14768
In main thread: 10376

其中,线程由thread my_thread(fun)启动,join()函数的目的是让主线程等待子线程执行完毕,若子线程持续执行,主线程将在join()函数处阻塞直到子线程函数执行完毕。若想在主线程退出后子线程继续执行,可以将my_thread.join()函数替换为my_thread.detach()函数,此时子线程将由系统接管,在后台执行。但由于子线程有时会使用主线程资源,故在主线程结束后子线程有时无法运行或运行出错,所以一般不推荐使用detach函数。

一般来讲,线程创建代码的()内需要是可调用对象,除函数外还有lambda表达式,类内重载的()操作符等,如下:

#include "pch.h"
#include <iostream>
#include <string>
#include <thread>	//C++11线程头文件

using namespace std;

auto l = []()
{
    cout << "In child thread: " << std::this_thread::get_id() << endl;
};

class A{
public:
    void operator() () {
	    cout << "In child thread: " << std::this_thread::get_id() << endl;
    }
};

int main()
{
    //thread my_thread(l);
    A a;
    thread my_thread(a);
    my_thread.join();
    cout << "In main thread: " << std::this_thread::get_id()<< endl;
}

注意,使用std::thread创建的线程一定要使用join()或者detach()函数,不然程序会运行异常。

线程启动函数的参数传递

本处主要介绍基本类型参数与类对象参数的传递方式。

基本对象作为参数

基本对象的函数参数传递方式如下:

void fun(int a) {
    a++;
    cout << "in the child thread a is: " << a << endl;
}

int main()
{
    int a = 2;
    thread my_thread(fun, a);
    my_thread.join();
    cout << "In main thread a is: " << a << endl;
}

输出结果为:

in the child thread a is: 3
In main thread a is: 2

若我们想在线程函数内更改主线程定义的变量a的值,更改fun函数的形参为int &a,即使用变量a的引言,发现程序无法编译通过。必须改为const类型,如下:

void fun(const int &a) {    //形参为(int &a)无法编译通过
    //a++;  //由于const关键字的存在无法对a进行改变
    cout << "in the child thread a is: " << a << endl;
}

这是由于线程库为了安全考虑,强制限制了无法对线程函数的外部参数进行修改。若是仍然想使用引用的形式对外部传递参数进行修改,可使用std::ref关键字强制转换为引用类型,如下:

void fun(int &a) { 
    a++;
    cout << "in the child thread a is: " << a << endl;
}

int main()
{
    int a = 2;
    thread my_thread(fun, std::ref(a));
    my_thread.join();
    cout << "In main thread a is: " << a << endl;
}

输出结果为:

in the child thread a is: 3
In main thread a is: 3

可以看到此时在子线程内部对a值的改变同时影响了主线程中变量a的值。

类对象作为参数

相比与基本数据类型,类对象较为复杂,在参数传递时涉及到基本构造函数,拷贝构造函数的执行时间与执行方式,假设代码如下:

class A {
public:
    int m_i;
    A(int i) :m_i(i) {      //普通构造函数
        cout << "执行标准构造函数于线程: " << std::this_thread::get_id() << endl;
    }

    A(const A &a) :m_i(a.m_i) {     //拷贝构造函数
        cout << "执行拷贝构造函数于线程: " << std::this_thread::get_id() << endl;
    }
};

void fun(A a)
{
    cout << "子线程执行,a = " << a.m_i << " 子线程id为: " << std::this_thread::get_id() << endl;
}

int main()
{
    int i = 2;
    A a(i);
    thread my_thread(fun, a);
    my_thread.join();
    cout << "主线程id为: " << std::this_thread::get_id() << endl;
}

执行结果如下:

执行标准构造函数于线程: 9432    
执行拷贝构造函数于线程: 9432
执行拷贝构造函数于线程: 16912
子线程执行,a = 2 子线程id为: 16912
主线程id为: 9432

发现与基本函数不同的是,拷贝构造函数执行了两次,即在子线程与主线程分布执行了一次。而基本的函数参数为类对象时拷贝构造函数往往仅执行一次,这就说明在线程开始时,额外执行了一次拷贝构造函数,修改代码为如下:

class A {
public:
    int m_i;
    A(int i) :m_i(i) {      //普通构造函数
        cout << "执行标准构造函数于线程: " << std::this_thread::get_id() << endl;
    }

    A(const A &a) :m_i(a.m_i) {     //拷贝构造函数
        cout << "执行拷贝构造函数于线程: " << std::this_thread::get_id() << endl;
    }
};

void fun(const A &a)
{
    cout << "子线程执行,a = " << a.m_i << " 子线程id为: " << std::this_thread::get_id() << endl;
}

int main()
{
    int i = 2;
    A a(i);
    thread my_thread(fun, a);
    my_thread.join();
    cout << "主线程id为: " << std::this_thread::get_id() << endl;
}

函数fun的参数为类对象的引用,执行结果如下:

执行标准构造函数于线程: 8304
执行拷贝构造函数于线程: 8304
子线程执行,a = 2 子线程id为: 9176
主线程id为: 8304

可以看到使用引用后,主线程内仍然执行了一次拷贝构造函数,即子线程内的对象a还是主线程内a的一个副本,且由于const的存在无法对其进行修改。若仍想要对a副本的内容进行修改,可以使用mutable关键字,如下:

class A {
public:
    mutable int m_i;
    A(int i) :m_i(i) {      //普通构造函数
        cout << "执行标准构造函数于线程: " << std::this_thread::get_id() << endl;
    }

    A(const A &a) :m_i(a.m_i) {     //拷贝构造函数
        cout << "执行拷贝构造函数于线程: " << std::this_thread::get_id() << endl;
    }

    void change() const
    { m_i++; }
};

另外,与基础类型类似,可以使用std::ref关键字来进行强制引用处理,不执行拷贝构造函数,子线程可直接对主线程内对象进行修改。

类内成员函数作为线程起始函数

线程起始函数可以为类内成员函数,如下:

class A {
public:
    int m_i;
    A(){}
    A(int i) :m_i(i) {      //普通构造函数
        cout << "执行标准构造函数于线程: " << std::this_thread::get_id() << endl;
    }

    A(const A &a) :m_i(a.m_i) {     //拷贝构造函数
        cout << "执行拷贝构造函数于线程: " << std::this_thread::get_id() << endl;
    }
    void fun(int a, int b){}

};

int main()
{
    int i = 2;
    int p,q = 0;
    A a(i);
    thread my_thread(&A::fun, &a, p, q); //类对象为引用,确保线程开始不执行类的拷贝构造函数
    my_thread.join();
    cout << "主线程id为: " << std::this_thread::get_id() << endl;
}

detach()函数的缺点

上文提到过,detach函数会使子线程脱离主线程,由系统接管在后台执行。然而很多场合子线程会使用主线程的资源来正常运行,特别当线程起始函数传递的参数为主线程变量的引用时,主线程结束后其内部变量将会被销毁,导致子线程也无法正确执行。另外,当线程起始函数为类成员函数时,由于使用了类对象的引用,主函数注销后此类对象内存会被释放,detach会使程序发生各种错误。实际过程中detach函数很少使用,因此本处先不做介绍。

线程锁

基本互斥锁mutex

线程锁一般用在多个线程同时访问同一数据时,特别是在需要对此数据进行修改时,不对待访问数据加锁会使程序出现错误。此处主要以下面的程序为例介绍线程锁的基本工作方式。两线程同时对一个队列内的数据进行读写并打印结果:

#include <iostream>
#include <string>
#include <thread>	//C++11线程头文件
#include <queue>

using namespace std;

class Task {
public:
    std::queue<int> taskQue;
    void getTask()
    {
        for (int i = 0; i < 1000; i++)
        {
            taskQue.push(i);
        }
    }

    void dealTask()
    {
        for(int i = 0; i < 1000; i++)
        {
            if (!taskQue.empty())
            {
                int task = taskQue.front();
                cout << "task is: " << task << endl;
                taskQue.pop();
            }
            else
            {
                cout << " task queue is empty..." << endl;
            }
        }
    }
};

int main()
{
    Task t;
    thread pushThread(&Task::getTask, &t);
    thread popThread(&Task::getTask, &t);

    pushThread.join();
    popThread.join();

    return 0;
}

运行时发现上文的程序存在问题,这是由于两线程同时对队列内数据进行读写造成的,此种情况可以使用mutex互斥锁修改程序,通过对待访问变量加锁、解锁实现双线程程序执行,使用lock与unlock函数完成:

class Task {
public:
    std::queue<int> taskQue;
    std::mutex myMutex;

    void getTask()
    {
        for (int i = 0; i < 1000; i++)
        {
            myMutex.lock();    //获取锁
            taskQue.push(i);
            myMutex.unlock();  //释放锁
        }
    }

    void dealTask()
    {
        for(int i = 0; i < 1000; i++)
        {
            myMutex.lock();    //获取锁
            if (!taskQue.empty())
            {
                int task = taskQue.front();
                cout << "task is: " << task << endl;
                taskQue.pop();
                myMutex.unlock();  //释放锁 
            }
            else
            {
                cout << " task queue is empty..." << endl;
                myMutex.unlock();
            }
        }
    }
};

std::lock_guard类模板

std::lock_guard类模板可以自行加锁与解锁,即代替使用mutex类的lock与unlock函数,上文中获取任务的代码可以替换为:

std::lock_guard<std::mutex> myGuard(myMutex); 
//std::lock_guard<std::mutex> myGuard(myMutex, std::adopt_lock); //若互斥量已经加锁
taskQue.push(i);

这是由于myGuard类会在执行构造函数时执行lock操作,执行析构函数时执行unlock操作。另外std::lock_guard构造函数提供了std::adopt_lock参数,表示若在程序上文已经对互斥量进行加锁,此时将不再进行第二次加锁操作,仅会在执行构造函数时解锁。

死锁的发生以及解决办法

死锁一般发生在存在多个互斥量的情况下,互斥量A等待B解锁才能释放A中占有的锁,互斥量B等待A解锁才能释放B中占有的锁,如下所示:

class Task {
public:
    std::queue<int> taskQue;
    std::mutex myMutex1;
    std::mutex myMutex2;

    void getTask()
    {
        for (int i = 0; i < 1000; i++)
        {
            myMutex1.lock();
            myMutex2.lock();
            taskQue.push(i);
            myMutex2.unlock();
            myMutex1.unlock();
        }
    }

    void dealTask()
    {
        for(int i = 0; i < 1000; i++)
        {
            //std::lock(myMutex1, myMutex2)           //同时对两个互斥量进行加锁
            myMutex2.lock();    //myMutex1.lock();    解除死锁状态
            myMutex1.lock();    //myMutex2.lock();  
            if (!taskQue.empty())
            {
                int task = taskQue.front();
                cout << "task is: " << task << endl;
                taskQue.pop();
                myMutex1.unlock();
                myMutex2.unlock();
            }
            else
            {
                cout << " task queue is empty..." << endl;
                myMutex1.unlock();
                myMutex2.unlock();
            }
        }
    }
};

解决死锁的办法很简单,仅需改变上文中两线程内加锁的顺序一致即可,或使用std::lock同时对两个互斥量进行加锁。

std::unique_lock类

std::unique_lock类与std::lock_guard类功能类似,区别在于前者使用方式更加灵活,但相对应的执行效率较慢,std::unique_lock

类的基本用法如下:

//与std::lock_guard类功能相同
std::unique_lock<std::mutex> uniqLock(myMutex); 

//默认程序上文已经对互斥量加锁
std::unique_lock<std::mutex> uniqLock(myMutex, std::adopt_lock);  

//对互斥量进行加锁,若加锁失败则直接返回,不会阻塞程序
std::unique_lock<std::mutex> uniqLock(myMutex, std::try_to_lock);
uniqLock.owns_lock();    //判断是否成果获取锁,是则返回true,否则返回false

//不会进行加锁,仅在析构时解锁
std::unique_lock<std::mutex> uniqLock(myMutex, std::defer_lock);

//std::unique_lock的一些成员函数
uniqLock.lock();    //加锁
uniqLock.unlock();  //解锁;
uniqLock.release();   //释放与互斥量myMutex的关系


//转换绑定的互斥量
std::unique_lock<std::mutex> uniqLockNew(std::move(uniqLock));

条件变量

对于上面描述的基本双线程代码,程序需要不断的判断队列是否为空,一定程度上会影响算法的执行效率,可使用条件变量对其进行改造,基本的条件变量用法如下:

class Task {
public:
    std::queue<int> taskQue;
    std::mutex myMutex;
    std::condition_variable myCond;

    void getTask()
    {    
        for (int i = 0; i < 1000; i++)
        {
            std::unique_lock<std::mutex> myUniqLock(myMutex);
            taskQue.push(i);
            myCond.notify_one();
        }
    }

    void dealTask()
    {
        for(int i = 0; i < 1000; i++)
        {
            std::unique_lock<std::mutex> myUniqLock(myMutex);
            myCond.wait(myUniqLock, [this]() {
                if (!taskQue.empty())    return true;
                return false;
            });
            int task = taskQue.front();
            cout << "task is: " << task << endl;
            taskQue.pop();
        }
    }
};
std::mutex myMutex;
std::condition_variable myCond; //创建条件变量

std::unique_lock<std::mutex> myUniqLock(myMutex);  //创建独占锁,仅独占锁可以使用条件变量

myCond.wait(myUniqLock, [this]() {
      if (!taskQue.empty())    return true;
      return false;
});    //第一个参数为独占锁,第二个参数一般为一个可调用对象,本处使用lambda表达式
       //当可调用对象返回true时,wait函数直接返回,无作用
       //当可调用对象返回false时,wait函数将释放互斥量,并等待notify函数将其唤醒
       //唤醒后继续检查可调用对象返回值,返回true时将尝试获取对互斥量加锁,成功后wait函数返回
       //若唤醒后的可调用对象返回值依旧为false,则继续等待下一次唤醒

myCond.notify_one()    //唤醒一个正在wait的线程
myCond.notify_all()    //唤醒多个线程

线程函数返回值的获取

上文提到的线程入口函数均为void类型,无返回值,若想得到线程入口函数的返回值,可以使用C++11提供的std::async函数模板与std::future类模板,基本作用与使用方式如下:

std::async是一个函数模板,启动一个异步任务,任务完成后返回一个std::future类型对象。

std::future:是一个类模板,其包含了线程入口函数所返回的结果,可以通过调用其成员函数get()来获取具体返回值。

#include "pch.h"
#include <iostream>
#include <string>
#include <thread>	  //C++11线程头文件
#include <mutex>
#include <future>   //async与future头文件

using namespace std;

int my_thread(int a, int b)
{
    cout << "this thread id is: " << std::this_thread::get_id() << endl;
    return a + b;
}

int main()
{
    int a = 10;
    int b = 10;
    std::future<int> result = std::async(my_thread, a, b);  //创建异步线程
    int sum = result.get();  //阻塞直到子线程执行完成,get函数仅能调用一次
    //result.wait()  //阻塞直到子线程执行完成,不获取返回值
    cout << "this thread id is: " << std::this_thread::get_id() << " result is: "<<sum<<endl;
    return 0;
}

值得注意的是,当线程函数执行时间较长时,即主线程调用get()函数时my_thread函数尚未执行完成,程序会在get()函数处阻塞,直到子线程返回。另外,get()函数仅能调用一次,无法重复调用。若不调用get函数,程序也会在主线程执行完毕之前阻塞,等待子线程结束才完全退出

std::async函数也支持std::launch类型的参数,来控制子线程的执行状况,如下:

//延迟创建线程
std::future<int> result = std::async(std::launch::deferred my_thread, a, b); 

std::launch::deferred参数的功能是是线程函数延迟调用,当future类对象执行get函数时才开始调用线程函数,值得注意的是,这时此函数为主线程调用,即没有开辟新的线程,不属于多线程执行。被延迟执行的函数需要调用result.get()才开始执行。

另一个参数是std::launch::async,其与std::async的默认调用形式相同。当两参数均被使用,即为std::launch::async | std::launch::deferred时,系统将在以上两种情况内自行选择。

std::packaged_task类模板

std::packaged_task类模板的作用是将可调用对象封装,可以使用future来调用其结果,对于上文的程序,可以改写为:

std::packaged_task<int(int, int)> mypt(my_thread);
std::thread t(std::ref(mypt), a, b);
t.join();    //必须调用join或detach
//mypt(a,b);    //mypt也是一个可调用对象,可以直接使用,不开辟新的线程
std::future<int> result = mypt.get_future();
int sum = result.get();

异步线程的执行状态std::future_status

对于上文提到的基本异步程序,子线程函数在很短的时间内即可执行完成,但若子线程函数计算量较大,主线程该如何获取子线程的执行状态呢,这就需要枚举类型std::future,其基本使用方式如下:

int my_thread(int a, int b)
{
    cout << "my_thread start()"<<" this thread id is: " << std::this_thread::get_id() << endl;
    std::chrono::seconds dur(5);
    std::this_thread::sleep_for(dur);   //线程等待5秒
    cout << "my_thread end()" << " this thread id is: " << std::this_thread::get_id() << endl;
    return a + b;
}

int main()
{
    int a = 10;
    int b = 10;
    std::future<int> result = std::async(my_thread, a, b);
    std::future_status status = result.wait_for(std::chrono::seconds(4));   //等待子线程执行4秒,并返回状态
    int sum{};
    if (status == std::future_status::timeout)      //等待完成后,子线程尚未执行完成
    {
        cout << "my_thread() time out..." << endl;
    }
    else if(status == std::future_status::ready)    //等待完成后,子线程已经执行完成,可以获取子线程结果
    {
        cout << "my_thread() get ready..." << endl;
        sum = result.get();
    }   
    else if (status == std::future_status::deferred)    //此状况仅在std::async函数使用std::launch::deferred参数时发生,很少使用
    {
        cout << "can not create new thread..." << endl;
    }
    cout << "main thread end()" << " this thread id is: " << std::this_thread::get_id() << " result is: "<<sum<<endl;
    return 0;
}

std::shared_future模板类

上文提到过,使用std::future获取的异步线程执行函数结果仅可以使用get()函数执行一次,但当多个线程或多个函数同时需要此结果时,就行需要使用std::shared_future类,此类获取的线程执行结果可以使用get()函数进行多次调用提取。

 std::future<int> result = std::async(my_thread, a, b);
 std::shared_future<int> result_s1(std::move(result));
 std::shared_future<int> result_s2(std::async(my_thread, a, b));

 std::packaged_task<int(int, int)> mypt(my_thread);
 std::thread t(std::ref(mypt), a, b);
 t.join();    //必须调用join或detach
 std::shared_future<int> result_s3(mypt.get_future());

原子操作std::atomic

原子操作的应用与互斥量类似,都是为了保护多线程对同一变量与代码段的访问,但相比于前者,原子操作仅能应用于单一变量,且相比于频繁的使用lock与unlock,效率较高。

std::atomic其实是一个模板类,对此类变量执行的操作都是原子操作,即当一个线程对其进行访问或者修改时,其他线程无法干扰,考虑如下情况,g_mcount++就是一个原子操作,两线程同时执行时不会发生干扰。

using namespace std;

std::atomic<int> g_mcount;
std::mutex m_mutex;

void my_thread()
{
    for (int i = 0; i < 100000; i++)
    {
        //m_mutex.lock();   //使用原子操作后不需要再进行加锁与解锁
        g_mcount++;
        //m_mutex.unlock();
    }
}

int main()
{
    std::thread t1(my_thread);
    std::thread t2(my_thread);

    t1.join();
    t2.join();
    cout << "g_mcount is: " << g_mcount << endl;
    return 0;
}

其他类型锁

除基本的类型锁外,c++11还提供了其他类型的互斥量,用于处理不同的应用场合,如下:

std::timed_mutex m_time_mutex;  //提供超时功能的互斥量,无法获取互斥量时不会一直等待
std::chrono::seconds dur(5);    //延迟5s的时间
m_time_mutex.try_lock_for(dur); //若获取不到互斥量时,将等待5s,否则程序直接返回false,若在5s内成果获取互斥量,返回true
m_time_mutex.try_lock_until(std::chrono::steady_clock::now()+dur);  //与try_lock_for功能相似,获取锁知道系统时间为参数时间
std::recursive_mutex m_re_mutex; //可以多次重复加锁,此类型互斥量也可以封装在lock_guard或unique_lock类内。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值