python coroutine,go routine对比--理解多进程、多线程、事件驱动、协程

1. 进程 vs 线程 vs 协程

1.1 上下文切换

linux系统中,处理器总处于以下状态中的一种:

  1. 内核态,运行于进程上下文,内核代表进程运行于内核空间;
  2. 内核态,运行于中断上下文,内核代表硬件运行于内核空间;
  3. 用户态,运行于用户空间;

一个进程的上下文可以分为三个部分:用户级上下文、寄存器上下文以及系统级上下文。

  1. 用户级上下文: 正文、数据、用户堆栈以及共享存储区;
  2. 寄存器上下文: 通用寄存器、程序寄存器(IP)、处理器状态寄存器(EFLAGS)、栈指针(ESP);
  3. 系统级上下文: 进程控制块task_struct、内存管理信息(mm_struct、vm_area_struct、pgd、pte)、内核栈。

当发生进程调度时,进行进程切换就是上下文切换(context switch).操作系统必须对上面提到的全部信息进行切换,新调度的进程才能运行。

1.2 进程和线程概念

  1. 进程是资源的容器,包含(一个或)多个线程
  2. 内核调度的基本单位是线程、而非进程
  3. 同一进程下的各个线程共享资源(address space, open files, signal handlers, etc),但寄存器、栈、PC等不共享

1.3 linux中的线程

  1. 在linux中,进程和线程都以task_struct表示,而不是为二者分别定义数据结构(如大多数操作系统)
  2. 无论进程还是线程,都是通过clone一个已存在的task实现的,差别在与参数不同(是否设置CLONE_VM–共享地址空间)。
  3. 从内核角度来看,每一个task有一个pid,同一进程下的线程共享一个TGID(Task Group ID)。这个TGID就是我们通常意义下的PID

1.4 LINUX线程切换开销

  1. 线程切换只能在内核态完成,如果当前用户处于用户态,则必然引起用户态与内核态的切换。
  2. 线程信息需要用一个task_struct保存,线程切换时,必然需要将旧线程的task_struct从内核切出,将新线程的切入,带来上下文切换。除此之外,还需要切换寄存器、程序计数器、线程栈(包括操作栈、数据栈)等。
  3. 线程调度算法需要管理线程的状态、等待条件等,如果根据优先级调度,则还需要维护优先级队列。如果线程切换比较频繁,该成本不容小觑。

就时间开销而言,Linux 下创建进程跟线程差不多,事实上创建线程的时间开销略微大一点点。
就空间开销而言,Linux 下创建进程总是有开销的,而且显然会略大于线程。

1.4 事件驱动

事件驱动模型还有另外一个名字,而且更加出名的名字:io多路复用。

在最开始的时候,为了实现一个服务器可以支持多个客户端连接,人们想出了fork/thread等办法,当一个连接来到的时候,就fork/thread一个进程/线程去接收并且处理请求。

当客户端连接越来越多,多进程/多线程模型无法承受了。因为进程/线程切换的开销太大。IO多路复用机制(即事件驱动架构)被发明出来了,简单的说就是由一些事件发生源来产生事件,由事件收集器来收集、分发事件,然后由事件处理器来处理这些事件(事件处理器需要先在事件收集器里注册自己想处理的事件)。

多个客户端连接的情况下,事件驱动虽然可以提高效率,但是事件驱动模型本身是无法利用多核CPU的。所以高性能web服务器一般采用多进程/多线程+事件驱动一起使用(比如nginx)。

2. python2 协程

python的协程,其实就是一种io多路复用。
python2的协程源于yield指令,实现起来有点不容易理解。

yield有两个功能:

  • yield item用于产出一个值,反馈给next()的调用方。
  • 作出让步,暂停执行生成器,让调用方继续工作,直到需要使用另一个值时再调用next()。

我们以一个经典的生产者、消费者模型举例:
生产者生产消息后,直接通过yield跳转到消费者开始执行
待消费者执行完毕后,切换回生产者继续生产

import time

def consumer():
    r = ''
    while True:
        n = yield r # consumer函数是一个generator(生成器),consumer通过yield拿到消息,处理,又通过yield把结果传回;
        if not n:
            return
        print('[CONSUMER] Consuming %s...' % n)
        time.sleep(1)
        r = '200 OK'

def produce(c):
    c.next() # 首先调用c.next()启动生成器;
    n = 0
    while n < 5:
        n = n + 1
        print('[PRODUCER] Producing %s...' % n)
        r = c.send(n) # 一旦生产了东西,通过c.send(n)切换到consumer执行;
        print('[PRODUCER] Consumer return: %s' % r)
    c.close() # produce决定不生产了,通过c.close()关闭consumer,整个过程结束。


if __name__=='__main__':
    c = consumer()
    produce(c)

