协程,线程和进程

老生常谈的问题

这个可能是在Python或Golang岗位中最普遍的一个面试题目:“协程,线程和进程分别是什么,他们有什么区别?”。这种问题我遇到了至少四五次,因此在这里进行记录。

进程

进程是由操作系统进行资源分配和调度的一个独立单位。每个进程都有自己独立的内存空间,他们之间通过进程间通讯(RPC远程调用,MQ消息队列,PIPE匿名管道,FIFO有名管道,Signal信号,Shared Memory共享内存,Semaphore信号量,Socket套接字)的手段进行通讯。当然了,即使使用文件IO也能到达这个效果。

线程

线程是进程的一个实体,是CPU调度的基本单位。而且线程基本不拥有自己的独立资源(除计数器,寄存器,栈等),可以和同一个进程内的所有线程共享进程的全部资源。他们可以通过全局变量,队列,管道进行通讯。

协程

协程是一个用户态的轻量级线程,调度完全有用户控制(也就是讲程序员无法控制线程和进程)。

对比中的区别

从上面的定义就能看出一些不同:

  1. 线程是进程内的一个执行单元,进程内至少有一个线程,多个线程共享进程的地址空间,而进程有自己独立的地址空间。
  2. 进程是资源分配的单位,同一个进程内的线程共享资源。
  3. 线程是CPU调度的基本单位。
  4. 协程存在于线程中,线程存在于进程中。
  5. 一个线程可已有多个协程,相应的一个进程也可以有多个协程。
  6. 线程进程都是使用同步机制,协程使用异步机制。

一些实例

进程

通过RPC通讯

9012年了为什么还要使用RESTful,RESTful作为业界规范确实十分合理,但在开发中并不是十分友善。他使用Json很丑陋的模拟SQL管道。如果功能复杂,很可能在URL的设计上就会出很多问题。

这里使用了最简单的zerorpc的框架,在单机情况下,我们的代码很可能会这样写:

import foo
foo.todo()

但在分布式我们可以使用:

foo = Remote()
foo.todo()

并且这种东西应该是语言无关,因此无论选择Python或者是NodeJS或者其他什么语言都可以。

import zerorpc

class Cooler():
    def hello(self, name):
        return 'hello {name}'.format(name=name)

s = zerorpc.Server(Cooler())
s.bind("tcp://0.0.0.0:4242")
s.run()

之后在命令行中:

zerorpc tcp://127.0.0.1:4242 hello jamchoi
connecting to "tcp://127.0.0.1:4242"
'hello jamchoi'

当然这些操作也完全可以在另外一个Python代码中实现:

In [6]: import zerorpc

In [7]: c = zerorpc.Client()

In [8]: c.connect('tcp://127.0.0.1:4242')
Out[8]: [None]

In [9]: c.hello('jamchoi')
Out[9]: 'hello jamchoi'

RPC我个人觉得一般用于远程调用,如果是通讯并不是十分合理。这主要是因为RPC采用同步调用,C端会十分依赖S端的处理速度。

使用MQ通讯

MQ的数据传递采用的是:

Sender -> Queue -> Receiver

的方式,即Sender发送消息给Queue;Receiver从Queue拿到消息来处理。
当要使用通讯的时候,Request-Reply的模式是最为简单的,这种模式情况下,Client在请求后,必须要求服务端响应:

# server
import zmq

content = zmq.Context()
socket = content.socket(zmq.REP)
socket.bind('tcp://127.0.0.1:4242')
while True:
    msg = socket.recv()
    print(msg)
    socket.send_string("jamchoi")
In [1]: import zmq

In [2]: content = zmq.Context()

In [3]: socket = content.socket(zmq.REQ)

In [4]: socket.connect('tcp://127.0.0.1:4242')

In [5]: socket.send_string('send message by ZeroMQ')

In [6]: socket.recv()
Out[6]: b'jamchoi'

这时在命令行中,会出现Client的请求信息b'send message by ZeroMQ'

PIPE

from multiprocessing import Pipe, Process

def proc_1(pipe):
    pipe.send('hello')
    print('proc_1 :', pipe.recv())

def proc_2(pipe):
    pipe.send('jamchoi')
    print('proc_2 :', pipe.recv())

