Go并发编程概念

0 阅读16分钟

1.串行程序与并发程序:

串行程序特指只能被顺序执行的指令列表.并发程序则是可以被并发执行的两个及以

上的串行程序的综合体.并发程序允许其中的串行程序运行在一个或多个可共享的

CPU上.同时也允许每个串行程序都运行在专为它服务的CPU上.前一种方式也称为多

元程序.由来自荷兰的图灵奖得主Edsger Wybe Dijkstra在1968年提出.它由操作

系统内核支持并提供多个串行程序复用多个CPU方法.多元处理是指计算机中多个

CPU共用一个存储器(即内存).并且在同一时刻可能会有数个串行程序分别运行在不

同的CPU之上.它由美国计算机科学家Anita K. Jones和Peter M. Schwar在1980

年提出.多元程序和多元处理是串行程序得以并发甚至并行运行的基础支撑.在现代计

算机系统中.它们已经得到了很好的融合.

2.并发程序与并行程序:

并发程序是指可以被同时发起执行的程序.而并行程序则被设计成可以在并行的硬件

上执行的并发程序.并发程序代表了所有可以实现并发行为的程序.是一个比较宽泛的

概念.其中包含了并行程序.

3.并发程序与并发系统:

并发程序属于程序.即使它被划分为许多部分(可以是规模更小的程序).只要这些部分

之间是紧密关联在一起.并且可以看作一个整体.那么它就属于一个程序.也可以称为一

个内聚软件单元.另一方面.程序与程序之间可以通过协商一致的协议进行通信.并且它

们之间是松耦合的.它们可以看作一个系统.而不是程序.并发程序和并发系统中的并发

的含义是一致的.但是.并发系统更有可能是并行的.因为其中的多个程序一般可以同时

在不同的硬件环境上运行.并发系统也常常称为并行系统.与并发系统同义的一个更加

流行的词是分布式系统.

4.并发程序的不确定性:

串行程序中所有代码的先后顺序都是固定的.而并发程序中只有部分代码是有序的.也

就是说.其中一些代码的执行顺序并没有明确指定.这一特性被称为不确定性.这导致并

发程序每次运行的代码执行路径都是不同的.即便是在输入数据相同的前提下.也是如

此.

5.并发程序内部的交互:

并发程序内部会被划分为多个部分.每个部分都可以看作一个串行程序.在这些串行程

序之间.可能会存在交互的需求.比如.多个串行程序可能都要对一个共享的资源进行访

问.又比如.它们需要相互传递一些数据.在这些情况下.就需要协调它们的执行.这就涉

及同步.同步的作用就是避免在并发访问共享资源时可能发生的冲突.以确保有条不紊

的传递数据.

感觉同步原则.程序如果想使用一个共享资源.就必须先请求该资源并获取到对它的访

问权.当程序不在需要某个资源的时候.它应该放弃对该资源的访问权(也称作释放资

源).一个程序对资源的请求不应该导致其他正在访问该资源的程序中断.而应该等到那

个程序释放资源之后再进行请求.在同一时刻.某个资源应该只被一个程序占用.

传递数据是并发程序内部的另一种交互方式.也称为并发程序内部的通信.协调这种内

部通信的方式不只"同步"这一种.也可以通过异步的方式对通信进行管理.这种方式使

得数据可以不加延迟的发送给数据接收方.即使数据接收方还没有为接收数据做好准

备.也不会造成数据发送方的等待.数据会临时存放在一个称为通信缓存的数据结构中.

通信缓存是一种特殊的共享资源.它可以同时被多个程序使用.数据接收方可以在准备

就绪之后按照数据存入通信缓存的顺序接收它们.

6.进程的定义:

进程是Unix及其衍生操作系统(包括linux操作系统)的根本.因为所有的代码都是在进

程中执行的.通常把一个程序的执行称为一个进程.反过来讲.进程用于描述程序的执行

过程.程序和进程是一对概念.它们分别描述了一个程序的静态形式和动态特征.除此之

外.进程还是操作系统进行资源分配的一个基本单位.

7.进程的衍生:

进程使用fork(一个系统调用函数)可以创建若干个新的进程.其中前者称为后者的父

进程.后者称为前者的子进程.每个子进程都是源自它父进程的一个副本.它会获得父进

程的数据段 堆和栈的副本.并与父进程共享代码段.每一份副本都是独立的.子进程对

属于它的副本的修改对其父进程和兄弟进程(同父进程)是不可见的.反之亦然.全盘复

制父进程的数据是一种相当低效的做法.Linux操作系统内核(以下简称内核)使用写时

复制(Copy on Write.简称COW)等技术来提高进程创建的效率.刚创建的子进程也

可以通过系统调用exec把一个新的程序加载到自己的内存中.而原先在其内存中的数

据段 堆 栈以及代码段就会被替换掉.在这之后.子进程执行的就会是那个刚刚加载进

来的新程序.

