C++ 多线程(一)
Multi-Threaded
多线程编程术语
线程
更确切地说,是执行线程,它是最小的处理单元。
- 由操作系统调度。
- 通常它包含在进程中。
- 因此,同一个进程中可以存在多个线程。它与进程共享资源:内存、代码(指令)和全局变量(上下文——它的变量在任何给定时刻引用的值)。
- 在单个处理器上,每个线程根据时间进行多路复用;在多处理器上,每个线程同时运行,每个处理器/核心运行一个特定的线程。
为什么用多线程
在应用程序本身中有多个执行线程的应用程序称为多线程应用程序。
例如,如果我们想创建一个服务器,它可以提供与服务器能够处理的并发连接数量一样多的连接,我们可以相对容易地完成这样的任务,如果我们为每个连接提供一个新线程。在某些情况下,多线程服务器不是创建单独的套接字来处理传入的连接,而是为每个传入的连接创建一个新线程,并为每个新线程创建一个新套接字。
我们需要多线程的另一个典型例子是 GUI 应用程序。GUI 应用程序有一个执行线程(主线程),每次执行一个操作。此线程要么在等待事件,要么在处理事件。因此,如果用户从用户界面触发一个耗时的操作,则在操作进行期间界面会冻结。在这种情况下,我们需要实现多线程。
主线程可以通过创建 thread 子类的对象来启动新线程。如果是多线程的,GUI 在它自己的线程中运行,在其他线程中进行额外的处理,即使在密集处理期间,应用程序也将具有响应性的 GUI。换句话说,我们将繁重的处理传递给分离的辅助线程,并让主 GUI 线程自由地响应用户。
这些新线程需要完成他们之间的沟通,以便应用程序可以让用户了解进展(保持用户界面更新关于进展),允许用户进行干预(为用户提供一些控制辅助线程),然后让主线程知道何时处理完成。通常,它们通过使用诸如互斥、信号量或等待条件等资源保护机制来一起使用共享变量。
线程 vs 进程
进程和线程彼此相关,但本质上是不同的。
一个进程可以被看作是一个正在运行的程序的实例。每个进程都是一个独立的实体,系统资源(如 CPU 时间、内存等)都被分配给它,并且每个进程都在一个独立的地址空间中执行。如果我们想访问另一个进程的资源,必须使用进程间通信,如管道、文件、套接字等。关于 Linux 进程的更多信息。
进程为每个程序提供了两个关键的抽象:
- 逻辑控制流
每个进程似乎都独占 CPU- 专用虚拟地址空间
每个进程似乎独占使用主存
线程使用进程的相同地址空间。一个进程可以有多个线程。进程和线程之间的一个关键区别是,多个线程共享它们的部分状态。通常,多个线程可以对同一个内存进行读写(没有一个进程可以直接访问另一个进程的内存)。但是,每个线程仍然有自己的激活记录堆栈和自己的 CPU 寄存器副本,包括堆栈指针和程序计数器,它们一起描述了线程的执行状态。
线程是进程的特定执行路径。当一个线程修改一个进程资源时,该更改立即对同级线程可见。
- 当线程在进程中时,进程是独立的。
- 进程有独立的地址空间,而线程共享它们的地址空间。
- 进程之间通过进程间通信进行通信。
- 进程携带大量状态信息(例如,就绪、运行、等待或停止),而进程中的多个线程共享状态以及内存和其他资源。
- 同一进程中线程之间的上下文切换通常比进程之间的上下文切换要快。
- 多线程比多进程有一些优势。线程比进程需要更少的管理开销,而且进程内线程通信比进程间通信的开销更小。
- 多进程并发程序有一个优点:每个进程可以在不同的机器上执行(分发程序)。分布式程序的例子有文件服务器(NFS)、文件传输客户端和服务器(FTP)、远程登录客户端和服务器(Telnet)、群件程序、Web 浏览器和服务器。
消息传递 vs 共享内存
- 消息传递
- 消息通过进程间通信(IPC)机制交换数据。
- 优点: 适用于本地和远程通信。
- 缺点: 在“环回”配置中,对于大消息可能会导致额外的开销。
- 共享内存
- 共享内存允许应用程序访问和交换数据,就像他们是本地地址空间的每个进程。
- 优点: 在环回配置中可以更有效地处理大消息。
- 缺点: 不能有效地推广到远程系统 & 可能更容易出错 & 对于OO应用程序是不可移植的。
多进程 vs 多线程
- 多进程
- 进程是资源配置和保护的单位。
- 进程管理某些资源,如虚拟内存、I/O处理程序和信号处理程序。
- 优点: 通过 MMU 保护进程不受其他进程的影响。
- 缺点: 进程间的 IPC 可能是复杂和低效的。
- 多线程
- 线程管理特定的资源,例如堆栈、寄存器、信号掩码、优先级和特定于线程的数据
- 优点: 线程间的 IPC 比进程间的 IPC 更有效率。
- 缺点: 线程可能相互干扰。
进程和内核
在 Linux 系统中,内核知道并控制运行中的系统的一切,但进程不知道。进程是一个正在执行的程序的实例。当一个程序执行时,内核将程序的代码加载到虚拟内存中,为程序变量分配空间,并设置内核记录数据结构来记录关于该进程的各种信息(如进程ID、终止状态、用户ID和组ID)。
进程
- 一个正在运行的系统通常有许多进程。
- 对于一个进程,许多事情都是异步发生的。
- 一个正在执行的进程不知道它下一次何时会超时,其他哪些进程将被调度到 CPU (以及以什么顺序),或者下一次何时调度。信号的传递和进程间通信事件的发生是由内核调解的,并且可以在进程的任何时候发生。
- 对于一个过程来说,许多事情都是透明的。进程不知道它位于 RAM 中的什么位置,通常也不知道它的内存空间的特定部分当前是驻留在内存中还是保存在交换区(用于补充计算机 RAM 的预留磁盘空间)中。
- 类似地,进程不知道它访问的文件在磁盘驱动器的何处;它只是通过名称引用文件。
- 进程是独立运行的;它不能直接与另一个进程通信。
- 进程本身不能创建新进程,甚至不能结束自身的存在。实际上,一个进程可以使用 fork() 系统调用创建一个新进程。调用 fork() 的进程称为父进程,新的进程称为子进程。内核通过复制父进程来创建子进程。
- 最后,进程不能直接与计算机上的输入和输出设备通信。
内核
- 内核促进了系统上所有进程的运行。
- 内核决定接下来哪个进程将获得对 CPU 的访问权、何时访问以及访问时间。
- 内核维护包含所有正在运行的进程信息的数据结构,并在创建、更改状态和终止进程时更新这些结构。
- 内核维护所有低级数据结构,这些结构使程序使用的文件名能够被转换到磁盘上的物理位置。
- 内核还维护数据结构,将每个进程的虚拟内存映射到计算机的物理内存和磁盘上的交换区域。
- 进程之间的所有通信都是通过内核提供的机制完成的。为了响应来自进程的请求,内核创建新进程并终止现有进程。
- 最后,内核(特别是设备驱动程序)执行所有与输入和输出设备的直接通信,根据需要向用户进程传递信息。
从“Linux 编程接口”中的“系统的进程与内核视图”编辑
实时操作系统中的调度方法
优先级
- 每个任务相对于所有其他任务有一个优先级
- 最关键的任务被赋予最高的优先级
- 准备运行的最高优先级的 Task 获得处理器的控制权
- Task 会一直运行,直到产生、终止或阻塞
- 每个 Task 都有自己的内存堆栈
- 在一个 Task 运行之前,它必须从它的内存堆栈中加载它的上下文(这可能需要许多个周期)
- 如果 Task 被抢占,它必须保存当前状态/上下文;当 Task 获得处理器的控制权时,将恢复此上下文
在这种抢占式调度中,线程(或进程)必须处于以下四种状态之一:
- 运行:任务在 CPU 的控制下
- 就绪:当调度策略显示该任务为系统中优先级最高的未被阻塞任务时,该任务不被阻塞,并准备接受 CPU 的控制。
- 不活动:任务被阻塞,需要初始化才能准备好。
- 阻塞:任务正在等待某事发生或等待资源可用。
轮循
在这种方法中,时间片以相等的部分和循环的顺序分配给每个进程,处理没有优先级的所有进程(也称为循环执行)。循环调度非常简单,易于实现,而且不受限制。
有关实时操作系统的更多信息,请访问嵌入式系统编程:RTOS (实时操作系统)
优先级反转
这个问题的存在早已为人所知,然而,并没有万无一失的方法来预测情况。
为了确保快速响应时间,嵌入式 RTOS 可以使用抢占(高优先级任务可以中断正在运行的低优先级任务)。高优先级任务结束运行后,低优先级任务从中断点开始继续执行。
不幸的是,在抢占式多任务环境中操作的任务之间共享资源的需求可能会造成冲突。与死锁一起,优先级反转是两个最常见的导致应用程序失败的问题之一。(火星探路者计划)
典型的例子是中优先级任务(M)抢占高优先级任务,导致优先级反转。一个低优先级进程(L)获取了一个高优先级进程(H)想要访问的资源,但是被一个中优先级进程(M)抢占,因此高优先级进程(H)阻塞在该资源上,而中优先级进程(M)完成。当第三个中等优先级(M)的任务在低优先级进程使用资源时变为可运行时,就会发生这种情况。一旦高优先级的任务变得不可运行,第三个中等优先级的任务(M)是最高优先级的可运行任务,因此它运行,而当它运行时,L不能释放资源。因此,在这个场景中,中优先级任务(M)抢占高优先级任务(H),导致优先级反转。
图片来自如何使用优先级继承
为了避免优先级反转,优先级被设置(提升)为预定义的优先级,高于或等于可能锁定特定互斥锁的所有线程的最高优先级。这就是所谓的优先级上限。
当一个任务获取一个共享资源时,该任务会被提升(其优先级暂时提升)到该资源的优先级上限(这种无条件提升与优先级继承不同)。优先级上限必须高于所有可以访问该资源的任务的最高优先级,以确保拥有共享资源的任务不会被试图访问同一资源的其他任务抢占。当被提升的任务释放资源时,任务将恢复到原有的优先级。
优先级上限的替代方法是优先级继承,这是一种使用动态优先级调整的变体。当低优先级任务获取共享资源时,该任务将继续以其原始优先级运行。如果高优先级的线程请求共享资源的所有权,那么低优先级的任务将被提升到请求线程的优先级级别(高优先级的线