python greenlet背景介绍与贯彻机制

最近开始研究Python的并行开发技术,包括多线程,多进程,协程等。逐步整理了网上的一些资料,今天整理了一下greenlet相关的资料。

greenlet的实现机制

以前使用python开发web程序,一直使用的是fastcgi模式.然后每个进程中启动多个线程来进行请求处理.这里有一个问题就是需要保证每个请求响应时间都要特别短,不然只要多请求几次慢的就会让服务器拒绝服务,因为没有线程能够响应请求了.平时我们的服务上线都会进行性能测试的,所以正常情况没有太大问题.但是不可能所有场景都测试到.一旦出现就会让用户等好久没有响应.部分不可用导致全部不可用.后来转换到了coroutine,python
下的greenlet
.所以对它的实现机制做了一个简单的了解.
每个greenlet都只是heap中的一个python
object(PyGreenlet).所以对于一个进程你创建百万甚至千万个greenlet都没有问题.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct _greenlet {
    PyObject_HEAD
    char* stack_start;
    char* stack_stop;
    char* stack_copy;
    intptr_t stack_saved;
    struct _greenlet* stack_prev;
    struct _greenlet* parent;
    PyObject* run_info;
    struct _frame* top_frame;
    int recursion_depth;
    PyObject* weakreflist;
    PyObject* exc_type;
    PyObject* exc_value;
    PyObject* exc_traceback;
    PyObject* dict;
} PyGreenlet;

每一个greenlet其实就是一个函数,以及保存这个函数执行时的上下文.对于函数来说上下文也就是其stack..同一个进程的所有的greenlets共用一个共同的操作系统分配的用户栈.所以同一时刻只能有栈数据不冲突的greenlet使用这个全局的栈.greenlet是通过stack_stop,stack_start来保存其stack的栈底和栈顶的,如果出现将要执行的greenlet的stack_stop和目前栈中的greenlet重叠的情况,就要把这些重叠的greenlet的栈中数据临时保存到heap中.保存的位置通过stack_copy和stack_saved来记录,以便恢复的时候从heap中拷贝回栈中stack_stop和stack_start的位置.不然就会出现其栈数据会被破坏的情况.所以应用程序创建的这些greenlet就是通过不断的拷贝数据到heap中或者从heap中拷贝到栈中来实现并发的.对于io型的应用程序使用coroutine真的非常舒服.

下面是greenlet的一个简单的栈空间模型(from greenlet.c)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
A PyGreenlet is a range of C stack addresses that must be
saved and restored in such a way that the full range of the
stack contains valid data when we switch to it.
 
Stack layout for a greenlet:
 
               |     ^^^       |
               |  older data   |
               |               |
  stack_stop . |_______________|
        .      |               |
        .      | greenlet data |
        .      |   in stack    |
        .    * |_______________| . .  _____________  stack_copy + stack_saved
        .      |               |     |             |
        .      |     data      |     |greenlet data|
        .      |   unrelated   |     |    saved    |
        .      |      to       |     |   in heap   |
 stack_start . |     this      | . . |_____________| stack_copy
               |   greenlet    |
               |               |
               |  newer data   |
               |     vvv       |

下面是一段简单的greenlet代码:

from greenlet import greenlet

def test1():
    print 12
    gr2.switch()
    print 34

def test2():
    print 56
    gr1.switch()
    print 78

gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()

 

目前所讨论的协程,一般是编程语言提供支持的。目前我所知提供协程支持的语言包括python,lua,go,erlang, scala和rust。协程不同于线程的地方在于协程不是操作系统进行切换,而是由程序员编码进行切换的,也就是说切换是由程序员控制的,这样就没有了线程所谓的安全问题。
所有的协程都共享整个进程的上下文,这样协程间的交换也非常方便。
相对于第二种方案(I/O多路复用),使得使用协程写的程序将更加的直观,而不是将一个完整的流程拆分成多个管理的事件处理。
协程的缺点可能是无法利用多核优势,不过,这个可以通过协程+进程的方式来解决。
协程可以用来处理并发来提高性能,也可以用来实现状态机来简化编程。我用的更多的是第二个。去年年底接触python,了解到了python的协程概念,后来通过pycon china2011接触到处理yield,greenlet也是一个协程方案,而且在我看来是更可用的一个方案,特别是用来处理状态机。
目前这一块已经基本完成,后面抽时间总结一下。

