同步和互斥
一、同步和互斥的基本概念
现代操作系统基本都是多任务操作系统,即同时有大量可调度实体在运行,同时运行可能是真的同时运行(SMP架构中),也可能仅仅是操作系统提供的服务(通过将CPU时间分片,并将时间片分给不同的任务)。在多任务操作系统中,同时运行的多个任务可能
- 都需要访问/使用同一种资源
- 多个任务之间有依赖关系,某个任务的运行依赖于另一个任务
这两种情形是多任务编程中遇到的最基本的问题,也是多任务编程中的核心问题,同步和互斥就是用于解决这两个问题的。
- 互斥:是指散步在不同任务之间的若干程序片断,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行,最基本的场景就是对资源的同时写,为了保持资源的一致性,往往需要进行互斥访问。
- 同步:是指散步在不同任务之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务,最基本的场景就是任务之间的依赖,比如A任务的运行依赖于B任务产生的数据。
显然,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。也就是说互斥是两个任务之 间不可以同时运行,他们会相互排斥,必须等待一个线程运行完毕,另一个才能运行,而同步也是不能同时运行,但他是必须要安照某种次序来运行相应的线程(也 是一种互斥)!因此互斥具有唯一性和排它性,但互斥并不限制任务的运行顺序,即任务是无序的。而同步的任务之间则有顺序关系。
在实际的设计、编码中,任务之间的相互依赖也就是情形2是比较容易发现的,而情形1就不那么
明显了,同步/互斥中出现的问题也多和情形1相关。情形1是和资源访问相关的,要避免此类问题或者解决已经出现的此类问题首先要解决的问题是识别那类资源
是被多个任务共享的,然后再使用同步/互斥的机制来保护这些资源的访问。简单的来说资源大概可以分为以下几类:
- 存储在存储器中的资源:这类资源不依赖于任务的运行,多以文件或数据库的形式存在于存储器中,它们对所有任务都是可见的(不考虑权限问题)。
- 内核中的资源:此类资源由内核创建和维护,对内核中的任务和获取了该资源的句柄的用户任务可见(对于内核创建的资源,如果用户任务想要使用它,往往都需要通过某种API类获取资源的句柄)。
- 用户任务中的资源:此类资源仅在该任务内可见,其它任务是无法访问该类资源的。
在进行设计、编码时我们可以首先对任务使用的资源进行分析,看它使用的资源属于那一类,如果有多个任务需要使用同一类资源,那么这里就需要进行同步/互斥
了。(从这里的分析也可以看出,如果用户任务不需要使用内核中的资源也不许要使用存储器中的资源,那么它就不存在情形1所涉及的同步互斥需求)
二、用户程序编程中常见的多任务
- 多进程
- 多线程
在这里可以将用户中的资源进行进一步的分类:
1. 进程中的资源:进程是操作系统分配资源的基本单位,进程的资源主要包括
- 地址空间(涉及到同步互斥的地址段主要是数据段和堆栈段)
- 打开的文件句柄
2. 线程中的资源:一个进程中的所有线程共享进程的地址空间(数据段/堆),打开的文件句柄,由线程独享的资源包括
- 栈
- 线程专有数据区
三、进程和线程的基本概念
1.进程
进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。显然, 程序是死的(静态的),进程是活的(动态的)。进程可以分为系统进程和用户进程。凡是用于完成操作系统的各种功能的进程就是系统进程,它们就是处于运行状 态下的操作系统本身;其它进程就是用户进程。进程是操作系统进行资源分配的单位(比如文件句柄,虚拟地址空间等等)。
linux下创建子进程的调用是fork(),它功能就是产生子进程,其特别之处在于它会返回2次。
2.线程
线程是可执行代码的可分派单元。这个名称来源于“执行的线索”的概念。在基于线程的多任务的环境中,所有进程有至少一个线程,但是它们可以具有多个任务。这意味着单个程序可以并发执行两个或者多个任务。
简而言之,线程就是把一个进程分为很多片,每一片都可以是一个独立的流程。这已经明显不同于多进程了,进程是一个拷贝的流程,而线程只是把一条河流截成很
多条小溪。它没有拷贝这些额外的开销,但是仅仅是现存的一条河流,就被多线程技术几乎无开销地转成很多条小流程,它的伟大就在于它少之又少的系统开销。
多线程是为了使得多个线程并行的工作以完成多项任务,以提高系统的效率。
3.使用线程的好处
使用线程的好处有以下几点:
- .提高应用程序的响应:可以对任何一个包含许多相互独立的活动的程序进行重新设计,以便将每个活动定义为一个线程。例如,多线程 GUI 的用户不必等待一个活动完成即可启动另一个活动。
- 更有效地使用多处理器:通常,要求并发线程的应用程序无需考虑可用处理器的数量。使用额外的处理器可以明显提高应用程序的性能。具有高度并行性的数值算法和数值应用程序(如矩阵乘法)在多处理器上通过多个线程实现时,运行速度会快得多。
- 改进程序结构:许多应用程序都以更有效的方式构造为多个独立或半独立的执行单元,而非整块的单个线程。多线程程序比单线程程序更能适应用户需求的变化。
- 占用较少的系统资源:多进程与多线程相比,每个进程都有一个完整的地址空间和操作环境状态。每个进程用于创建和维护大量状态信息的成本,与一个线程相比,无论是在时间上还是空间上代价都更高。此外,进程间所固有的独立性使得程序员需要花费很多精力来处理不同进程间的通信。
4.线程唯一的资源
线程唯一的资源包括:
- 线程 ID
- 寄存器状态(包括 PC 和栈指针)
- 栈
- 信号掩码
- 优先级
- 线程专用存储
四、进程及线程的比较以及注意事项
线程是多线程编程中的主编程接口。线程仅在进程内部是可见的,进程内部的线程会共享诸如地址
空间、打开的文件等所有进程资源。由于线程可共享进程指令和大多数进程数据,因此一个线程对共享数据进行的更改对进程内其他线程是可见的。一个线程需要与
同一个进程内的其他线程交互时,该线程可以在不涉及操作系统的情况下进行此操作。
进程是操作系统分配资源的单位,不同的进程拥有的资源不同,比如地址空间、打开的文件等等。
1.多线程编程的特殊之处
因为多线程共享进程的大多数数据,因此也引入了新的注意事项:
- 线程安全函数:在C语言中局部变量是在栈中分配的,任何未使用静态数据或全局数据的函数都是线程安全的。非线程安全的函数可以通过加锁的方式来使函数实现线程安全。
- 线程安全的(Thread-Safe):如果一个函数在同一时刻可以被多个线程安全地调用,就称该函数是线程安全的。线程安全函数解决多个线程调用函数时访问共享资源的冲突问题。
- 可重入(Reentrant):函数可以由多于一个线程并发使用,而不必担心数据错误。可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据。可重入性解决函数运行结果的确定性和可重复性。
可重入函数编写规范:
- 不在函数内部使用静态或全局数据
- 不返回静态或全局数据,所有数据都由函数的调用者提供。
- 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。
- 如果必须访问全局变量,利用互斥机制来保护全局变量。
- 不调用不可重入函数。
线程安全与可重入函数之间的关系:
- 一个函数对于多个线程是可重入的,则这个函数是线程安全的。
- 一个函数是线程安全的,但并不一定是可重入的。【例如可以使用互斥锁实现的线程安全】
- 可重入性要强于线程安全性。
例如标准库中的malloc是不可重入的,但是标准库的实现一般都有提供线程安全的版本(具体怎么用线程安全的版本可以查看使用的库的手册,总之小心一点)。
线程安全和可重入函数针对的是多线程环境下,由多个线程共享的资源(静态数据、全局数据)的使用问题。之所以有这个问题是因为在某些函数会使用静态 或者全局数据,它运行的结果依赖于这些数据,这时如果此类函数在运行中被切换走,然后另一个线程也调用了该函数就会出问题,因此这类问题是特定存在于多线 程环境的(当然类似的问题在进程环境下也存在,存在的原因是信号处理函数可能在任意时刻被调用,因而它可能打断进程的正常运行;多个进程之间是不存在这类 问题的,因为每个进程看到的都是自己独立的地址空间)。
2.同步和互斥的实现
- 多线程使用互斥锁、条件变量、自旋锁、读写锁和信号量来实现同步和互斥。
- 多进程也使用互斥锁、条件变量、自旋锁、读写锁和信号量来实现同步和互斥,但是需要用进程间通信(IPC)来实现信息共享/传输。
需要注意的是,并没有所谓的线程间通信,因为同一个进程内部的线程会共享该进程的资源,比如堆空间、打开的文件等等,因而同一个进程内的多个线程之间不存在通信问题;如果是不同进程之间的线程,就按照进程之间通信方式进行通信即可。
3.何时采用多线程何时采用多进程
这是一个比较艰难的抉择:),取决于应用场景,它们的区别或许是有参考价值的:
- 一个进程中的所有线程都必须运行相同的可执行程序(它们最多能做到的是运行相同可执行程序的不同部分)。而一个子进程可以运行一个完全不同的可执行程序。
- 进程是操作系统分配资源的基本单位,而线程则不是,线程独立拥有的主要是栈和线程专用缓冲区
- 由于同一个进程内的多个线程共享同一块虚拟内存和其它资源,因而一个线程出错可能会影响到同一个进程中的其它线程。而多进程环境下,一个进程出错并不会影响其它进程,因为每一个进程都拥有自己独立的资源。
- 创建新进程时的资源拷贝使得创建新的进程比创建新的线程效率底下很多。不过由于写时拷贝机制的存在,因而如果子进程不产生写请求,这个影响就会很小。
- 如果一个任务可以被分解为多个几乎完全相同的子任务,则多线程就可能是一个很好的选择。
- 由于同一个进程内的多个线程共享进程的资源,因而多个线程共享资源非常简单(当然,代价是必须防止出现竞态)。而多进程之间共享资源则需要通过IPC机制