python web并发编程实现

最近重读《深入理解计算机系统》,在书本第十二章-并发编程中,作者简明地讲解了并发程序的三种基本构造方法并进行了优缺点分析(具体内容下文会略微提到,有兴趣的可以去阅读原书),再加上前阵子看的tornado异步框架源码,向趁此来聊聊并发编程,如有纰漏,欢迎指正。

1 先谈谈几个令人混乱的名词

1.1 阻塞&非阻塞 。

《操作系统设计与实现》一书中提到进程有三种基本状态:就绪,阻塞和运行,因此阻塞和非阻塞就指的是进程或线程(后文会用进程统称)的状态。现代操作系统的内核调度器(调度器本身也是进程,其属于系统进程)统一管理着用户进程;

  • 运行是指进程正在CPU上执行;
  • 就绪是指其在等待被执行且最终会被内核调度;
  • 阻塞是指进程被挂起,暂时不会被调度,阻塞的原因可能是进程执行了IO操作,如向磁盘读写数据,从网络读写数据等,当IO设备执行完指令后,会发出中断信号,调度器会通过该信号重新激活相关进程,使进程进入就绪或运行态。

1.2 同步&异步。

先说结论:异步与同步,是指任务调度时,两个任务之间的协同机制(任务可以是进程,线程或简单的函数调用);若任务间无关系,两者不依赖彼此,就为异步;一个依赖于另一个的执行结果,就为同步
举个具体的例子来解释下。上文提到了系统的调度,调度器管理着众多进程,CPU轮流地执行着进程。为了更好地理解,以单核CPU为例(多核原理相同):假设调度器管理着进程A和进程B;若A,B之间是同步的,意味着其中一个需要等待另一个的执行结果才能执行,例如CPU会执行进程A直至它完成并返回结果,之后才会进入内核模式,由调度器去执行进程B;若A,B是异步的,意味着A,B相互独立,毫无关系,CPU不用立刻执行完进程A,中途也可以转而去执行进程B。从定义来看,现代操作系统的调度器,与各个进程间都是异步的,这也是操作系统并发的前提。同时,不相关的任务间可以是异步的。因为异步的无序(无法预测的逻辑流的走向),才导致了竞争(多个进程同时访问公共资源)的发生,进程间才会在部分时间需要同步机制。

1.3 并发&并行。

并发和并行的概念比较容易理解,在此不再赘述。上文提到的并发,是系统内核运行多个程序的机制,但如果你是个程序员,就应该知道其在应用程序中的普遍,也或多或少会了解三种基本并发技术:进程(process)线程(thread)IO多路复用(multiplexing)。实际上,正确编写并发程序是非常困难的,因为一个并发程序往往具有多个逻辑流,并且还具有不可预测性。
基于进程的并发编程。相对于IO多路复用而言,基于进程和线程的并发程序更容易编写(也仅是相对而言,考虑进程间通信和同步问题常会让人头大 @v@),并且能充分发挥现代处理器的多核优势,对于CPU密集型程序来说,性能优势会很明显。但进程间独立的地址空间使得进程间需要显式的通信(IPC),而且上下文切换的开销也很大,这也可能是个优点,这样一来可以避免进程间相互覆盖彼此的地址空间。
基于线程的并发编程 对比进程优势明显,属于同个进程的线程运行在一个进程的上下文中,它们共享着部分上下文数据,这使得线程间通信更加容易,当然坑也不少,由此引入的同步问题也值得程序员们小心对待。接下来到了本文的重点——基于IO多路复用的并发编程,希望读者耐心看下去。

2 基于IO多路复用的并发编程(也叫事件驱动编程)

标题中已经提到,web方向的并发编程是本文讨论重点,限于笔者知识的局限性,接下来会以python语言举例,讨论web开发中的并发编程。

2.1 技术原理及优劣

Ryan Dahl在介绍Node.js时主张不能像对待传统函数那样对待阻塞型函数,因为IO操作会拖累整个程序,下面这张表了更易于我们直观理解。为解决IO事件带来的阻塞困境,IO多路复用出现了。


图1.1.png

IO多路复用的核心是调用select(在不同os中,可能是epoll, poll 或kqueue)函数,内核会挂起进程,在发生一个或多个IO事件后,再去进行相关调用。原理似乎很简单,但实际编程时,代码量会是基于进程的三倍(引用自csapp书中的数据),编程难度还会随着并发粒度的减少而上升,而且不能充分利用多核处理器。但该方法带来的优势却值得我们费更多精力去编写它。
基于IO多路复用的程序运行在单独的进程上下文中,因此逻辑流之间不存在上下文切换,也没有多线程带来的同步问题,系统资源占用极低,且在IO密集型程序中运行效率非常高,可以对比传统web框架(如Django)和异步框架(如Twisted,tornado)之间的并发量差距。而且web服务器需要处理大量的连接,若采用一个线程去处理一个连接,需消耗大量系统资源来维护线程池,线程之间的上下文切换也会产生大量开销。

2.2 编程实例

讲了一大堆理论,接下来讲讲具体的IO多路复用编程(下文简称为事件驱动编程)实现。
主要有两种主流的实现方式:回调协程
基于回调的异步编程,最典型的如javascript,有个显著的缺点“回调地狱”:各层调用之间的相互依赖,就必须嵌套回调;回调地狱会使得代码难以阅读,更难以编写。
更现代的方式是采用协程。协程又称为用户级线程,即任务间的切换完全由编程人员控制,这极大提升了编程难度,因此常见的异步框架都会将整个异步模型简化为单线程,并合理地将任务拆分,同时还要避免阻塞调用。
python3.3中正式引入了yield from句法,并实现了底层的事件调度器,这使得协程的使用更加简单。在python的标准库asyncio库中引入了事件循环,可以使用async关键字或@asyncio.coroutine来将函数标记为协程,并在该协程的IO阻塞操作前加上await或yield from关键字来实现异步IO调用,最后将协程加入事件循环即可,但前提是该调用是异步的。下面是简单的asyncio库使用示例:

import asyncio
import aiohttp
import cProfile

async def get_page(index):
    url = 'http://www.baidu.com'
    async with aiohttp.ClientSession() as session:
        print('get:',index)
        response = await session.get(url)
        if response.status == 200:
            html = await response.text()
            print('{} recv: {}'.format(index, html))

def test():        
    tasks = [asyncio.ensure_future(get_page(i)) for i in range(100)]
    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait(tasks))
    loop.close()

def run_client():
    cProfile.run('test()', 'profile_server.txt', sort='time')
    p = pstats.Stats('profile.txt')
    p.sort_stats('time').print_stats()

if __name__ == "__main__":
    run_client()

如果你觉得只是会调用api还不够,你需要更进一步地了解其底层事件调度器如何实现,你可以继续读下去。
我写了一个基于IO多路复用的简单HttpServer和HttpClient项目,采用了协程的实现方法。源码文件较多,文中展示不便,附上github repo地址。代码经过测试,可以直接运行,基于python3.4+,windows平台,参考了python3-cookbook一书(强烈推荐python程序员阅读此书)中的部分代码和tornado源码。如果大家觉得需要讲下源码的话,可以考虑另写。如果觉得本文或项目对你有所帮助,请给个❤或。

参考文献
[1] David Beazley, Brian K. Jones. Python Cookbook 3rd Edition.
[2] Luciano Ramalho. 流畅的Python. 人民邮电出版社,2017.
[3] Bryant, R.E, O'Hallaron, D.R. 深入理解计算机系统. 北京: 机械工业出版社, 2010.11

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容