2019独角兽企业重金招聘Python工程师标准>>>

原文链接:http://bbs.ajaxjs.com/viewthread.php?tid=3542

我们在设计一个服务器的软件架构的时候,通常会考虑几种架构:多进程,多线程,非阻塞/异步IO(callback) 以及Coroutine模型。

多进程
这种模型在linux下面的服务程序广泛采用,比如大名鼎鼎的apache。主进程负责监听和管理连接,而具体的业务处理都会交给子进程来处理。这里有一篇我以前写的文章具体的解释这种架构的实现。
这种架构的最大的好处是隔离性,子进程万一crash并不会影响到父进程。缺点就是对系统的负担过重,想像一下如果有上万的连接,会需要多少进程来处理。所以这种模型比较合适那种不需要太多并发量的服务器程序。另外,进程间的通讯效率也是一个瓶颈之一,大部分会采用share memory等技术来减低通讯开销。
多线程
这种模型在windows下面比较常见。它使用一个线程来处理一个client。他的好处是编程简单,最重要的是你会有一个清晰连续顺序的work flow。简单意味着不容易出错。
这种模型的问题就是太多的线程会减低软件的运行效率。
多进程和多线程都有资源耗费比较大的问题,所以在高并发量的服务器端使用并不多。这里我们重点来研究一下两种架构,基于callback和coroutine的架构。

Callback- 非阻塞/异步IO
这种架构的特点是使用非阻塞的IO,这样服务器就可以持续运转,而不需要等待,可以使用很少的线程,即使只有一个也可以。需要定期的任务可以采取定时器来触发。把这种架构发挥到极致的就是node.js,一个用javascript来写服务器端程序的框架。在node.js中,所有的io都是non- block的,可以设置回调。举个例子来说明一下。

传统的写法:

  1. var file = open(‘my.txt’);
  2. var data = file.read(); //block
  3. sleep(1);
  4. print(data); //block

