发布于: 2023-10-9最后更新: 2023-10-9字数 00 分钟

进程、线程、协程

线程同步

同步是指协调线程的运行顺序,避免产生data races导致结果不一致。常见的方式有以下六种。
  • Mutex,互斥锁,确保同一时间只有一个线程能访问资源。
  • ReadWrite lock,读写锁,读时共享,写时其他线程无法访问资源。
  • Spin lock,自旋锁,当资源被锁时,线程循环等待,直到获得锁。
  • Semaphore,信号量,一个计数器,当信号量满足条件时,线程可以访问资源。与mutex的不同在于,mutex只有lock和unlock两种状态,而semaphore可以有多种状态。
  • Conditional variable,条件变量,线程等待直到条件满足。
  • Barrier,屏障,线程等待直到所有线程都到达屏障。
线程同步方式。互斥锁、读写锁、自旋锁区别。适用什么场景。
比如自旋锁主要适用短小的互斥代码逻辑。判断依据是执行互斥代码逻辑开销小于线程沉睡唤醒的切换开销。
以下是这些同步方式的使用场景:
  • 互斥锁:当你需要保护的临界区代码不能被多个线程同时执行时,可以使用互斥锁。比如说,对全局变量的修改,或者对数据库的写入操作。
  • 读写锁:当你的程序中有大量的读操作和较少的写操作时,可以使用读写锁。比如说,一个在线词典,大部分用户只需要查询词汇(读操作),而只有管理员偶尔需要更新词条(写操作)。
  • 自旋锁当你需要保护的临界区代码执行非常快,且你预计锁不会被长时间持有时,可以使用自旋锁。比如说,对一个数据结构的简单更新操作,这个操作可能只需要几个CPU指令就能完成。

进程同步

线程和进程的共享内存有什么区别

线程共享内存: 在同一进程中的线程共享同一内存空间。这意味着一个线程在内存中创建的数据结构可以被同一进程中的其他线程访问。线程之间共享内存主要包括:堆内存、已打开的文件描述符、全局变量等。这使得线程间的通信更加方便,可以通过修改和检查共享内存中的变量来进行。然而,这也意味着线程需要协调对共享内存的访问,以避免冲突和不一致,这就需要使用线程同步机制,如互斥量(mutexes)、条件变量(condition variables)等。
线程的生命周期从创建开始,结束于线程主动退出或被其他线程取消。当线程结束时,其栈被清空,但它在堆上分配的内存或者打开的文件描述符等资源并不会自动回收,这些资源需要由其他线程或进程进行清理。
进程共享内存: 默认情况下,进程并不共享内存。每个进程都有自己独立的地址空间,一个进程无法访问另一个进程的内存。但是,进程间也可以通过特定的机制实现共享内存,这就是所谓的共享内存(Shared Memory)。共享内存是进程间通信的一种方式,它允许两个或多个进程访问同一块物理内存。在这种情况下,进程需要协调对共享内存的访问,以避免冲突和不一致。
共享内存的生命周期并不完全受控于单个进程。它的生命周期开始于某个进程创建共享内存区域(例如在Unix系统中,使用shmget或者mmap等系统调用),并结束于系统中的最后一个使用它的进程解除映射(munmap)并删除它(例如在Unix系统中,使用shmctl函数与IPC_RMID命令)。

协程

对比线程如何降低了切换开销。(其实没怎么看懂,用得太少了)
协程(coroutines)是一种程序组件,它比线程更轻量级,可以用来处理大量的并发任务。协程不同于线程的一大特点就是它不是由操作系统调度,而是由程序员或库在用户空间调度。这使得协程的切换开销比线程小得多。
在C++20中,引入了对协程的支持。C++协程使用co_await, co_yield, co_return关键字来标记其挂起(suspension)点。
以下是一个简单的C++20协程的例子:
在这个例子中,hello()函数是一个协程。当调用hello()时,它会输出"Hello ",然后在co_await语句处挂起,控制权返回给main()函数。注意,此时hello()函数并未结束,它仍然保留着自己的状态。当main()函数结束时,所有未完成的协程都会被恢复并运行至结束,此时hello()函数会被恢复,输出"world!\n"。
协程在C++中有多种应用场景,主要包括以下几个方面:
  1. 异步编程:协程可以极大地简化异步编程的复杂性。使用协程,你可以像写同步代码一样编写异步代码,无需处理复杂的回调或者Promise结构。例如,你可以使用协程来编写异步的网络编程代码,或者处理其他的异步I/O操作。
  1. 生成器:协程可以用来创建生成器(Generator),即一种特殊的迭代器,可以在每次产生一个新的值时暂停。当需要下一个值时,协程会从上次暂停的地方恢复执行。这在处理大量数据或者无限序列时非常有用。
  1. 并发编程:虽然协程不是并行执行的,但是它们可以用来处理大量的并发任务。由于协程的切换开销非常小,你可以在同一线程中运行数以百万计的协程,用来处理大量的并发任务,比如网络请求。
  1. 协作多任务:协程可以用来编写协作式多任务代码。在这种模式下,每个任务都是一个协程,任务之间可以通过co_yield关键字来互相切换。这种方式比抢占式多任务(线程)更加轻量级,更易于控制。
  1. 异步工作流程和状态机:协程也可以用来编写复杂的异步工作流程和状态机。由于协程可以在任何地方暂停和恢复,这使得你可以用直接的方式来编写复杂的工作流程和状态机,而无需分割你的代码到多个状态函数中。
协程(Coroutine)和线程(Thread)都是用于并发编程的工具,但它们在设计和实现上有一些关键的区别,这些区别导致了协程在切换开销上相比线程有很大的优势。
  1. 上下文切换:线程的切换(即线程上下文切换)需要操作系统的介入,涉及到了一系列复杂的步骤,如保存和恢复寄存器、程序计数器、堆栈、内存映射等状态信息,这些操作都需要消耗大量的时间。而协程的切换则仅仅涉及到了栈的切换,这个过程可以在用户空间内完成,开销远小于线程的上下文切换。
  1. 调度机制:线程的调度由操作系统来控制,是抢占式的,即操作系统可以在任何时刻打断线程的执行,并切换到另一个线程。这种切换是不可预测的,因此需要复杂的同步机制来避免并发问题。而协程的调度是协作式的,即协程需要显式地指出在哪里切换到另一个协程。这种方式使得协程的切换更加可控,不需要复杂的同步机制。
  1. 内存占用:每个线程都有自己的堆栈,通常大小为几MB,如果创建大量的线程,将会消耗大量的内存。而协程的堆栈大小是动态的,可以根据需要来分配和释放,因此创建大量的协程的内存开销远小于创建大量的线程。
 

秋招复盘9.21:开摆后的日常
秋招复盘9.21:开摆后的日常