C++并发编程:线程管理

1. 线程启动

线程在std::thread对象创建时启动,即,在构造std::thread对象时启动,为了能让编译器识别std::thread类,需要包含<thread>头文件。每一个线程都需要一个入口函数,因此构造线程的一个必不可少的参数,就是指明线程的入口函数。

1.1 普通函数作为线程入口

void thread_run();
std::thread my_thread{ thread_run };

//带有参数的普通函数,作为线程入口函数
void thread_run12(int i, std::string msg);
std::thread my_thread12{ thread_run12, 3, "msg"};

1.2 类成员函数作为线程入口

class background_task {
public:
    void thread_run(int i) {
        std::cout << __FUNCTION__<<std::endl;
    }
}

void thread_test() {
    background_task task{};
   //1.首先获取成员函数地址,然后传入参数即可,
   //2.成员函数的第一个参数默认是当前的类对象实例(this指针),因此需要传入该类对象实例
   //3.由于参数都是复制到新的线程内存空间中,如果改参数是一个类对象,则会调用拷贝构造函数复制一个新的对象
   //那么就导致成员函数的this指向的是这个新复制的对象,因此这里最好获取对象地址直接传递。
    std::thread t1{&background_task::thread_run, &task, 3}; //----> (1)
  
  //内部会拷贝构造出一个新的task对象,然后获取地址,传递给this
  std::thread t2{&background_task::thread_run, task, 3};//------> (2)
  
  //这里告诉thread传递的是一个引用,如果明确传递的是引用,其实内部和取地址是一样的同(1)
  std::thread t3{&background_task::thread_run, std::ref(task), 3}; 
}

这里需要注意,虽然(2)在一般情况下也能正常运行,但task作为线程参数被复制到新的线程内存空间中执行,因此,执行当前线程的background_task实例并不是原来的入参task

1.3 可调用类型对象作为线程入口

class background_task {
public:
    void operator()() const {
        std::cout << __FUNCTION__ << std::endl;
    }
}

void thread_test() {
    backgourn_task task{};
  
  //使用可调用对象和使用类对象原理差不多,不需要传递入口函数,默认的入口函数时operator()()
  //这里的task依然会复制到新的线程空间中执行
    std::thread my_thread{task};
  std::thread my_thread{&task}; //错误,因为被认为传递的是一个整数。
  
  //如果想要当前的类对象传递进线程空间可以使用引用
  std::thread my_thread{std::ref(task)};//正确
  
}

1.2原理一样,函数对象会复制到新线程的存储空间中,函数对象的执行和调用都在新线程的内存空间中进行。函数对象的副本应与原函数保持一致,否则得到的结果与我们的期望不同。

1.4 使用 lambda表达式作为线程入口

  std::thread t1{[]{
    std::cout << __FUNCTION__ << std::endl;
  } };

 std::thread t2{[]()->void {
        
 }};

启动了线程,你还需要明确的是:要等待线程执行结束,还是让其自主运行,如果std::thread对象销毁前还没有做出决定,程序就会终止(std::thread的析构函数会调用std::terminate())。因此,即便是有异常存在,也需要保证线程能够正确的join或者detached

如果不等待线程,就必须保证线程结束之前,可访问的数据有效性,这不是一个新问题,即便是在单线程代码中,对象销毁后再去访问,也会产生未定义的行为。不过线程的生命周期增加了这个问题的发生几率。

这种情况很可能发生在线程还没结束,函数已经退出的时候,这时线程函数还持有函数的局部变量或者引用。

1.5 重载函数作为函数入口

当线程的入口函数存在多个重载时,按着上面的方式,编译器无法确认应该将那个版本的函数作为入口函数,因此我们需要明确的告诉编译器。

void fun_run() {
}
void fun_run(int a, const std::string& name) {
}

void test() {
  std::thread t1{static_cast< void (*)() >(fun_run) };
  std::thread t2{static_cast<void (*)(int, const std::string&)>(fun_run),
                 10, std::string("abc")};
}

2. 向线程函数传递参数

2.1 传递普通变量

void thread_run(int a) {
  std::cout << " a =" << a << std::endl;
}
void thread_test() {
  int n = 3;
  std::thread my_thread{thread_run, n};
}

需要注意的是,参数是拷贝到线程独立内存中,即使是入口函数参数定义的是一个引用类型,也是如此。原理见2.2

2.2 传递引用

下面的代码是编译不过的

void thread_run(int& a) { //--->(1)
    a = 2;
}
void thread_test() {
  int n = 3;
  std::thread my_thread{thread_run, n}; //---->(2)
}

虽然thread_run()的参数是引用类型,但当编译器解析到(2)代码时,并没有信息告诉编译器thread_run()函数所需要的参数类型;因为此处执行是std::thread构造函数,my_thread()对于std::thread构造函数而言只是一个函数指针,因此当将变量n直接传入时,编译器认为thread_run()函数接受的是一个普通变量,因此就将n直接拷贝到线程空间中;而thread_run()在定义处 (1) 明确表明其参数的是一个引用,它告诉编译器:通过改变该参数a的值,可以回传给入参变量n(1)(2)传递给编译器的信息存在明显的矛盾,因此编译器不允许编写这样的代码。

通过上面的分析,知道了矛盾的所在,既然无法通过改变a回传给入参变量n,那直接将传出特性禁用掉,修改代码后,下面代码将正确执行。

