go并发编程基础
并发其主要思想是使多个任务可以在同一时间执行以便能够更快的得到结果。并发编程的思想来自于多任务操作系统。
多任务操作系统允许同时运行多个程序。与之相对的是单用户计算机系统的操作系统,任务是被一个接一个的读取,寻找资源并运行的,各任务的运行完全是串行的。
并发程序内部会被划分为多个部分,每个部分都可以被看作是一个串行程序,在这些串行程序之间可能会存在交互的需求,这就需要操作系统去协调。在这之前,我们先来看下进程。
进程
我们通常把一个程序的执行称为一个进程,同时进程也被用来描述程序的执行过程。
一个进程可以使用系统调用fork创建若干新的进程。前者被称为后者的父进程,每一个进程都有父进程。所有的进程共同组成了一个树状结构,内核启动进程作为进程树的根并负责系统的初始化操作。它的父进程就是它自己。
为了管理进程,内核必须对每个进程的属性,行为进行详细的记录,包括进程的优先级,状态,虚拟地址范围以及各种访问权限等等。这些信息都会被记录在每个进程的进程描述符中,而被保存在进程描述符中的进程ID(常叫做PID)是进程在操作系统中的唯一标识,同时进程描述符中还会包含当前进程的父进程的ID(常被称为PPID)。
进程的状态共有6个,分别是可运行状态,可中断的睡眠状态,不可中断的睡眠状态,暂停状态或跟踪状态,僵尸状态和退出状态。
linux操作系统可以凭借cpu快速在多个进程之间切换,以产生多个进程在同时运行的假象。但切换正在运行的进程是需要付出代价的。
内核对进程的合理切换和调度使多个进程可以有条不紊的并发执行,在很多时候,多个进程之间需要相互配合并合作完成一个任务,这就需要进程间通讯机制(IPC)的支持。下面就讲一下go语言支持的IPC方法。它们是管道,信号和Socket。
管道
管道(pipe)是一种是单向的通讯方式。它只能被用于父进程与子进程以及同祖先的子进程之间的通讯。例如,我们在使用shell命令的时候常常会用到管道:
|
|
shell命令为每个命令都创建一个进程,然后把左边的命令的标准输出用管道与右边的命令的标准输入连接起来。
管道的优点在于它的简单,而缺点则是只能单向通讯以及对通讯双方关系上的严格限制。
对于管道,go语言是支持的。通过标准库代码包os/exec中的API,我们可以执行操作系统命令并在此基础上建立管道。
|
|
上面我们讲的是匿名管道,与之相对的是命名管道。与匿名管道不同,任何进程都可以通过命名管道交换数据。实际上,命名管道以文件的形式存在于文件系统中,使用它的方法与使用文件类似,linux支持使用shell命令创建和使用命名管道,例如:
|
|
在上面的实例中,我们先使用命令mkfifo在当前目录创建了一个命名管道mififo2,然后又使用这个命名管道和命名tee把tepipe.txt文件中的内容写到了of_login文件中。
这里只是使用了命名管道搬运了数据,我们也可以在此基础上实现诸如数据的过滤或转换,以及管道的多路复用等功能。注意,命名管道默认是阻塞式的,更具体的说,只有在对这个命令管道的读操作和写操作都已准备就绪后,数据才会流转。它相对于匿名管道的优势就是通讯双方可以毫不相关。但命名管道也是单向的。
|
|
信号
它是IPC中唯一一种异步的通讯方法。它的本质是利用软件来模拟硬件的中断机制。信号被用来通知某个进程有某个事件发生了。使用kill命令查看当前系统支持的信号。
linux支持的信号有62种,分别分为两大类,1到31号为标准信号,也叫不可靠信号,34到64为实时信号,也叫可靠信号。
对同一进程来说,每种标准信号只会被记录并处理一次,并且如果某一进程的标准信号种类有好多,其处理顺序也是完全不确定的。而实时信号正好相反,即同种类的多个信号都可以被记录,并且可以按照发送的顺序被处理。
进程响应信号的方式有3种:忽略,捕捉和执行默认操作 .
linux对每个标准信号都有默认的操作方式。对大多数标准信号,我们可以自定义当进程接收到他们之后进行怎样的处理。在程序中,这些作为信号响应的自定义操作往往是由函数来代表的。
go命令会对其中的一些以键盘输入为来源的标准信号作出相应。这是由于go命令使用了在标准库代码包os/signal 中的被用于处理信号的API。
下面我们看下os.Signal接口类型的声明:
|
|
从声明可知,其中的Signal()方法的声明并没有实际意义。它只是作为os.Signal接口类型的一个标识。
在标准库代码包syscall中,已经为不同的操作系统的所支持的每一个标准信号都声明了一个同名常量,其类型都为syscall.Signal——os.Signal接口类型的一个实现类型,同时也是一个int类型的别名类型。每个信号常量的整数值与他所代表的信号在操作系统中的编号一致。
代码包os/signal 中的Notify函数用来把操作系统发给当前进程的指定信号通知给该函数的调用方。声明如下:
|
|
signal处理程序在向接受通道发送值的时候,并不会因为通道已满而产生阻塞。
前面说过,大部分的标准信号我们都可以自定义其处理方法,不过有两种信号除外。SIGKILL和SIGSTOP。对他们的响应只执行系统默认操作。
对于其他信号,我们可以自行处理也可以恢复对他们的系统默认操作,这需要使用到os/signal包中的Stop函数。声明如下:
|
|
只需将Notify中的输入通道作为参数传入即可取消对这些信号的自行处理。
当然,我们也可以只对部分信号取消自定义处理,这时可以重新调用Notify函数,只需要第一个参数相同即可。
下面通过一个程序来实现以下功能:
1.执行一系列操作系统命令并获取演示进程的进程ID;
2.使用该进程值之上的API相对应的进程发送一个SIGINT信号,并输出演示进程已受到信号的凭证。
|
|
参考Go并发编程实战