if __name__ == "__main__":
    # 创建一个双向管道
    pipe = Pipe()
    # pipe[0]和pipe[1]分别表示管道的两端
    p1 = Process(target=proc_1, args=(pipe[0], ))
    p2 = Process(target=proc_2, args=(pipe[1], ))
    p1.start()
    p2.start()

    p1.join()
    p1.join()

实际上,管道的操作类似于我们初中上课时偷偷给心仪的女生写小纸条时帮我们传递的那位同学(管道是个缓冲区)。那位雷锋同学为我和她服务(管道两端链接两个进程),只需要一个同学就可以,他可以帮我们传递多次纸条(管道是一种环形结构,可以循环使用)。但是在我勾搭她失败之后,那位同学也就不必再传纸条了(程序结束后,管道也会自动消失)。

FIFO

FIFO是一个先进先出(First In First Out)的数据结构。这种特点很容易就会联想到Queue。

from multiprocessing import Queue, Process
import time
import os

def _input(queue):
    info = 'pid: {pid}, input_time: {input_time}'.format(
        pid=os.getpid(), input_time=time.ctime())
    queue.put(info)
    print('input', info)

def _output(queue):
    info = queue.get()
    print('output', info)
    queue.put(str(time.ctime()))

if __name__ == "__main__":
    queue = Queue()
    p1 = Process(target=_input, args=(queue, ))
    p2 = Process(target=_output, args=(queue, ))
    p1.start()
    p2.start()

    p1.join()
    p1.join()

Signal

信号(Signal)是Unix-like系统和Linux系统中常见的的一种进程通讯方式。比如常用的kill -9 pid中的-9,就是采用了这种方式。可以通过man signal查看到当前系统的各个信号的定义。信号的全称时软中断信号,本质时在软件层面上对硬件中断的模拟。

除了使用查看man文档的方式外,也可以通过kill -l和Python的方法看到信号的定义:

~  kill -l
HUP INT QUIT ILL TRAP ABRT EMT FPE KILL BUS SEGV SYS PIPE ALRM TERM URG STOP TSTP CONT CHLD TTIN TTOU IO XCPU XFSZ VTALRM PROF WINCH INFO USR1 USR2
In [1]: import signal

In [2]: dir(signal)
Out[2]:
['Handlers',
 'ITIMER_PROF',
 'ITIMER_REAL',
 'ITIMER_VIRTUAL',
 'ItimerError',
 'NSIG',
 'SIGABRT',
 'SIGALRM',
 'SIGBUS',
 'SIGCHLD',
 'SIGCONT',
 'SIGEMT',
 'SIGFPE',
 'SIGHUP',
 'SIGILL',
 'SIGINFO',
 'SIGINT',
 'SIGIO',
 'SIGIOT',
 'SIGKILL',
 'SIGPIPE',
 'SIGPROF',
 'SIGQUIT',
 'SIGSEGV',
 'SIGSTOP',
 'SIGSYS',
 'SIGTERM',
 'SIGTRAP',
 'SIGTSTP',
 'SIGTTIN',
 'SIGTTOU',
 'SIGURG',
 'SIGUSR1',
 'SIGUSR2',
 'SIGVTALRM',
 'SIGWINCH',
 'SIGXCPU',
 'SIGXFSZ',
 'SIG_BLOCK',
 'SIG_DFL',
 'SIG_IGN',
 'SIG_SETMASK',
 'SIG_UNBLOCK',
 'Sigmasks',
 'Signals',
 '_IntEnum',
 '__builtins__',
 '__cached__',
 '__doc__',
 '__file__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 '_enum_to_int',
 '_int_to_enum',
 '_signal',
 'alarm',
 'default_int_handler',
 'getitimer',
 'getsignal',
 'pause',
 'pthread_kill',
 'pthread_sigmask',
 'set_wakeup_fd',
 'setitimer',
 'siginterrupt',
 'signal',
 'sigpending',
 'sigwait']

Shared Memory

共享内存(Shared Memory)是最简单的进程通讯方式,他允许多个进程同时访问相同的内存,一个进程改变其中的数据后,其他进程均可看到变化。
因为这个操作纯依赖内存实现,因此可能是最快的进程间通讯的方式。