Unix/Linux操作系统中的每一个进程都有父进程.所有的进程共同组成了一个树状结

构.内核启动进程作为进程树的根.负责系统的初始化操作.它是所有进程的祖先.它的

父进程就是它自己.如果某一个进程先于它的子进程结束.那么这些子进程就会被内核

启动进程"收养".成为它的直接子进程.

8.进程的标识:

为了管理进程.内核必须对每个进程的属性和行为进行详细的记录.包括进程的优先级

状态 虚拟地址范围以及各种访问权限.等等.这些信息都会被记录在每个进程的描述符

中.进程描述符并不是一个简单的符号.而是一个复杂的数据结构.保存在进程描述符中

的进程ID(常称为PID)是进程在操作系统中的唯一标识.其中进程ID为1的进程就是之

前提到的内核启动进程.进程ID是一个非负整数且总是顺序的编号.新创建的进程ID总

是前一个进程ID递增的结果.此外进程ID也可以重复利用.当进程ID达到其最大限值

时.内核会从头开始查找闲置的进程ID并使用最先找到的那个作为新进程的ID.进程描

述符中还会包含当前进程的父进程的ID(常称为PPID).

func main() {
    pid := os.Getpid()
    ppid := os.Getppid()
    fmt.Println(pid, ppid)
}

注:PID并不传达与进程相关的任何信息.它只是一个用来唯一标识进程的数字而已.进

程属性信息只包含在内核中与PID对应的进程描述符里.PPID体现了两个进程之间的

亲缘关系.

进程ID对内核以外的程序非常有用.内核可以高效的把进程ID转换成对应进程的描述

符.可以shell命令kill终止某个进程ID对应的进程.还可以通过进程ID找到对应的进程

并向它发送信号.

9.进程状态:

在Linux操作系统中.每个进程在每个时刻都是有状态的.可能状态共有六个.分别是可

运行状态 可中断的睡眠状态  暂停状态或跟踪状态 僵尸状态和退出状态.

1).可运行状态(TASK_RUNNING.简称为R):如果一个进程处于在该状态.那么说明

它立刻要或正在CPU上运行.不过运行的时机是不确定的.这由进程调度器决定.

2).可中断的睡眠状态(TASK_INTERRUPTIBLE.简称为S):当进程正在等待某个事件

(比如网络连接或信号量)到来时.会进入此状态.这样的进程会被放入对应事件的等待

队列中.当事件发生.对应的等待队列中的一个或多个进程就会被唤醒.

3).不可中断的睡眠状态(TASK_UNINTERRUPTIBLE.简称为D):此种状态与可中断

的睡眠状态的唯一区别就是它不可被打断.这意味着处在此种状态的进程不会对任何

信号做出响应.发送给此状态的进程的信号直到它从该状态转出才会被传递过去.处于

此状态的进程通常是在等待一个特殊的事件.比如等待同步的I/O操作(磁盘I/O等)完

成.

4).暂停状态或跟踪状态(TASK_STOPPED或TASK_TRACED.简称为T):向进程发送

SIGSTOP信号.就会使该进程转入暂停状态.除非该进程正处于不可中断的睡眠状态.

向正处于暂停状态的进程发送SIGCONT信号.会使该进程转向可运行状态.处于该状

态的进程会暂停.并等待另一个进程(跟踪它的那个进程)对它进行操作.例如.使用调试

工具GDB在某个程序中设置一个断点.而后对应的进程在运行到该断点处就会停下来.

这时.该进程处于跟踪状态.跟踪状态与暂停状态非常类似.向处于跟踪状态的进程发送

SIGCONT信号并不能使它恢复.只有当调试进程进行了相应的系统调用或者退出后,

它才能恢复.

5).僵尸状态(TASK_DEAD_EXIT_ZOMBIE.简称为Z):处于此状态的进程即将结束运

行.该进程占用的绝大多数资源也都已经被回收.不过还有一些信息未删除.比如退出码

以及一些统计信息.之所以保留这些信息.主要是考虑到该进程的父进程可能需要它们.

由于此时的进程主体已经被删除而只留下一个空壳.故此状态称为僵尸状态.

6).退出状态(TASK_DEAD_EXIT_DEAD.简称为X):在进程退出的过程中.有可能连退

出码和统计信息都不需要保留.造成这种情况的原因可能是显示的让该进程的父进程

忽略掉SIGCHLD信号(当一个进程消亡的时候.内核会给其父进程发送SIGCHLD信号

以告知此情况).也可能是该进程已经被分离(分离即让子进程和父进程分别独立运行).

分离后的子程序将不会在使用和执行与父进程共享的代码段指令.而是加载并运行一

个全新的程序.在这些情况下.该进程在退出的时候就不会转入僵尸状态.而会直接转入

退出状态.处于退出状态的进程会立即被干净利落的结束掉.它占用的系统资源也会被

操作系统自动回收.

流程图:

10.进程的空间:

用户进程(或者说程序的执行示例)总会生成在用户空间中.它可以做很多事.但是却不