node.js的写法:

  1. fs.open(‘my.txt’,function(err,data){
  2. setTimeout(1000,function(){
  3. console.log(data);
  4. }
  5. }); //non-block

这种架构的好处是performance会比较好,缺点是编程复杂,把以前连续的流程切成了很多片段。另外也不能充分发挥多核的能力。

Coroutine-协程
coroutine本质上是一种轻量级的thread,它的开销会比使用thread少很多。多个coroutine可以按照次序在一个thread里面执行,一个coroutine如果处于block状态,可以交出执行权,让其他的coroutine继续执行。使用coroutine可以以清晰的编程模型实现状态机。让我们看看Lua语言的coroutine的例子:

  1. > function foo()
  2. >> print("foo", 1)
  3. >> coroutine.yield()
  4. >> print("foo", 2)
  5. >> end
  6. > co = coroutine.create(foo) -- create a coroutine with foo as the entry
  7. > = coroutine.status(co)
  8. suspended
  9. > = coroutine.resume(co) <--第一次resume
  10. foo 1
  11. > = coroutine.resume(co) <--第二次resume
  12. foo 2
  13. > = coroutine.status(co)
  14. dead

Google go语言也对coroutine使用了语言级别的支持,使用关键字go来启动一个coroutine(从这个关键字可以看出Go语言对coroutine 的重视),结合chan(类似于message queue的概念)来实现coroutine的通讯,实现了Go的理念 ”Do not communicate by sharing memory; instead, share memory by communicating.”。
一个例子:

  1. func ComputeAValue(c chan float64) {
  2. // Do the computation.
  3. x := 2.3423 / 534.23423;
  4. c <- x;
  5. }
  6. func UseAGoroutine() {
  7. channel := make(chan float64);
  8. go ComputeAValue(channel);
  9. // do something else for a while
  10. value := <- channel;
  11. fmt.Printf("Result was: %v", value);
  12. }

coroutine的优点是编程简单,流程清晰。但是内存消耗会比较大,毕竟每个coroutine要保留当前stack的一些内容,以便于恢复执行。

Callback vs Coroutine
在业务流程实现上,coroutine确实是更理想的实现,基于callback的风格,代码确实不是那么清晰,你可能会写出这样的代码。

  1. //pseudo code
  2. socket.read(function(data){
  3. if(data==’1’)
  4. db.query(data,function(res){
  5. socket.write(res,function(){
  6. socket.read(function(data){
  7. });
  8. });
  9. });
  10. else
  11. doSomethingelse(...);
  12. });

当然你可以使用独立的function函数来代替匿名的函数获得更好的可读性。如果使用coroutine就获得比较清晰的模型。

  1. //pseudo code
  2. coroutine handle(client){
  3. var data = read(client); //coroutine will yield when read, resume when complete
  4. if(data==’1’){
  5. res = db.query(data);
  6. }
  7. else{
  8. doSomething();
  9. }
  10. }

复制代码

但是现实世界中,coroutine到目前为止并没有真正流行起来,第一,是因为支持的语言并不是很多,比较新的语言(python/lua/go /erlang)才支持,但是老一些的语言(java/c/c++)并没有语言级别的支持。第二个原因是因为coroutine的使用可能让一些糟糕的代码占用过多的内存,而且比较难于排查。另外在实现一个工作流的构架中,流的暂停和恢复的时机都是未知的,系统的状态并不能放在内存中存放,都必须序列化,所以coroutine本身要提供序列化的机制,才可以实现稳定的系统。我想这些就是coroutine叫好不叫座的原因。
尽管有很多人要求node.js实现coroutine,Node.js的作者Ryan dahl在twitter上说: ”i’m not adding coroutines. stop asking”.至于他的理由,他在google group上提到:
Yes, there have been discussions. I think, I won’t be adding
coroutines. I don’t like the added machinery and I feel that if I add
them, then people will actually use them, and then they’ll benchmark
their programs and think that node is slow and/or hogging memory. I’d
rather force people into designing their programs efficiently with
callbacks.

我想这是一种风格的选择,优劣并不是绝对的。

4 comments

  • 深夜两点 (160 days)

    Callback vs Coroutine
    真没看出来coroutine好在什么地方。之前没用过coroutine,但是如果仅仅是根据这一段的描述,我看不出在代码结构和业务流程上coroutine有什么优势。可能我习惯callback的思想了吧。
    其实即使是callback,提供多线程支持也不难,依然可以充分利用多核。

  • Raymond (144 days)
    对于callback的评论:另外也不能充分发挥多核的能力。
    不能赞同,因为能不能发挥多核跟callback是没有关系的,跟框架本身是否支持多核有关

    • JerryTian (93 days)
      赞成Raymond关于callback与多线程执行限制之间没有必然的联系的观点;相反,由于在利用callback进行设计时,会尽量避免或更谨慎的使用共享状态,实现时相比其它范式可能会更便捷。
  • tinysche (115 days)

    协程有个麻烦,每个协程要有一个堆栈,有个内存空间占用的问题;如果是动态编译成机器代码的脚本还需内核提供堆栈保护,以防运行时堆栈溢出。回调好处是切换快速,所有回调共用一个堆栈,缺点是编程要有状态机的脑子。在实现上面,可以用异步回调来实现协程,mozilla apr上的用户态线程,就是如此。

先总结一下。

线程是最容易编写的并发方式,操作系统也提供了最好的支持;协程可以做到更强的并发能力,但需要实现调度器;回调是开销最小的,它本来不是特别为并发来设计的,它是通用的异步化操作的实现模型。

注意线程和协程本身也是使用异步来模拟同步,线程由操作系统来模拟,协程由用户级调度器模拟。模拟的过程是:发起事件请求、挂起当前执行过程(线程或协程)、响应事件请求、恢复挂起的执行过程。回调没有这么复杂,你需要自己把连续的执行过程分解成多步操作。

线程就不讨论了,用起来比较简单;协程之前简单研究了一下,切换开销比线程有很大改进,但还是有点大,用作IO事件调度还可以,粒度更小的操作就显得开销过大了,想象一下极端情况下每次调度只处理一字节;回调看起来是个不错的方式,但也有许多问题需要解决。

以上次的代码为例,这里简化一下:

  1. void server_loop(int fd) {
  2. while(true) {
  3. register_read_event(fd);
  4. wait_read_event();
  5. int client = accept(fd);
  6. close(client);
  7. }
  8. }

复制代码

改成对应的callback方式,为了让代码更像是工具转过来的,我把上一篇的代码修改一下:

  1. void server_loop(int fd) {
  2. register_read_event(fd);
  3. wait_read_event({
  4. int clientfd = accept(fd);
  5. close(clientfd);
  6. server_loop(fd);
  7. });
  8. }

复制代码

这里不打算讨论while循环的转换问题,虽然这也是个难题,但可以通过设置一个变量来避开。

看一下循环里面是如何转换的?只要把wait_read_event后面的代码放进一个closure就可以了。看起来很简单,如果可以通过宏或其它语言设施通知编译器把某行后面的代码构造一个代码块,的确是会简单一些,不过目前我还不知道有哪种语言可以这样。如果没有语言可以做到这样(或者你用的语言做不到),像上面一样手工构造这个代码块也是可以接受的,毕竟它提供了高性能的并发模式。

问题还没有彻底解决,构造这个代码块实际是撕裂了整个逻辑,想象一下,如果要把register_read_event/wait_read_event和accept封装起来,隐藏这个实现过程,结果要类似这样:

  1. void server_loop(int fd) {
  2. int client = register_read_event_AND_wait_AND_accept(fd);
  3. close(client);
  4. server_loop(fd);
  5. }

复制代码

问题来了,由于不是线程和协程模式,而是异步回调模式,这么写是不可能的,你只能再构造一个代码块:

  1. void register_read_event_AND_wait_AND_accept(int fd, void delegate(int) dg) {
  2. register_read_event(fd);
  3. wait_read_event({
  4. int client = accept(fd);
  5. dg(client);
  6. }
  7. }
  8. void server_loop(int fd) {
  9. register_read_event_AND_wait_AND_accept(fd, (int client){
  10. close(client);
  11. server_loop(fd);
  12. }
  13. }

复制代码

原以为把回调过程封装起来,以后直接调用就漂亮了,但结果却是每一次包装都没有减少它,你的逻辑必须做成closure传递进去,脱离不了这种不连贯的方式。

或许让编译器来转换?还是写成这样:

  1. void server_loop(int fd) {
  2. int client = register_read_event_AND_wait_AND_accept(fd);
  3. close(client);
  4. server_loop(fd);
  5. }

复制代码

它自动转成:

  1. void server_loop(int fd) {
  2. register_read_event_AND_wait_AND_accept(fd, (int client){
  3. close(client);
  4. server_loop(fd);
  5. }
  6. }

复制代码

如果修改编译器或许能让它做到,把register_read_event_AND_wait_AND_accept声明成 asynchronized,它就自动把该调用以及之后的代码进行转换。或者只要把wait_read_event这样声明,所有调用它的方法都自动把这个属性向外传播。。。。。。

一种新的语言?

本文介绍一个使用大概10行代码, 实现一个类似老赵JSCEX[1]的工具

首先来看一下下面这段Node.js代码:

  1. var copy = function(src, dst) {
  2. var fd_src = fs.open(src, "r");
  3. var fd_dst = fs.open(dst, "w");
  4. while (true) {
  5. var buffer_size = 4096;
  6. var buffer = new Buffer(buffer_size);
  7. var bytes_read = fs.read(fd_src, buffer, 0, buffer_size);
  8. if (bytes_read > 0) {
  9. var write_at = 0;
  10. while (write_at < bytes_read) {
  11. write_at += fs.write(fd_dst, buffer, write_at, bytes_read - write_at);
  12. }
  13. } else {
  14. break;
  15. }
  16. }
  17. };

复制代码

这段看起来像是一个文件拷贝的函数, 但是这个函数在Node里面是无法运行的, 因为Node的所有函数调用都必须是异步的, 这里用到的fs.open, fs.read, fs.write都必须修改为异步调用, 修改后可运行的代码如下:

  1. var copy = function(src, dst) {
  2. fs.open(src, "r", undefined, function(err, fd_src) {
  3. fs.open(dst, "w", undefined, function(err, fd_dst) {
  4. var copy_rec = function(position) {
  5. var buffer_size = 4096;
  6. var buffer = new Buffer(4096);
  7. fs.read(fd_src, buffer, 0, buffer_size, position, function(err, bytes_read) {
  8. if (bytes_read > 0) {
  9. var write_rec = function(write_at) {
  10. if (write_at < bytes_read) {
  11. fs.write(fd_dst, buffer, 0, bytes_read, position, function(err, written) {
  12. if (written > 0) {
  13. write_rec(write_at + written);
  14. }
  15. });
  16. } else {
  17. copy_rec(position + bytes_read);
  18. }
  19. };
  20. write_rec(0);
  21. }
  22. });
  23. };
  24. copy_rec(0);
  25. });
  26. });
  27. };

复制代码

看到这里, 我想第一次接触Node的同学就已经要崩溃了, 一个简单的文件拷贝函数竟然需要嵌套4~5层函数回调, 那么更复杂的比如web servlet岂不是要嵌套10多层函数回调了?

难道没有方法让最上面的简洁明了的代码运行起来么? 下面就介绍一种方法, 不修改最开始那个简洁明了的拷贝函数, 让它直接跑起来, 并且不会阻塞整个JS引擎

首先分析一下同步调用与异步调用的区别

  1. // sync
  2. var fd = fs.read (name, mode);
  3. // use fd
  4. // async
  5. fs.read (name, mode, function (fd) {
  6. // use fd
  7. });

复制代码

仔细观察这两个调用, 会发现其实这就是一个CPS变换[2], 所以, 我们只需要做一个CPS变换的包装就可以实现同步的文件操作, 具体实现如下:

  1. var efs = {
  2. open: function (name, mode) {
  3. return call_cc(function (continuation) {
  4. fs.open(name, mode, undefined, function(err, fd_src) {
  5. continuation(fd_src);
  6. });
  7. });
  8. },
  9. read: function (fd, buffer, offset, length) {
  10. return call_cc(function (continuation) {
  11. fs.read(fd, buffer, offset, length, null, function(err, bytes_read) {
  12. continuation(bytes_read);
  13. });
  14. });
  15. },
  16. write: function (fd, buffer, offset, length) {
  17. return call_cc(function (continuation) {
  18. fs.write(fd, buffer, offset, length, null, function(err, written) {
  19. continuation(written);
  20. });
  21. });
  22. }
  23. };

复制代码

这个实现的核心就是call/cc函数[3], 如果了解lisp或者scheme的同学可能会非常熟悉这个函数, 这个函数作用是把当前正在运行的线程挂起, 然后执行call/cc本体, call/cc函数不会返回, 直到主动调用参数传进来的那个回调函数(本例中continuation变量绑定的函数), call/cc函数的返回值就是传给回调函数的参数. call/cc函数确实很难理解, 下面举例说明一下:

  1. util.log("before call/cc");
  2. var result = call_cc(function (continuation) {
  3. util.log("in call/cc before set timer");
  4. setTimeout(function () {
  5. util.log("before callback");
  6. continuation("hello world!");
  7. util.log("after callback");
  8. }, 1000);
  9. util.log("in call/cc after set timer");
  10. });
  11. util.log("after call/cc " + result);

复制代码

这段代码的输出为:

  1. 31 Oct 13:01:33 - before call/cc
  2. // 运行call/cc函数主体
  3. 31 Oct 13:01:33 - in call/cc before set timer
  4. 31 Oct 13:01:33 - in call/cc after set timer
  5. // 休息1秒, 进入setTimeout的回调
  6. 31 Oct 13:01:34 - before callback
  7. // 进入continuation函数, 调用continuation函数就相当于call/cc函数返回了"hello world!", 从call/cc返回开始继续执行
  8. 31 Oct 13:01:34 - after call/cc hello world!
  9. // 执行setTimeout回调剩下部分
  10. 31 Oct 13:01:34 - after callback

复制代码

现在我们已经使用CPS转换得到了"同步"的文件处理函数, 虽然说看起来是同步的, 但是内部使用的是call/cc实现, 实际上是不会阻塞JS引擎的, 其他回调仍然能正常执行. 有了这些函数的支持, 本文开头给出的那个文件拷贝函数就能够不作任何修改正常执行, 现在一切问题都已经解决了, 唯一剩下的问题就是call/cc函数的实现, 标准的node.js引擎是没有call/cc函数的, 这里我们使用node-fibers[4]实现call/cc函数.

  1. var call_cc = function(cont) {
  2. var fiber = Fiber.current;
  3. cont(function (a) {
  4. fiber.run(a);
  5. });
  6. return Fiber["yield"]();
  7. };

复制代码

由于node-fiber的限制, 这个copy函数必须运行在fiber环境中, 完整代码如下:

  1. var fs = require("fs");
  2. require("fibers");
  3. var call_cc = function(cont) {
  4. var fiber = Fiber.current;
  5. cont(function (a) {
  6. fiber.run(a);
  7. });
  8. return Fiber["yield"]();
  9. };
  10. var efs = {
  11. open: function (name, mode) {
  12. return call_cc(function (continuation) {
  13. fs.open(name, mode, undefined, function(err, fd_src) {
  14. continuation(fd_src);
  15. });
  16. });
  17. },
  18. read: function (fd, buffer, offset, length) {
  19. return call_cc(function (continuation) {
  20. fs.read(fd, buffer, offset, length, null, function(err, bytes_read) {
  21. continuation(bytes_read);
  22. });
  23. });
  24. },
  25. write: function (fd, buffer, offset, length) {
  26. return call_cc(function (continuation) {
  27. fs.write(fd, buffer, offset, length, null, function(err, written) {
  28. continuation(written);
  29. });
  30. });
  31. }
  32. };
  33. var copy = function(src, dst) {
  34. var fd_src = efs.open(src, "r");
  35. var fd_dst = efs.open(dst, "w");
  36. while (true) {
  37. var buffer_size = 4096;
  38. var buffer = new Buffer(buffer_size);
  39. var bytes_read = efs.read(fd_src, buffer, 0, buffer_size);
  40. if (bytes_read > 0) {
  41. var write_at = 0;
  42. while (write_at < bytes_read) {
  43. write_at += efs.write(fd_dst, buffer, write_at, bytes_read - write_at);
  44. }
  45. } else {
  46. break;
  47. }
  48. }
  49. };
  50. Fiber(function () {
  51. copy("src", "dst");
  52. }).run();

复制代码

最后一点空间, 我想简单评论一下老赵的JSCEX[1], 简单来说, 就是一点: 简直就是在重复发明轮子.
有关函数式编程, 在lisp界已经有40~50年的研究, 各个方面都已经有非常完善的体系, 当然这种异步同步函数之间互相转换早已经有体系化, 可证明的研究[5], 实现这样一个异步类库, 最好是从底层做起, 首先实现fiber[4]或者shift/reset[6]这样的底层函数, 然后再其之上, 构建一个异步框架. 像老赵这样, 一上来就搞JS再编译, 那是绕了十万八千里的远路, 不仅本身会引入bug, 也会给用户的调试造成很大困扰, 性能问题也无法保证.

1. [1] JavaScript版本的AsyncEnumerator http://blog.zhaojie.me/2010/11/a ... -in-javascript.html ↩
2. [2] Continuation-passing style http://en.wikipedia.org/wiki/Continuation-passing_style ↩
3. [3] Call-with-current-continuation http://en.wikipedia.org/wiki/Call-with-current-continuation ↩
4. [4] Fiber support for v8 and Node https://github.com/laverdet/node-fibers ↩
5. [5] Olivier Danvy and Andrzej Filinski. 1990. Abstracting control. In Proceedings of the 1990 ACM conference on LISP and functional programming (LFP '90). ACM, New York, NY, USA, 151-160. DOI=10.1145/91556.91622 http://doi.acm.org/10.1145/91556.91622 ↩
6. [6] Tiark Rompf, Ingo Maier, and Martin Odersky. 2009. Implementing first-class polymorphic delimited continuations by a type-directed selective CPS-transform. In Proceedings of the 14th ACM SIGPLAN international conference on Functional programming (ICFP '09). ACM, New York, NY, USA, 317-328. DOI=10.1145/1596550.1596596 http://doi.acm.org/10.1145/1596550.1596596 ↩

http://blog.kghost.info/index.php/2011/10/callcc-and-node-js/

转载于:https://my.oschina.net/blackfish/blog/54854

风格之争:Coroutine vs Callback相关推荐

  1. Coroutine in Java - Quasar Fiber实现--转载

    转自 https://segmentfault.com/a/1190000006079389?from=groupmessage&isappinstalled=0 简介 说到协程(Corout ...

  2. 计算机科学精彩帖子收集

    linux源码 LXR 源自"the Linux Cross Referencer",中间的"X"形象地代表了"Cross".与 Sourc ...

  3. Golang适合高并发场景的原因分析

    典型的两个现实案例: 我们先看两个用Go做消息推送的案例实际处理能力. 360消息推送的数据: 16台机器,标配:24个硬件线程,64GB内存  Linux Kernel 2.6.32 x86_64  ...

  4. Golang适合高并发场景的原理

    典型的两个现实案例: 我们先看两个用Go做消息推送的案例实际处理能力 360消息推送的数据: 16台机器,标配:24个硬件线程,64GB内存  Linux Kernel 2.6.32 x86_64  ...

  5. 数字农业WMS库存操作重构及思考

    简介: 数字农业库存管理系统在2020年时,部门对产地仓生鲜水果生产加工数字化的背景下应运而生.项目一期的数农WMS中的各类库存操作均为单独编写.而伴随着后续的不断迭代,这些库存操作间慢慢积累了大量的 ...

  6. 数字农业 WMS 库存操作重构及思考

    一 问题背景 数字农业库存管理系统(以下简称数农WMS)是在2020年时,部门对产地仓生鲜水果生产加工数字化的背景下应运而生.项目一期的数农WMS中的各类库存操作(如库存增加.占用.转移等)均为单独编 ...

  7. Swift 开源带来的思考

    苹果全球开发者大会总能掀起一股旋风,虽然今年大会的"猛料"不如往期,但Swift在今年晚些时候开源的消息,却足以赚足开发者眼球. 其实苹果的行事风格,我还是蛮喜欢的:"苹 ...

  8. Python 的协程库 greenlet 和 gevent

    greenlet 官方文档:https://greenlet.readthedocs.io/en/latest/ From:https://www.jianshu.com/u/3ab212f28d91 ...

  9. Joel谈优秀软件开发-摘录

    Joel谈优秀软件开发这本书在图书馆近日稍微看另一部分,更像一个散文集,讲了软件开发中的方方面面.下面是一些记录,书本中提到的一些网址摘录在下面,大家有空的可以去看看,有些挺不错的. 1.Ken Ar ...

最新文章

  1. 机器学习Sklearn实战——其他线性模型
  2. 教你编写Node.js中间件,实现服务端缓存
  3. Apache的详细配置
  4. c++之按距离某点的距离排序
  5. 苹果 Mac 如何同时输出两个蓝牙喇叭或两副 AirPods 耳机?
  6. matlab画直方图_直方图规定化+暗通道去雾 python
  7. 建筑业建筑业大数据行业现状_建筑—第2部分
  8. 实践出真知:微服务经验之避坑指南
  9. chr() 、ord()
  10. Uncaught RangeError: Maximum call stack size exceeded
  11. java用线程做小球碰撞_多线程之碰撞小球
  12. 用20 ETH的成本撬动3000万美元收益——Harvest攻击全复盘
  13. ai人工智能相关职业_2020年及以后的5个人工智能最佳职业
  14. 地球引擎初级教程——影像的镶嵌和拼接Mosaics and Composites(含长津湖区域练习)
  15. HTML——文本域标签(textarea)
  16. win10分屏快捷键无法使用_如何使用Windows10系统分屏快捷键?
  17. Vagrant+VirtualBox快速搭建Linux环境
  18. 科技创新让互联更简单:荣耀多款新品助力智慧生活全面进阶
  19. 120本技术大咖都在力荐的畅销书籍包邮送给你 【编程 数据 AI 思维 求职 】
  20. Latex 中勾的几种画法总结

热门文章

  1. web前端工作笔记008---js延迟执行代码
  2. android学习笔记---32_文件断点上传器,解决多用户并发,以及自定义协议,注意协议中的漏洞
  3. EJB3.0学习笔记-----Stateful Session Bean的原理
  4. relu 里面的是激活函数
  5. 9W人脸清洗的问题--20170208
  6. fastadmin 后台管理 时间戳字段使用
  7. JAVA Druid 查询语句Json构造
  8. symfony app dev.php,Symfony 2:404未找到当tryes打开/app_dev.php时出错
  9. manacher java_最大回文子串(Manacher算法)
  10. linux nginx添加php版本号,linux伪装隐藏Nginx,PHP版本号提升服务器安全性