执行结果

[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK

整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。

3. python3 协程

实现协作式多任务,在Python3.5正式引入了 async/await表达式,使得协程正式在语言层面得到支持和优化,大大简化之前python2的yield写法。

import asyncio


async def compute(x, y):
    print("Compute %s + %s ..." % (x, y))
    await asyncio.sleep(x + y)
    return x + y


async def print_sum(x, y):
    result = await compute(x, y)
    print("%s + %s = %s" % (x, y, result))


loop = asyncio.get_event_loop()
tasks = [print_sum(1, 2), print_sum(3, 4)]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

python协程,由语言的运行时中的 EventLoop(事件循环)来进行调度。和大多数语言一样,在 Python 中,协程的调度是非抢占式的,也就是说一个协程必须主动让出执行机会,其他协程才有机会运行。让出执行的关键字就是 await。也就是说一个协程如果阻塞了,持续不让出 CPU,那么整个线程就卡住了,没有任何并发。

作为服务端,event loop最核心的就是IO多路复用技术,所有来自客户端的请求都由IO多路复用函数来处理;

作为客户端,event loop的核心在于利用Future对象延迟执行,并使用send函数激发协程,挂起,等待服务端处理完成返回后再调用CallBack函数继续下面的流程。

4. go协程

Golang的一大特色就是其简单高效的天然并发机制,使用goroutine和channel实现了CSP模型。

4.1 goroutine

Go天生在语言层面支持,和Python类似都是采用了关键字,而Go语言使用了go这个关键字,可能是想表明协程是Go语言中最重要的特性。

package main

import (
    "fmt"
    "time"
)

func f(from string) {
    for i := 0; i < 3; i++ {
        fmt.Println(from, ":", i)
    }
}

func main() {

    f("direct")

    go f("goroutine")

    go func(msg string) {
        fmt.Println(msg)
    }("going")

    time.Sleep(time.Second)
    fmt.Println("done")
}

python coroutine和goroutine 虽然都叫做协程,但其实完全不一样。

python coroutine 是严格的 1:N 关系,也就是一个线程对应了多个协程。多个协程运行在同一个线程中。同一时刻其实只有一段代码运行,并没有真正实现并行,只能算是并发。而且在用 asyncio 时,必须要使用异步库。

goroutine 是真正意义上的并行,是 M:N 的关系,也就是 N 个协程会映射分配到 M 个线程上。goroutine 有两点好处:

  • 多个线程能分配到不同核心上,CPU 密集的应用使用 goroutine 也会获得加速.
  • 即使有少量阻塞的操作,也只会阻塞某个 worker 线程,而不会把整个程序阻塞。

4.2 channel

Go 语言中最常见的、也是经常被人提及的设计模式就是 — 不要通过共享内存的方式进行通信,而是应该通过通信的方式(CSP)。

goroutine通过channel的方式进行通信,channel分为两种。

  • 同步 Channel — 不需要缓冲区,发送方会直接将数据交给(Handoff)接收方;
    默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须有接收端相应的接收数据。
  • 异步 Channel — 基于环形缓存的传统生产者消费者模型;
    带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。
ch <- v    // 发送值v到Channel ch中
v := <-ch  // 从Channel ch中接收数据,并将数据赋值给v

你可以把channel看成一个管道,它的操作符是箭头 <- ,箭头的指向就是数据的流向。

目前的 Channel 收发操作均遵循了先入先出(FIFO)的设计,具体规则如下:

  • 先从 Channel 读取数据的 Goroutine 会先接收到数据;
  • 先向 Channel 发送数据的 Goroutine 会得到先发送数据的权利;

python coroutine,go routine对比--理解多进程、多线程、事件驱动、协程_第1张图片

以一个简单的channel应用开始,使用goroutine和channel实现一个任务队列,并行处理多个任务。

package main

import (
    "fmt"
    "time"
)

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Println("worker", id, "started  job", j)
        time.Sleep(time.Second)
        fmt.Println("worker", id, "finished job", j)
        results <- j * 2
    }
}

func main() {

    const numJobs = 5
    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)

    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)

    for a := 1; a <= numJobs; a++ {
        <-results
    }
}

从上面的代码可以看出,使用golang的goroutine和channel可以很容易的实现一个生产者-消费者模式的任务队列,相比Java, c++简洁了很多。

同样,在golang中,阻塞/非阻塞、超时、同步等机制,利用channel都能很简单的实现。

参考:

进程和线程之间有什么根本性的区别?

python的协程与golang的协程有什么区别吗?总感觉不太一样,但又说不出哪里不一样?

事件驱动与协程:基本概念介绍

Go by Example: Worker Pools

Go Channel 详解

Go by Example

你可能感兴趣的:(编程)