线程和并发
计算机术语中的并发,指的是单个系统里同时执行多个独立的活动,而不是顺序一个个执行,对于单核CPU来说,某个时刻只可能处理一个任务,但是它却不是顺序执行的一个个任务,而是对CPU进行分时复用,一直在任务之间切换,每个任务完成一部分就去执行下一个任务,看起来任务在并行发生,虽然不是严格意义上执行多个任务,但是我们仍然称之为并发(concurrency)。真正的并发是在多核处理器上,能够真正同时执行多个任务,称为硬件并发(hardware concurrency)。
假设完成A和B任务都被分为5个大小相等,单核CPU交替的执行两个任务,每次执行其中的一块,其花费的时间并不是先完成A任务再玩成B任务所花费时间的两倍,而是要更多。因为系统从一个任务到另一个任务号需要完成一次上下文切换,这是需要时间的(图中的灰色块)。上下文切换需要操作系统为当前执行的任务保存状态和指令指针,由调度器计算出切换到哪个任务,并未要切换的任务加载处理器状态,然后将新任务的指令和数重新加载到缓存。
并发的方式分为多进程并发和多线程并发
多进程并发:这些独立的进程可以通过常规的进程间通信方式进行通信,比如管道,消息队列,共享内存,信号量,存储映射I/O,套接字等,缺点是进程间通信较为复杂,速度相对线程间的通信也比较慢,启动进程的开销比线程大,使用的系统资源也更多。
多线程并发:线程类似轻量级的进程,但是一个进程中所有线程都共享相同的地址空间,线程间的大部分数据都可以共享,线程间的通信一般都可以通过全局变量来实现。
C++ 11 多线程库
C++ 98标准中并没有线程库的实现,而在C++ 11中提供了多线程的标准库,提供了管理线程,保护共享数据,线程间的同步操作,原子操作的功能。
使用普通函数或者静态函数创建线程
// static void threadLoop();
// 使用静态函数的方式构造
void threadDemo::threadLoop() {
int i = 0;
while (true) {
if (i > 10) {
cout << "exit thread" << endl;
break;
//this_thread::yield(); // 让出CPU的使用权
}
cout << "threadDemo::threadLoop thread tid = " << this_thread::get_id() << endl;
// microseconds 表示 us
//this_thread::sleep_for(chrono::microseconds(2000));
this_thread::sleep_for(chrono::milliseconds(1000));
i++;
}
}
// 使用普通函数的方式构造
void _threadloopsleep() {
while (true) {
cout << "_threadloopsleep thread tid = " << this_thread::get_id() << endl;
cout << "cpu count: " << std::thread::hardware_concurrency() << endl;
this_thread::sleep_for(chrono::milliseconds(1000));
}
}
// 使用静态函数方式创建
{
t = new thread(threadLoop);
t->join();
}
{
// 使用普通函数的方式创建
t = new thread(_threadloopsleep);
t->join();
cout << "thread get handle " << t->native_handle() << endl;
}
- 首先创建的一个new 一个线程对象得到一个 std::thread类型的指针 t,构造的时候传递了一个参数,这个参数是一个函数,这个函数就是这个线程的入口函数,函数执行完毕退出,整个线程也就执行完成了
- 线程创建成功后,就会立刻启动,没有一个类似于 start 的函数来显式启动线程。
- 一旦线程开始运行,就要在主函数中显式决定是否要等待它启动完成(join),或者分离它让它自己运行(detach),只需要在 thread 对象销毁前完成这个决定就可以了,指针 t 是栈上的变量,只需要在 main 函数执行完成之前决定即可。
- join 函数时主线程阻塞,等待子线程执行完成,join 的另一个任务就是回收该线程中资源
C++11 还提供了获取线程 id,获取系统CPU个数,获取thread 的 native_handle (handle 可以用于 pthread 相关操作),使得线程休眠等功能。
调用线程的 detach 函数,可以将线程放在后台运行,所有权和控制权被转交给C++ 运行时库,以确保线程相关的资源能够在线程退出时正确的回收。这个模式类型于 UNIX 中守护进程的概念(damon),线程被分离后,即使该线程对象被析构了,线程还是能够在后台运行,但是由于线程对象被析构了,主线程不能通过该对象名和这个线程进行通信。
传递参数给线程
线程的入口函数不带参数,也可以从主线程传递参数给子线程,这是传递的参数需要用 ref 函数包装起来
void threadDemo::threadLoopPara(int& para) {
while (true) {
cout << "thread tid = " << this_thread::get_id() << " get para = " << para << endl;
this_thread::sleep_for(chrono::milliseconds(1000));
}
}
{
int temp = 10;
t = new thread(threadLoopPara, ref(temp));
t->join();
}
注意传递的参数的生命周期,传递的参数 temp,会在主函数执行完毕之后被回收,假设上面的线程使用的是线程分离的模式运行,打印出的 temp 将是错误的结果。
sleep_for 和 sleep_util 函数
// 固定时间间隔来执行程序 using sleep_until
/*_threadloopsleeputil thread tid = 12664
_threadloopsleeputil duration 1005
_threadloopsleeputil thread tid = 12664
_threadloopsleeputil duration 1014
_threadloopsleeputil thread tid = 12664
_threadloopsleeputil duration 1014
_threadloopsleeputil thread tid = 12664
_threadloopsleeputil duration 1015
_threadloopsleeputil thread tid = 12664*/
void _threadloopsleeputil() {
//using std::chrono::operator""ms;
chrono::time_point<chrono::steady_clock> awaketime;
static clock_t lastclock = clock();
while (true) {
{
clock_t duration = clock() - lastclock;
cout << "_threadloopsleeputil duration " << duration << endl;
lastclock = clock();
}
chrono::time_point<chrono::steady_clock> time_now = chrono::steady_clock::now();
awaketime = time_now + 1000ms;
cout << "_threadloopsleeputil thread tid = " << this_thread::get_id() << endl;
this_thread::sleep_until(awaketime);
}
}
使用 sleep_util 函数可以使线程休眠到特定的时间点唤醒,sleep_for 是让线程休眠特定的时间段。
使用仿函数创建线程
仿函数(Functor) 又称为函数对象 (Function Object) 是一个能行使函数功能的类,仿函数的语法几乎和我们普通的函数调用是一致的,特殊的是作为一个类,必须重载 operator() 运算符,因为调用仿函数,实际上就是通过类对象调用重载后的 operator() 运算符。
需要注意的是,如果线程的入口函数使用了仿函数中的变量,线程使用分离的方式运行,要注意变量的生命周期导致的变量值失效的问题。
class loopMain {
public:
loopMain(const char *name);
~loopMain() = default;
void operator()();
private:
string mName;
};
loopMain::loopMain(const char *name): mName(name){
cout << "construct " << mName << endl;
}
void loopMain::operator()() {
while (true) {
cout << "loopMain operator()" << endl;
this_thread::sleep_for(chrono::milliseconds(1000));
}
}
void threadDemo::threadLoopMain() {
while (true) {
cout << "method threadLoopMain" << endl;
this_thread::sleep_for(chrono::milliseconds(1000));
}
}
{
loopMain loop("mLoop");
t = new thread(loop);
t->join();
}
使用成员函数创建线程
使用类型的成员函数的方式创建线程,此时注意必须要先实例化对象,然后传入类的实例化对象,如果变量以分离的方式运行,要注意变量的生命周期导致的变量值失效的问题。
public:
void threadLoopMain();
void threadDemo::threadLoopMain() {
while (true) {
cout << "method threadLoopMain get var:" << m << endl;
this_thread::sleep_for(chrono::milliseconds(1000));
}
}
{
t = new thread(&threadDemo::threadLoopMain, this);
t->detach();
}
使用lamda表达式创建线程
lamda 本质上是一种匿名函数类型,函数类型包含了函数指针的功能,所以也可以用于创建线程。
{
int temp = 100;
t = new thread([&]() {
while (true) {
cout << "thread tid = " << this_thread::get_id() << " get para = " << temp << endl;
this_thread::sleep_for(chrono::milliseconds(1000));
}
});
t->join();
}