Practice make perfect

《Go并发编程实战(第2版)》

并发编程综述

多进程编程

多进程程序中多个进程协作完成任务,需要进程间通信,即 IPC ( Inter-Process Communication )

从处理机制可分为三大类:

  • 基于通信的 IPC 方法
    • 数据传送
      • 管道 ( pipe ): 传送字节流
      • 消息队列 ( message queue ): 传送结构化消息对象
    • 共享内存:共享内存区 ( share memory ),最快的一种 IPC 方法
  • 基于信号的 IPC 方法:操作系统信号(signal)机制,唯一的异步 IPC 方法
  • 基于同步的 IPC 方法:信号量(semaphore)

Go 支持的 IPC 方法有管道、信号和 socket

进程

定义

代码在进程中执行,把一个程序的执行称为一个进程,进程用于描述程序的执行过程,程序和进程分别描述一个程序的静态形态和动态特征。

进程是资源分配的最小单位,线程是CPU调度的最小单位

衍生

进程使用 fork (系统调用函数)可创建若干新进程,两者为父子关系,子进程获得父进程的数据段、堆和栈的副本,与父进程共享代码段。副本相互独立,修改其他进程(兄弟、父)不可见。

全盘复制父进程数据相当低效,Linux 内核使用 写时复制( copy on write,简称COW )等技术提高进程创建效率。刚创建的子进程也可通过 exec (系统调用)把一个新的程序加载到自己的内存中,替换原先的数据段、堆和栈及代码段,执行加载进来的新程序。

Unix/Linux 每个进程都有父进程,组成树状结构,内核启动进程为进程树根,若某父进程先于子进程结束,这些子进程(孤儿进程)会被内核进程“收养”。

进程的标识

为管理进程,内核记录每个进程的属性和行为,包括:进程优先级、状态、虚拟地址范围、各种访问权限等,这些记录在进程描述符中,进程 ID ( PID )是进程在操作系统中的唯一标识, ID 为 1 的即内核启动进程 ( init process ),新建进程 pid 为前一个进程 ID 递增的结果。当 ID 达到最大值,内核会从头查找闲置的进程 ID 并使用最先查找到的那一个作为新进程 ID。PPID 即父进程 ID

进程的状态
  • 可运行状态 ( TASK_RUNNING, 简称 R ) : 立刻或正在CPU上运行,运行时机不确定,由进程调度器决定
  • 可中断的睡眠状态(TASK_INTERRUPTIBLE,简称S ): 进程等待某个事件(如网络连接或信号量),这样的进程会放入对应事件的等待队列中,当事件发生,对应等待队列一个或多个进程被唤醒。
  • 不可中断的睡眠状态(TASK_UNINTERRUPTIBLE,简称D): 与 S 区别为不可被打断,不响应任何信号,等待某个事件,如同步的 I/O (磁盘I/O等) 操作。
  • 暂停状态或跟踪状态(TASK_ STOPPED或TASK TRACED,简称为T): 向进程发送(进程不处于 D )SIGSTOP信号,就会使该进程转入暂停状态,再向该进程发送SIGCONT信号,进程转向可运行状态。跟踪状态与暂停状态类似,但 SIGCONT信号不能使其恢复,常用于调试。
  • 僵尸状态(TASK_DEAD-EXIT ZOMBIE,简称为Z): 处于此状态的进程即将结束运行,该进程占用的绝大数资源已被回收,还有部分信息如退出码及统计信息未删除,因为父进程可能需要。该状态称为僵尸状态
  • 退出状态(TASK_ DEAD-EXIT DEAD,简称为X): 在进程退出的过程中,退出码及统计信息不需要保留。可能是该进程父进程忽略掉SIGCHLD信号(当一个进程消亡的时候,内核会给其父进程发送SIGCHLD信号以告知此情况),也可能是该进程已经被分离(分离即让子进程和父进程分别独立运行)。分离后的子程序将不会再使用和执行与父进程共享的代码段中的指令,而是加载并运行个全新的程序。 在这些情况下,该进程在退出的时候就不会转人僵尸状态,而会直接转人退出状态。处于退出状态的进程会立即被干净利落地结束掉,它占用的系统资源也会被操作系统自动回收。
进程状态转换图

进程的空间

用户空间和内核空间是操作系统在内存上划分的一个范围,共同瓜分操作系统支配的内存区域,体现 Linux 对物理内存的划分。 用户进程生存在用户空间,不能与硬件交互,内核生存在内核空间,用户进程无法直接访问内核空间。

内存区域每个单元都有地址,由指针来标识和定位。通过指针寻找内存单元的操作称为内存寻址。指针是一个正整数,如 32 位计算机标识 $$2^{32}$$ 个内存单元。

此地址非物理内存真实地址,而是虚拟地址,由虚拟地址来标识的内存区域称为虚拟地址空间,也称为虚拟内存。内核和 CPU 负责维护虚拟内存和物理内存之间的映射。

内核为每个用户进程分配的是虚拟内存而不是物理内存,用户进程分配到的虚拟内存总在用户空间,每个用户进程认为自己拥有整个用户空间,用户进程间相互不可见,因为映射至不同物理内存上。

