C++11 多线程编程概述

线程创建与参数传递

线程的创建

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;
}

线程启动函数的参数传递

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

基本对象作为参数

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

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的值。处了上文提及的传递参数的方式外,还可以使用std::bind关键字,如下:

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

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

类对象作为参数

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

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函数很少使用,因此本处先不做介绍。

猜你喜欢

转载自blog.csdn.net/qq_34561506/article/details/89234751