【C++11多线程与并发编程】(1)操作系统中的进程与线程以及<thread>库的基本运用

C++11 多线程与并发编程(1)


C++11多线程与并发编程

进程与线程的基础知识

以下概念基于操作系统,没有深入介绍。

如果有错误的地方,欢迎指正。

包含:进程、线程、多进程(线程)、死锁、并发、异步等

进程与线程

进程:即为执行中的程序。一个进程是操作系统分配资源的基本单位,包含程序代码、数据、资源等(如内存)。

线程:可视作“轻量级进程”。

  • 一个进程可以包含多个线程
  • 进程有独立的内存空间;而线程共享进程的地址空间和资源。
  • 线程间的通信比进程间的通信更高效。
  • 创建和销毁进程的代价高于线程的创建与销毁

进程(线程)的几种基本状态

  1. 创建态(新建态):创建进程(线程)
  2. 就绪态:其它资源准备充分,等待分配CPU
  3. 等待态(阻塞态):因等待某一事件暂无法运行
  4. 运行态:运行程序
  5. 终止态(结束态):运行结束

image-20240410151002173

进程的几种状态

多进程(进程):都是用于实现并发执行的技术,其目的是让程序能够同时执行多个任务,提高系统的资源利用率和响应性能。

多进程多线程
概念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;
}

image-20240410181401849

运行结果
  • 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()); 

参考:

[1] 面试必考的:并发和并行有什么区别?

[2] C++网络编程高并发 讲师:陈子青

[3] cppreference——thread

[4] cppreference——mutex

[5] 帝江VII ——c++11 call_once用法(多线程时仅初始化一次的完美解决方案)

  • 30
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值