内核会把进程的虚拟内存分为若干页(page),物理内存单元划分由 CPU 负责。 一个物理内幕才能单元被称为一个页框(page frame)。不同进程大多数页和不同页框相对应。

AB 页78 共享同一页框,共享内存区。有部分页与页框不对应,可能为数据不需要要使用,可通过页面置换算法调入调出(如LRU)。

系统调用

用户进程使用操作系统暴露的接口访问内核空间,这些接口称为系统调用

为保证操作系统稳定与安全,内核依据由 CPU 提供的、可让进程驻留的特权级别分别建立两个特权状态——内核态和用户态。大部分时间 CPU 处于用户态,CPU 只能对用户空间进行访问。 当用户进程发出系统调用时,内核会把 CPU 切换至内核态,即用户进程通过系统调用使用内核提供的功能。内核函数执行完毕后,内核会把 CPU 切回用户态,并把执行结果返回用户进程。

socket

多线程编程

多线程编程是一种比多进程更灵活、高效的的编程方式。Unix 中,POSIX 标准中定义的线程及操作方法被广泛认可和遵循。Linux 也提供以POSIX线程(以 POSIX 标准定义的线程)为中心的各种系统调用。Linux中,最贴近 POSIX 的实现为 NPTL (Native POSIX Threads Library)。

POSIX(Portable Operating System Interface of Unix),中文可以翻译为 Unix 可移植性操作系统接口,由美国的电气电子工程师学会(IEEE)为了提高各种类 Unix 操作系统下应用程序的可移植性而开发的一套规范。该规范之后被美国国家标准协会( ANSI)和国际标准化组织( ISO )标准化。

POSIX 线程是 Go 并发编程模型在 Linux 系统下真正使用的内核接口。

线程

线程可看做进程中控制流,随进程启动会创建第一个线程,称为该进程主线程,进程至少包含一个线程。通过系统调用( pthread_create ),已有线程可创建新线程。拥有多个线程的进程可并发执行多个任务,大大改善程序响应时间和吞吐量。线程不能独立进程存在,生命周期在进程内。

每个线程都有自己的线程栈,以此存储自己私有数据。这些线程栈在其所属进程的虚拟内存地址中。线程共享当前进程的虚拟内存地址中的数据段、代码段、堆、信号处理函数 及当前进程文件描述符等。因此同一进程多线程间共享数据很容易。新建线程相较于新建进程开销更小。

线程标识

每个线程都有自己的 ID ,称为线程 ID 或 TID。线程 ID 系统范围内可以不唯一,在其所属进程范围内唯一,Linux 系统线程系统范围内唯一。线程 ID可复用 (线程不存在后 ID 被其他线程使用), 由操作系统内核分配和维护。

线程间控制

进程间存在父子关系,而同一进程的任意两个线程无层级关系,相互平等。任何线程都可对同一进程中的线其他线程进行有限的管理,包括以下 4 种。

  • 创建线程

    主线程在所属进程启动时创建,对于其他线程,都可通过调用系统调用 pthread_create 创建。创建线程时,调用线程给定新线程将要执行的函数及传入该函数的参数,该函数称为 start 函数。start 函数可有返回值,其他线程通过与新线程连接得到该返回值。如果新线程创建成功,调用该线程会得到线程 ID。

  • 终止线程

    线程可通过多种方式终止同一进程中的其他线程。其中一种是调用系统调用 pthread_cancel,该函数作用是取消给定线程ID的线程。它会向目标线程发送一个终止执行请求,之后立即返回,并不会等目标线程响应,线程具体什么时候响应、做出怎样的响应,取决于其他因素,目标线程会执行到某个取消点后才会执行取消请求。

  • 连接已终止的线程

    此操作由系统调用 pthread_join 来执行,该函数会一直等待 到 给定线程ID 那个线程终止,并把该函数返回值返回给调用函数。如果目标线程已终止,那么该函数立即返回。该过程相当于控制流程的连接。如果一个线程可连接,该在它终止时就连接,否则会变为僵尸线程,导致系统资源浪费以及减少所属进程可创建的线程数量。

  • 分离线程

    此操作由系统调用 pthread_detach 来执行,参数为线程ID。 线程分离后线程不再可连接。默认情况下,线程总可被其他线程连接。分离操作的另一个作用是让系统内核在目标线程终止时自动执行清理销毁工作。分离操作不可逆,但对于一个已处于分离状态的线程,执行终止操作仍然有效。

    线程对自身也可进行两种控制:终止和分离。线程终止自身方式也有很多种。在线程执行的 start 函数中执行 return 语句,会让该线程随 start 函数结束而终止。若在主线程中执行了 return 语句, 该进程中所有线程都会终止;在任意线程中执行 系统调用 exit 也是如此(原因为进程退出)。线程终止自身还可通过 显示调用系统调用 pthread_exit,如果在主线程中调用该函数,只有主线程自己终止,其他线程仍然照常运行。线程分离自身与分离其他线程方式一致,区别仅为传入的是自己的 ID 还是其他线程的 ID 。

线程的状态

线程的调度
线程实现模型

线程的同步

共享数据的一致性
互斥量
条件变量
线程安全性

多进程与多线程

多核心时代的并发编程

Go 并发机制

评论