C++11 多线程与并发编程(1)
文章目录
进程与线程的基础知识
以下概念基于操作系统,没有深入介绍。
如果有错误的地方,欢迎指正。
包含:进程、线程、多进程(线程)、死锁、并发、异步等
进程与线程
进程:即为执行中的程序。一个进程是操作系统分配资源的基本单位,包含程序代码、数据、资源等(如内存)。
线程:可视作“轻量级进程”。
- 一个进程可以包含多个线程
- 进程有独立的内存空间;而线程共享进程的地址空间和资源。
- 线程间的通信比进程间的通信更高效。
- 创建和销毁进程的代价高于线程的创建与销毁
进程(线程)的几种基本状态
- 创建态(新建态):创建进程(线程)
- 就绪态:其它资源准备充分,等待分配CPU
- 等待态(阻塞态):因等待某一事件暂无法运行
- 运行态:运行程序
- 终止态(结束态):运行结束
多进程(进程):都是用于实现并发执行的技术,其目的是让程序能够同时执行多个任务,提高系统的资源利用率和响应性能。
多进程 | 多线程 | |
---|---|---|
概念 | OS中同时运行多个独立的进程 | 同一个进程内运行多个线程 |
资源 | 多个进程间相互独立,各自拥有资源 | 多个线程间共享进程的资源 |
通信 | 进程间通信需要IPC机制 | 多个线程间可直接访问共享变量 |
优点 | 稳定性高(因其互相独立,一个进程崩溃不会影响其他进程) | 资源共享和通信简单高效,创建与销毁开销小 |
缺点 | 创建和销毁开销大 | 共享资源可能导致安全问题 |
- 很明显,进程的管理调度是由系统操作,所以我们在C++中,通常使用多线程库来管理调度线程,从而实现并发。
- 后续针对线程的优点和缺点使用代码进行操作和说明。
死锁:两个或多个进程(线程)相互等待对方释放资源,导致所有参与者都无法继续执行。
这种情况通常发生在多个进程(线程)同时持有一些资源,而每个线程(进程)都在等待其他进程(线程)释放自己所需要的资源,从而形成循环等待。
死锁会让进程处于阻塞态。
【一个很有名的死锁笑话】
面试官:解释一下什么叫做死锁,解释明白我们就会要你。
程序员:先发 offer,签完 offer 再解释。
产生的必要条件:①互斥条件(资源排他)②不剥夺条件 ③请求保持条件(吃着盆里的想着锅里的) ④循环等待条件
发生的典型场景:①互斥访问 ②资源竞争 ③ 循环等待
为了避免死锁的发生,我们需要根据以上它产生的条件及场景入手,针对性击破,这部分在后面线程安全进行操作。
并行并发
并发:多个任务在同一时间段内交替执行,其在这个时间段都处于已开始运行到运行完毕之间。通过系统调度实现“同时”进行多个任务。
比方在单CPU的计算机中,某一时间点只允许执行一个程序。假设我们在听歌和打游戏,我们感觉这两件事是同时进行的。
但事实上,操作系统是在游戏进程和音乐播放器进程之间来回切换执行,只是计算机处理速度很快,如果调配得当,用户察觉不到“停顿”,自然认为是在同时进行。
并行:当系统有一个以上CPU,A cpu执行一个进程,B cpu可以执行另一个进程,两个进程互不抢占CPU资源,同时进行。
- 并发的“同时”是宏观上的同时,微观上交替进行。是同一个时间段内的同时。
- 好比一个普通人在一个小时内,写一会儿数学题,写一会儿英语题,他说刚刚那个小时自己同时做了数学和英语作业。
- 并行的“同时”是事实上的同时,是同一个时间点上的同时。
- 好比一个可以一心二用的天才,左手写英语题,右手写数学题,他说自己刚刚同时做了数学和英语作业。(不必加上时间限定词)
总结:**并发的多个任务互相抢占资源,并行**的多个任务各自独立拥有资源。只有在多CPU的情况下,才会发生并行。否则,看似同时发生的事情,都是并发执行的。
同步异步
操作系统中的同步(Synchronous)和异步(Asynchronous)指的是任务之间执行和交互的方式。
同步:指任务按照顺序执行,一个任务的执行必须等待上一个任务的完成
- 每个任务都会阻塞当前线程,直到它操作完成。
- 任务之间的执行顺序可预测,任务按序依次执行
异步:指任务可以同时进行,不需要等待上一个任务的完成。
- 一个任务的执行不会阻塞当前线程,可以继续执行其他任务。
- 在异步任务中,任务的执行时非阻塞的,任务完成后会通知调用者
- 异步通常 用于需要并发执行或等待时间较长的任务,如:网络请求,文件操作。
假设你需要洗衣服和煮面,这两件事需要的资源各不相同。
“同步方式”:必须先洗完衣服,再煮面条。做B事情,必须等A做完。
“异步方式”:可以同时洗衣服和煮面。洗衣服的同时,也开始煮面条,而不需要等衣服洗完。
线程库<thread>的基本运用(C++11新特性)
头文件:<thread>
<thread>库 是C++ 11 标准引入的新库,用于支持多线程编写。其命名空间也在std下。
创建调用
1、创建一个线程:std::thread thread_name(func, args...)
func
是线程入口点的函数或可调用对象。args
是传递给func
这个函数的参数。- 使用
<thread>
库创建一个线程,该线程会被创建,并立刻开始执行指定的可调用对象。
由于新线程的执行是自发的,可能出现新线程未执行完,主程序已经结束,导致新线程无法执行。
2、 等待线程完成其执行:thread_name.join()
void join()
是thread类
的成员函数,作用是等待当前线程完成执行。- 使用
join()
将阻塞当前线程,直至调用它的线程(即*this所标识的线程)结束执行
3、分离线程:thread_name.detach()
void detach()
也是thread类
的成员函数,它将从thread对象分离线程,使它独立地执行。当该线程退出时,将释放其分配的任何资源。- 调用
detach()
后,this不再占有任何线程。 - 换句话说,线程被分离出去后,即便主程序结束,也会继续执行它的任务,直到任务执行完成。
4、检查thread对象是否还在执行:joinable()
bool joinable()
也是thread类
的成员函数,它检查thread对象是否还在执行,也就是该对象可以被join()
或detach()
- 如果对一个不可加入的线程调用
join()
或detach()
,将会抛出std::system_error
异常。
综合代码示例
#include <iostream>
#include <thread>
#include <string>
#include <chrono>
void printHello(std::string msg)
{
std::cout << msg << std::endl;
}
void waitAndPrint(std::string msg)
{
// 休眠五秒
std::this_thread::sleep_for(std::chrono::seconds(10));
std::cout << "sleep over" << std::endl;
std::cout << msg << std::endl;
}
int main()
{
// 创建线程,传参为一个函数,后续参数给第一个函数传参
std::thread thread1(printHello, "Hello Thread.");
// 1. 用于检查线程是否结束,
// 防止出现子线程未结束,主程序已结束的情况
// join函数会阻塞,让主线程等待子线程
thread1.join();
// 2.利用joinable()检查线程状态
std::thread thread3;
std::cout<< "before start thread3, joinable: "<< thread3.joinable() <<std::endl;
thread3 = std::thread{printHello, "judge Thread."};
if(thread3.joinable()){
std::cout<< "start running thread3, joinable: "<< thread3.joinable() <<std::endl;
thread3.join();
}
std::cout<< "after join thread3, joinable: "<< thread3.joinable() <<std::endl;
// 3. 分离操作,子线程后台运行,主线程结束,子线程可还在后台继续运行
std::thread thread2(waitAndPrint, "detach Thread.");
thread2.detach(); //如果没有这行,就会报错:terminate called without an active exception
return 0;
}
thread2
运行时主程序已经结束,所以没有成功在控制台打印std::cout<< "start running thread3, joinable: "<< thread3.joinable() <<std::endl;
这行代码未运行到end
时,thread3
已经运行,所以产生上图的奇怪结果。
【附:thread类
的其他常用成员函数】
- swap(other_thread):交换两个thread对象
- get_id(): 返回一个std::thread::id,它标识与
*this
关联的线程。
命名空间:std::this_thread
1、get_id()
- 返回当前线程的id
std::thread::id this_id = std::this_thread::get_id(); // 函数体内
2、sleep_for<>()
- 接受一个时间段参数
std::chrono::duration
- 函数会使当前线程休眠指定的时间段,然后继续执行后续代码(函数阻塞时间可能长于指定时间)
using namespace std::chrono_literals;
std::this_thread::sleep_for(2000ms); // 线程休眠2000ms
// 配合时间库<chrono>使用
std::chrono::milliseconds interval(100);
std::this_thread::sleep_for(interval);
3、sleep_until<>()
- 接受一个时间点参数
std::chrono::time_point
- 函数会使当前线程休眠直到指定的时间点,然后继续执行后续代码。
auto now() { return std::chrono::steady_clock::now(); }
auto awake_time()
{
using std::chrono::operator""ms;
return now() + 2000ms;
}
std::this_thread::sleep_until(awake_time());
参考: