c++并发编程(concurrency)----线程管理

  • 启动线程,指定线程运行函数的多种方法
  • 等待线程执行完毕
  • 唯一辨别线程的方法
    如果已经有兴趣启动多线程程序了,那么问自己个问题,如何启动多线程?如何检查多线程执行结束?带着疑问开启我们的多线程之旅。

基础线程管理

每个C++程序都至少有一个线程,由C++运行时库(runtime)启动,线程执行函数main()。当然你的程序也可以启动其他的线程并指定其他的线程入口函数。这些线程可以与初始线程并发运行。类似程序main函数执行完成退出,线程执行函数执行完成返回时线程执行完毕退出。std::thread对象启动一个线程,可以等待它执行完成,第一步先启动线程。

启动线程

  1. 简单线程执行函数,无参/无返回值
  2. 带参线程执行函数,可执行独立操作并等待某些消息系统信号
    请谨记要包含编译器可看到std::thread 类的定义
    跟C++Standard Library 一样,std::thread 可以跟任何(any callable type)可调用类型配合工作,因此你可以传递一个重载调用操作符的函数对象(a class with a function call operator )
class background_task{public:void operator() () const{do_something();do_something_else();}
};
background_task f;
std::thread my_thread(f);

完整示例代码

#include <iostream>
#include <thread>void do_something() {std::cout << "uniview test ." << std::endl;
}void do_something_else() {std::cout << "uniview test else function ." << std::endl;
}class background_task {public:void operator() () const {do_something();do_something_else();}
};
int main()
{background_task f;std::thread my_thread(f);my_thread.join();
}

请注意,std::thread my_thread(background_task());语句不符合语法规则,可验证编译器提示错误,gnu c++编译器报错提示如下:

错误:对成员‘join’的请求出现在‘my_thread’中,而后者具有非类类型‘std::thread(background_task (*)())’
问题原因:One thing to consider when passing a function object to the thread constructor is to avoid what is dubbed “C++’s most vexing parse.”If you pass a temporary rather a named variable, then the syntax can be the same as that of a function declara-tion, in which case the compiler interprets it as such, rather than an object definition.
中文释义:当传递函数对象给线程构造函数时请避免让C++费解的语法。当你传递临时对象而非具名对象时,语法解释同样可理解为是一个函数声明,而非一个对象定义。

解决方法有2个:

  1. 临时对象外加括号

std::thread my_thread((background_task()));
额外的括号可阻止编译器解释为函数声明,因此允许my_thread被定义为std::thread变量

  1. 用初始化列表语法

std::thread my_thread{background_task()};
用最新语法{}初始化列表而非(),直接表示定义一个变量无歧义

  1. 另外一种避免此问题的可调用对象 a lambda expression,它是c++11的新特性

lambda允许写本地函数,可以捕获本地变量,因此替代传递参数的需求

std::thread_my_thread([](do_something();do_something_else();
))

线程一旦启动则需要显示决定是否等待线程结束

如果线程启动销毁时没有决定是join()还是detach(),你的程序会异常终止。the std::thread destructor calls std::terminate() 析构函数会调用中止函数。
因此保证线程被正确的join()或者detach()是至关重要的,即使是异常发生时也要正确回收处理。
join()或者detach()一定要在线程销毁前确认完毕,detach()会持续运行很长时间(if you detach it, then the thread may continue running long after the std::thread object is destroyed)。
若不等待线程执行结束,则需要确保线程在执行期间访问数据的有效性,此关注点不是一个新问题,在单线程代码中若要访问一个销毁资源的行为也会出现未定义的行为。不过使用多线程给这种变量生存周期访问的问题更多的机会。
场景举例说明

  1. 线程执行函数持有局部变量的引用或者指针,局部变量所在函数执行结束但线程运行暂未结束。
    代码示例说明 A function that returns while a thread still has access to local variables