void thread_run(const int& a) {
}
void thread_test() {
  int n = 3;
  std::thread my_thread{ thread_run, n };
}

如果我们真的需要在线程中实现引用传递该怎么做呢?在参数传递时,使用std::ref明确指定,我要传递的是引用

void thread_run(int& a) {
  a = 2;
}
void thread_test() {
  int n = 3;
  std::thread my_thread{ thread_run, std::ref(n) }; //--- (1)
}

2.3 传递指针

虽然指针也是复制到新的线程空间中,但是其复制的是内存地址。

void thread_run(int* i) {
  *i = 2;
}
void thread_test() {
  int a = 1;
  std::thread t{ thread_run, &a };
  std::cout << "a = " << a << std::endl;
  t.join();
}

2.4 传递类对象

class A {
    int a = 0;
public:
    A(int a_) : a(a_) {
         std::cout << "构造函数:" << this << "threadId =" << std::this_thread::get_id()<< std::endl;
    }

    A(const A &other) {
        std::cout << "拷贝构造函数:this =" << this << " other =" << &other << std::endl;
    }

    ~A() {
        std::cout << "析构函数:" << this << std::endl;
    }

    int get() const {
        return a;
    }
};

void func(int i, const A &a) {
     std::cout << "func 子线程Id:"<<std::this_thread::get_id() <<" a =" << a.get() << std::endl;
}

void test( ) {
        int i = 2;
    int b = 3;
    std::cout << "test start: threadId ="<< std::this_thread::get_id() << std::endl;
    std::thread t{func, 1, b};      //这一步仅仅是将变量b的内存值复制到子线程空间中,后续就不管了。
    t.detach();
    std::cout << "test end" << std::endl;
}

-------out-----
test start: threadId =34544
test end
构造函数:00000091AF6FFD60 threadId =32688
func 子线程Id:32688 a =3
析构函数:00000091AF6FFD60

从这里我们可以看出,子线程的创建过程,仅仅是从一个线程中需要的函数入口地址,函数参数等需要的条件复制到另一个线程空间中,然后启动该线程,剩下的事情就完全由子线程自己负责完成。在上述代码中,func需要一个A的类对象实例,而在线程入口处,传递的是一个整数,当b复制到新的线程空间后,调用匿名转换,构造出对象A

3. 转移线程的所有权

C++标准库中有很多资源占有类型,例如std::ifstream,std::unique_ptr,以及本篇中的std::thread,他们的对象不能够拷贝,但是可以移动。下面将展示一个例子,例子中创建了两个线程,并且在std::thread实例之间转移所有权。

void thread_run1(int i);
void thread_run2(int y);
void thread_test() {
  int a = 10;
  std::thread t1{thread_run1, a};
  std::thread t2 = std::move(t1);    // (1)
  t1 = std::thread{thread_run2, a}; // (2)
  std::thread t3{};         //(3)
  t3 = std::move(t2);       //(4)
  t1 = std::move(t3);       //(5) 赋值操作将使程序崩溃
}

当显示使用std::move创建t2(1)t1关联的线程的所有权就转移给了t2t1和线程执行已经没有关联了;执行thread_run1的线程现在与t2关联。

然后创建了一个临时的std::thread对象(2),启动了一个新线程,由于所有者是一个临时的对象,因此不需要显示的调用std::move(),移动操作将会隐式的调用。

t3使用默认构造的方式进行构造,没有与任何执行线程关联(3),调用std::move()将线程t2的所有权转移到t3(4),因为t2是一个命名对象,需要显示的调用std::move(), 移动操作完成后,t1与执行thread_run2()的线程相关联,t3与执行thread_run1()的线程相关联。

最后一个移动操作,将t3关联的线程所有权转移给t1,由于t1已经有了一个关联的线程,所以这里系统直接调用std::terminate(), 终止程序继续运行。这样做是为了保证与std::thread的析构函数行为一致。之前说过,需要在线程对象被析构前,显示的等待线程执行完成,或者将其分离;进行赋值时也需要满足这些条件(说明不能通过赋一个新值给std::thread()对象的方式来丢弃一个线程)。

std::thread支持移动操作,就意味着线程的所有权可以在函数外进行转移。

std::thread getThread1() {
  void thread_run();
  return std::thread{ thread_run };
}

std::thread getThread2() {
  void thread_run(int a);
  std::thread t{thread_run, 1};
  return t;
}

当线程所有权可以在函数内部转移,就允许std::thread实例可作为函数参数进行传递,代码如下:

void thread_run();
void trans_thread(std::thread t);
void test4() {
  trans_thread(std::thread{ thread_run });
  std::thread t{ thread_run };
  trans_thread(std::move(t));
}

当线程所有权被转移走后,就不能再对该std::thread实例执行join()或者detach()操作,否则将引发运行时异常。

4.获取线程并发数

std::thread::hardware_concurrency()这个函数将 返回能同时并发在一个程序中的线程数量,例如,在多核系统中返回CPU线程的数量。返回值也仅仅是一个提示,当系统信息无法获取时,函数也会返回0。但是,这无法掩盖这个函数对启动线程数量的帮助。

unsigned int num = std::thread::hardware_concurrency()

5. 获取线程标识

std::thread::id id = std::this_thread::get_id();
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,233评论 6 495
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,357评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,831评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,313评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,417评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,470评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,482评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,265评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,708评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,997评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,176评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,503评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,150评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,391评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,034评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,063评论 2 352

推荐阅读更多精彩内容