能与其所在的计算机硬件进行交互.内核可以与硬件交互.但是它却生存在内核空间中.

用户进程无法直接访问内核空间.用户空间和内核空间都是操作系统在内存上划分出

一个范围.它们共同瓜分了操作系统能够支配的内存区域.体现了Linux操作系统对物

理内存的划分.

内存区域中的每一个单元都是有地址的.这些地址由指针来标识和定位.通过指针来寻

找内存单元的操作也称为内存寻址.指针是一个正整数.由若干个二进制位表示.具体的

二进制位的数量由计算机(更确切的说是CPU)的字长决定.因此.在32位计算机中可以

有效标识2的32次方个内存单元.而在64位计算机中可以有效表示2的64次方个内存

单元.

这里说的地址并非物理内存中的真实地址.而是虚拟地址.由虚拟地址来标识的内存区

域又称为虚拟地址空间.有时也称为虚拟内存.

注:虚拟内存的最大容量与实际可用的物理内存大小无关.内核和CPU会负责维护虚拟

内存与物理内存之间的映射关系.

内核会为每个用户进程分配的是虚拟内存而不是物理内存.每个用户进程分配到的虚

拟内存总是在用户空间中.而内核空间则留给内核专用.每个用户进程都认为分配给它

的虚拟内存就是整个用户空间.一个用户进程不可能操纵另一个用户进程的虚拟内存.

因为后者的虚拟内存对于前者来说是不可见的.换句话说.这些进程的虚拟内存几乎是

彼此独立的 互不干扰的.这是由于她们基本上被映射到了不同的物理内存之上.

内核会把进程的虚拟内存划分为若干页.而物理内存单元的划分由CPU负责.一个物理

内存单元被称为一个页框.不同进程的大多数页都会与不同的页框对应.

11.系统调用:

用户进程生存在用户空间中且无法直接操作计算机硬件.但是内核空间中的内核却可

以做到.用户进程无法直接访问内核空间.也无法随意指使内核去做它能做的一些时.为

了使用户进程能够使用操作系统更加底层的功能.内核会暴露出一些接口供他们使用.

这些接口是用户进程使用内核功能(包括操作计算机硬件)的唯一手段.也是用户空间

和内核空间的一座桥梁.用户进程使用这些接口的行为称为系统调用.不过在很多时

候"系统调用"这个词也指内核提供的这些接口.注意.虽然系统调用也是由函数呈现.但

它与普通函数有明显区别.系统调用是向内核空间发出一个明确的请求.而普通函数只

是定义了如何获取一个给定的服务.更重要的是系统调用会导致内核空间中数据的存

取和指令的执行.而普通函数却只能在用户空间中有所作为.如果在一个函数的函数体

中包含了系统调用.那么它执行也将涉及对内核空间的访问.但是这种访问仍然是通过

函数体内的系统调用来完成的.系统调用是内核的一部分.普通函数却不是.

说到系统调用.就不得不提及另外一对概念----内核态和用户态.为了保证操作系统的

稳定和安全.内核依据由CPU提供的 可以让进程驻留的特权级别建立了两个特权状

态---内核态和用户态.大部分时间里.CPU都处于用户态.这时CPU只能对用户空间进

行访问.CPU在用户态下运行的用户进程是不能与内核接触的.当用户进程发出一个系

统调用的时候.内核会把CPU从用户态切换到内核态.而后会让CPU执行对应的内核函

数.CPU在内核状态下是有权限访问内核空间的.这就相当于用户进程可以通过系统调

用使用内核提供的功能.当内核函数执行完毕后.内核会把CPU从内核态切换回用户态.

并把执行结果返回给用户进程.

12.进程的切换和调度:

与其他分时操作系统一样.Linux操作系统也可以凭借CPU的威力快速的再多个进程

之间切换.这也称为进程间的上下文切换.如此会产生多个进程同时运行的假象.而每个

进程都会认为自己独占了CPU.这就是多任务操作系统称谓的由来.不过.无论切换速度

如何.在同一时刻正在运行的进程仅会有一个.

切换CPU正在运行的进程是需要付出代价的.内核此刻要换下正在CPU上运行的进程

A.并让CPU开始运行进程B.再换下进程A之前.内核必须要及时保存进程A的运行时状

态.另一方面.假设进程B不是第一次运行.那么让进程B重新运行之前.内核必须保证已

经依据之前保存的相关信息把进程B恢复到之前被换下时的运行时状态.这种在进程换

出换入期间必须要做的任务称为进程切换.这个任务主要还是由内核完成.除了进程切

换.为了使各个生存着的进程都有运行的机会.内核还要考虑下次切换时运行哪个进程

何时进行切换 被换下的进程何时在换上.等等.解决类似问题的方案和任务统称为进程

调度.

语雀地址www.yuque.com/itbosunmian…?

《Go.》 密码:xbkk 欢迎大家访问.提意见.

悠悠的往事回流的水.