struct func
{int& i;func(int& i_):i(i_){}void operator()(){for(unsigned j=0;j<1000000;++j){do_something(i); //Potential access to dangling reference}}
};
void oops()
{int some_local_state=0;func my_func(some_local_state);std::thread my_thread(my_func);my_thread.detach(); //Don’t wait for thread to finish
} //New thread might still be running

解决上述代码问题,最简单的就是拷贝一份数据至线程执行函数而不是使用共享数据。对于可调用对象也是拷贝一份至新启动的线程,但仍然要小心可调用对象是否包含局部变量的指针或者引用。创建线程时使用局部变量是一个坏主意,除非能保证线程执行退出时局部变量一直有效。

等待线程执行结束

如果需要等待线程执行结束,可在线程对象上执行join()等待std::thread 对象执行完成,如果需要细粒度的控制线程,比如检查线程是否执行完成或者等待一定的时间,可以通过条件变量或者future(期望),后面会讲到。 join() 真实动作是清理线程的所有资源,join()完成则线程对象与线程无关了,也有任何其他线程无关了。因此joins仅可对一个线程对象执行一次,如果你调用完成join()完成,std::thread对象则不再可以joinable(),调用会返回false。

waiting in exceptional circumstances

启动立即detach()则不用担心异常发生,若join()等待则需要确保异常发生时可正常join(),比如异常捕获处理位置无真正的join(),为了避免异常发生时程序被异常中止,除了正常处理流程在异常发生时也要处理。

//Waiting for a thread to finish
struct func;
void f()
{int some_local_state=0;func my_func(some_local_state);std::thread t(my_func);try{do_something_in_current_thread();}catch(...){t.join();throw;}t.join();
}

上述的try {} catch(…)处理语句繁杂冗长,解决上述的问题有个解决方案,One way of doing this is to use the standard Resource Acquisition Is Initialization (RAII) idiom and provide a class that does the join() in its destructor。资源获取即初始化在析构时进行线程回收。

//Using RAII to wait for a thread to complete
class thread_guard
{std::thread& t;
public:explicit thread_guard(std::thread& t_):t(t_){}~thread_guard(){if(t.joinable()) {t.join(); }}thread_guard(thread_guard const&)=delete; thread_guard& operator=(thread_guard const&)=delete;
};
struct func;
void f()
{int some_local_state=0;func my_func(some_local_state);std::thread t(my_func);thread_guard g(t);do_something_in_current_thread();
}

无论因何种原因f()函数执行退出时,thread_guard 析构函数都会等待线程执行完毕退出,哪怕do_something_in_current_thread()有异常抛出。
在join()前调用joinable()很重要,因为在线程退出时仅可调用一次join()进行资源回收,若进行多次join()则会由异常发生(可自测)。
拷贝构造和拷贝赋值被标注为=delete确保它们不会被编译器自动提供。拷贝构造或者赋值线程对象是危险的,因为它可能会超出join()的工作范围。
如果不想等待线程执行结束,避免异常安全问题处理可使用detach()方法,分离线程执行对象的关联关系,线程对象在被销毁时std::terminate()函数也不会被调用,即使线程仍然在后台运行。

线程在后台运行

调用detach() 线程执行函数与线程对象分离,没有线程对象与之关联,因此也不需要join()。 detached threads真正在后台运行,所有权归属以及控制权交由 C++ Run-time library 运行时库将保证线程使用的资源在退出时可正确回收。
detach threads 被称为daemon threads (守护进程)类似于 UNIX 守护进程的概念在后台运行没有显示接口提供用户使用。这种线程典型的特征,长期运行/比如在引用程序运行整个声明周期,执行监视文件系统的后台程序,在后台清理不使用的缓存中的实体对象或者优化数据结构。
detach(0调用完成 线程对象与线程执行函数则无关联,因此也不再joinable。

std::thread t(do_background_work);
t.detach();
assert(!t.joinable());

调用detach()的前提条件是线程对象是joinable(),即线程对象有线程执行函数,因此可通过准确的方法判断 std::thread t 当t.joinable() returns true 才可调用t.detach().

传递参数给线程执行函数

请谨记:默认参数传递时拷贝至线程函数内部,它们可以被线程执行函数访问,即使函数传递时是通过引用,也会拷贝一份,示例代码如下:

void f(int i,std::string const& s);
std::thread t(f,3,”hello”);

f()的第二个参数 std::string const& s,传递了一个char const* 转换成std::string仅在新线程中存在。尤其需要关注的 如果传递的是一个局部变量的指针,代码如下:

void f(int i,std::string const& s);
void oops(int some_param)
{char buffer[1024];
sprintf(buffer, "%i",some_param);
std::thread t(f,3,buffer);
t.detach();
}

上述代码有个特别严重的风险,oops()执行完成退出时此时新线程还未执行buffer 拷贝至std::string 因此会出现拷贝一个销毁的局部变量的操作,会导致未定义的行为。对应的解决方法是提前将局部变量拷贝至std::string对象而不是将buffer变量传递给std::thread 构造函数。代码如下:

void f(int i,std::string const& s);
void not_oops(int some_param)
{char buffer[1024];
sprintf(buffer,"%i",some_param);
std::thread t(f,3,std::string(buffer)); //Using std::string avoids dangling pointer
t.detach();
}

上述解决方法是解决了依赖隐式转换指针buffer到std::string对象,现在线程构造函数直接拷贝std::string临时对象不需要类型转换。此处的实现都是拷贝一份,如果你想更新传递的变量对象,怎么办呢?比如下面的代码:

void update_data_for_widget(widget_id w,widget_data& data);
void oops_again(widget_id w)
{widget_data data;
std::thread t(update_data_for_widget,w,data);
display_status();
t.join();
process_widget_data(data);
}

尽管update_data_for_widget()第二个参数是引用类型但是std::thread构造函数并不知道;构造时显示的调用函数期望的类型和拷贝一份提供的value。当调用process_widget_data传递的是一份内部临时对象拷贝的引用而非数据本身的引用。因此当线程执行结束时,更新将被忽略因内部临时对象拷贝数据将被销毁,因此data数据将得不到更新。问题类似于std::bind 解决方法也简单直接,需要将参数由std::ref包裹,比如本示例修改如下:

std::thread t(update_data_for_widget,w,std::ref(data));

修改代码可实现意图传递参数的引用至函数内部,而不是一份临时对象的拷贝。
如果对std::bind语法熟悉则对上述解决方法不会感到惊讶,因此std::thread 与 std::bind处理机制一样,因此你也可以传递一个类的成员函数指针给线程对象,示例代码如下:

class X
{public:
void do_lengthy_work();
};
X my_x;
std::thread t(&X::do_lengthy_work,&my_x);

你也可以传递参数给类的成员函数,放在构造函数的第3个参数的位置,因此顺序如下:

  1. 类成员函数指针
  2. 类对象
  3. 类成员函数第一个参数
  4. 类成员函数第二个参数
  5. 以此类推
    另外一种有趣的语法,参数不能被拷贝仅支持移动,(move),对象可以从一个对象移动到另外一个对象,原始的对象变为"empty"。移动语义支持函数参数以及返回值,临时对象会自动调用移动语义,显示定义变量则需要显示调用std::move
    ()。示例代码如下:
void process_big_object(std::unique_ptr<big_object>);
std::unique_ptr<big_object> p(new big_object);
p->prepare_data(42);
std::thread t(process_big_object,std::move(p));

std::thread是支持移动的,但不支持拷贝,这可以确保一个线程仅有一个线程对象关联,但是线程对象之间所有权可以转移。

线程所有权转移

如果需要进行线程所有权转移,std::thread转移功能是支持的。C++标准库比如std:ifstream / std::unique_ptr均是支持移动不支持拷贝,std::thread也是如此。

void some_function();
void some_other_function();
std::thread t1(some_function);
std::thread t2=std::move(t1);
t1=std::thread(some_other_function);
std::thread t3;
t3=std::move(t2);
t1=std::move(t3); // This assignment will terminate program!
  1. t1将所有权转移给t2, std::move显示调用转移,t1与线程执行函数不再有关联,线程执行函数与t2关联
  2. 线程临时对象赋值给t1,此处无需std::move显示调用,moving动作对于临时对象是自动且进行隐式转换;
  3. t3是默认线程构造函数创建,它不与任何线程执行函数相关,t2将线程转移给t3,此处调用std::move,因t2是具名对象不能自动调用隐式转换;
  4. 当前状态t1与some_other_function线程执行函数关联,t2与任何线程无关,t3与初始线程some_function()线程关联
  5. t3将所有权转移给t1此动作非法,想想是因为什么??? t1当前与some_other_function线程关联,强行移动会被s td::terminate()异常中止。这样做的目的是可以保证线程destructor析构时一致性。前文已多次讲到必须显示指定是等待线程执行完成还是detach分离它,且要在线程触发析构前,此要求同样适用于赋值:你不能这样you can’t just “drop” a thread by assigning a new value to the std::thread object that manages it。通过赋值新的线程对象丢弃。
//Returning a  std::thread from a function
std::thread f()
{void some_function();
return std::thread(some_function);
}
std::thread g()
{void some_other_function(int);
std::thread t(some_other_function,42);
return t;
}
//scoped_thread and example usage
class scoped_thread
{std::thread t;
public:
explicit scoped_thread(std::thread t_):
t(std::move(t_))
{if(!t.joinable())
throw std::logic_error(“No thread”);
}
~scoped_thread()
{t.join();
}
scoped_thread(scoped_thread const&)=delete;
scoped_thread& operator=(scoped_thread const&)=delete;
};
struct func;
void f()
{int some_local_state;
scoped_thread t(std::thread(func(some_local_state)));
do_something_in_current_thread();
}

此处实现scoped_thread直接将新线程赋值给它而非创建线程对象再赋值,且在f()函数执行完成会触发scoped_thread析构,等待线程执行完成担心线程创建失败可在构造函数中添加判断线程创建失败抛出异常。

//Spawn some threads and wait for them to finish 简单线程池
void do_work(unsigned id);
void f()
{std::vector<std::thread> threads;
for(unsigned i=0;i<20;++i)
{threads.push_back(std::thread(do_work,i)); //Spawn threads
}
std::for_each(threads.begin(),threads.end(),std::mem_fn(&std::thread::join));//Call join() on each thread in turn
}

可以通过创建一批线程进行统一管理,同时也可在运行时选择线程数量。
获取硬件支持并发数, std::thread::hardware_concurrency()。

此处留个问题,如何获取线程执行完成的返回结果???

识别线程

std::thread::id 通过 get_id()获取线程id,如果线程没有与之关联的线程执行函数则返回 std::thread::id默认构造函数创建的对象,显示“not any thread”。
std::this_thread::get_id()获取线程ID ,std::thread::id线程ID可以自由的拷贝/比对,比如通过比较id可判断是不是相同的线程。
标准库提供std::hash < std::thread::id>
线程ID一般用于查找指定线程做特殊处理,示例代码如下:

std::thread::id master_thread;
void some_core_part_of_algorithm()
{if(std::this_thread::get_id()==master_thread)
{do_master_thread_work();
}
do_common_work();
}

c++并发编程(concurrency)----线程管理相关推荐

  1. [转]Java并发编程:线程池的使用

    Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了, ...

  2. Java并发编程一线程池简介

    推荐:Java并发编程汇总 Java并发编程一线程池简介 为什么我们需要使用线程池? 我们知道线程是一种比较昂贵的资源,我们通过程序每创建一个线程去执行,其实操作系统都会对应地创建一个线程去执行我们的 ...

  3. Java并发编程:线程池的使用

    在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统 ...

  4. 【Java 并发编程】线程池机制 ( ThreadPoolExecutor 线程池构造参数分析 | 核心线程数 | 最大线程数 | 非核心线程存活时间 | 任务阻塞队列 )

    文章目录 前言 一.ThreadPoolExecutor 构造参数 二.newCachedThreadPool 参数分析 三.newFixedThreadPool 参数分析 四.newSingleTh ...

  5. 【Java 并发编程】线程池机制 ( 线程池示例 | newCachedThreadPool | newFixedThreadPool | newSingleThreadExecutor )

    文章目录 前言 一.线程池示例 二.newCachedThreadPool 线程池示例 三.newFixedThreadPool 线程池示例 三.newSingleThreadExecutor 线程池 ...

  6. 《转载》Python并发编程之线程池/进程池--concurrent.futures模块

    本文转载自 Python并发编程之线程池/进程池--concurrent.futures模块 一.关于concurrent.futures模块 Python标准库为我们提供了threading和mul ...

  7. (转)Java并发编程:线程池的使用

    背景:线程池在面试时候经常遇到,反复出现的问题就是理解不深入,不能做到游刃有余.所以这篇博客是要深入总结线程池的使用. ThreadPoolExecutor的继承关系 线程池的原理 1.线程池状态(4 ...

  8. 并发编程-13线程安全策略之两种类型的同步容器

    文章目录 脑图 概述 同步容器 集合接口下的同步容器实现类 Vector (线程安全性比ArrayList好一些,但并非绝对线程安全) 同步容器 线程不安全的场景 其他注意事项 Hashtable C ...

  9. 并发编程-12线程安全策略之常见的线程不安全类

    文章目录 脑图 概述 字符串拼接子之StringBuilder.StringBuffer StringBuilder (线程不安全) StringBuffer (线程安全) 小结 时间相关的类 Sim ...

最新文章

  1. 灭霸来了!微软发布BugLab:无需标注,GAN掉bug
  2. mysql数据库连接过多的错误,可能的原因分析及解决办法
  3. 这13个开源GIS软件,你了解几个?【转】
  4. 用到f6的快捷键_RHINO快捷键这么多,GH电池组又太复杂怎么办?
  5. MCS-51单片机的指令时序
  6. CRM订单状态的Open, In process和Completed这些条目是从哪里来的
  7. 偶然在网上看到的题目,jQuery功底如何一测便知晓!!!!!!
  8. Nodejs Guides(四)
  9. BZOJ 2957 楼房重建-线段树
  10. tensor也可以作为索引
  11. Android--音乐播放器
  12. Android8.0适配-Only fullscreen opaque activities can request orientation
  13. 文件创建时间、访问时间、修改时间
  14. 179 Largest Number 把数组排成最大的数
  15. win10安装tomcat7的安装与配置【详细教程】
  16. 电机控制方法以及区别
  17. sniffer pro 4.7.5安装教程(附安装系统环境及软件链接)
  18. jacob转pdf linux,Java 使用jacob实现doc转pdf(附带其他方法分析)
  19. noi题库c语言 1.5答案,NOIP2004提高组复赛试题答案c语言版
  20. OSG三维渲染引擎编程学习之十七:“第二章:OSG数学基础” 之 “2.7 世界坐标系、物体坐标系、摄像机坐标系”

热门文章

  1. 微信小程序实现时间预约功能
  2. “世界很美好,值得你为之奋斗”我只同意后半句。
  3. 爆笑,2008最新一句话笑喷饭!
  4. 男生和女生的十个瞬间 (温馨啊)【转载】
  5. t检验,单因素方差和相似非参数检验-IBM SPSS 第六版第9章译文
  6. 小猫爪:嵌入式小知识01-存储器
  7. ttyS、ttySAC、tty、ttyn的区别
  8. 如何让centos7串口数(ttyS*)大于4个
  9. 已知两点和切线如何确定圆心和半径长度
  10. 2012最新网站手工注入详解教程