总结一下
1)多进程能够利用多核优势,但是进程间通信比较麻烦,另外,进程数目的增加会使性能下降,进程切换的成本较高。程序流程复杂度相对I/O多路复用要低。
2)I/O多路复用是在一个进程内部处理多个逻辑流程,不用进行进程切换,性能较高,另外流程间共享信息简单。但是无法利用多核优势,另外,程序流程被事件处理切割成一个个小块,程序比较复杂,难于理解。
3)线程运行在一个进程内部,由操作系统调度,切换成本较低,另外,他们共享进程的虚拟地址空间,线程间共享信息简单。但是线程安全问题导致线程学习曲线陡峭,而且易出错。
4)协程有编程语言提供,由程序员控制进行切换,所以没有线程安全问题,可以用来处理状态机,并发请求等。但是无法利用多核优势。
上面的四种方案可以配合使用,我比较看好的是进程+协程的模式

我看书有个特点,不写笔记,就感觉好像没有看过书,印象不深刻(虽然写了也未必深刻),所以我看书会比较慢,笔记会很多。这里总结一下并发。
最近学习《深入理解计算机系统》,最后一章中讲到了并发。之前一直以为并发是为了提高性能,书中将并发理解为逻辑控制流在时间上的重叠。简单的说,就是让机器
能够同时处理多个事情,充分利用机器的能力。特别是现在多核机器的普遍,并发可能越来越重要。
现代操作系统提供了三种基本的方法用于构造并发:进程,I/O多路复用,线程。我们公司在工作中用的最多的其实是第二种。你也许知道,他是无法利用CPU的多核的。
不过还好,我们的服务器上一般会跑多个程序(系统划分为多个程序模块)。
某些语言也从语言层面提供了一种并发的机制:协程,比如lua和python,go好像也提供了这种机制。我比较熟悉的是python的协程。

并发处理的技术背景

并行化处理目前很受重视,
因为在很多时候,并行计算能大大的提高系统吞吐量,尤其在现在多核多处理器的时代,
所以像lisp这种古老的语言又被人们重新拿了起来, 函数式编程也越来越流行。
介绍一个python的并行处理的一个库: greenlet。 python
有一个非常有名的库叫做 stackless ,用来做并发处理,
主要是弄了个叫做tasklet的微线程的东西, 而greenlet
跟stackless的最大区别是, 他很轻量级?不够,
最大的区别是greenlet需要你自己来处理线程切换,
就是说,你需要自己指定现在执行哪个greenlet再执行哪个greenlet。

并发处理的技术背景

并行化处理目前很受重视,
因为在很多时候,并行计算能大大的提高系统吞吐量,尤其在现在多核多处理器的时代,
所以像lisp这种古老的语言又被人们重新拿了起来, 函数式编程也越来越流行。
介绍一个python的并行处理的一个库: greenlet。 python
有一个非常有名的库叫做 stackless
,用来做并发处理, 主要是弄了个叫做tasklet的微线程的东西, 而greenlet
跟stackless的最大区别是, 他很轻量级?不够,
最大的区别是greenlet需要你自己来处理线程切换,
就是说,你需要自己指定现在执行哪个greenlet再执行哪个greenlet。

总结一下:
1)多进程能够利用多核优势,但是进程间通信比较麻烦,另外,进程数目的增加会使性能下降,进程切换的成本较高。程序流程复杂度相对I/O多路复用要低。
2)I/O多路复用是在一个进程内部处理多个逻辑流程,不用进行进程切换,性能较高,另外流程间共享信息简单。但是无法利用多核优势,另外,程序流程被事件处理切割成一个个小块,程序比较复杂,难于理解。
3)线程运行在一个进程内部,由操作系统调度,切换成本较低,另外,他们共享进程的虚拟地址空间,线程间共享信息简单。但是线程安全问题导致线程学习曲线陡峭,而且易出错。
4)协程有编程语言提供,由程序员控制进行切换,所以没有线程安全问题,可以用来处理状态机,并发请求等。但是无法利用多核优势。
上面的四种方案可以配合使用,我比较看好的是进程+协程的模式。

发表评论

电子邮件地址不会被公开。 必填项已用*标注