理解与实现(by Python)JS event loop

为什么需要event loop

因为: 需要用event loop来实现异步IO(回调函数的方式)。
异步IO的好处在于可以单线程执行程序却不会被IO阻塞,而单线程使得我们不用担心线程安全问题 -- 每个函数在执行过程中不会被其他逻辑(协程)中断。
任何异步IO(基于回调函数)方式,无论是JavaScript, python的asyncio, ruby的event machine, 都是在执行一个单线程的event loop,其中JS隐式地执行event loop。

区分同步与异步:

# python:
# synchronous:
line = input()      # blocked until input a line
print(line)
  • 同步IO(== 阻塞) 步骤:
    1. 用户线程 主动发起系统调用
      • 比如read(int fd, void *buf, size_t count) 读IO
    2. OS将此线程被挂起到此进程的等待队列
    3. IO读写完毕时, OS将对应被挂起的线程唤醒(放入进程的就绪队列)
      • 比如read(int fd, void buf, size_t count) 完毕, OS将数据放入buf, 唤醒线程
// js:
// asynchronous IO:
axios.get("api.github.com")
     .then(data => handle(data))   # by callback
immediatelyDoSomething()  # non-blocking
  • 异步IO(== 非阻塞 == 轮询) 步骤:
    1. 用户线程发起一次IO查询:
      1. 用户线程 发起系统调用, 检查IO读写是否就绪
        • 例如 select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout)
      2. 无论读写是否就绪, 系统调用都立即返回
        • 例如 select()中, OS将读写就绪的*fd_readable, *fd_writable替换 *readfds, *writefds
    2. 如果还有读写没就绪,用户线程需要重复步骤1查询

Event Loop 与 用户逻辑

无论是显式地还是隐式地,整个程序(线程)的执行顺序都是:

  1. 创建event loop对象
  2. 执行用户逻辑(或者把整个用户逻辑当作一个回调函数schedule到event loop对象的queue中)
    • 其中用户代码通过setTimeout(), httpRequest() 等API, 往event loop对象的queue中存入callback, 整个过程是非阻塞的
  3. 执行event loop 死循环: 每次迭代中,
    1. 每个循环遍历并执行所有到期的callback
      • 用优先队列存储timeout queue
    2. 系统调用查询并执行就绪的IO
      • 可以用select, epoll, kqueue等系统调用,本质都是一次非阻塞的IO查询

这里所有IO共享一个IO queue, 因为unix-like(linux, macOS)操作系统中,所有IO都映射成了文件IO,IO读写的系统调用都一样。

实现 event loop

我们只要处理两种不同类型回调:

  1. timeout回调
    • 用优先队列存储所有timeout回调,每个循环只遍历已经到期的callback
  2. IO读写回调
    • 每个循环调用select查询一次IO

另外, 为了避免空循环消耗cpu, 每个循环 sleep 50ms.

以下使用python实现一个简单的event loop, 支持timeout/interval, stdio, httpGet:

import queue, time, select, os, socket


class EventLoop:
    IDLE_SECONDS = 0.05

    def __init__(self):
        self.time_queue = queue.PriorityQueue()
        self.io_read_queue = {}           # fd -> callback
        self.io_write_queue = {}
        self.io_exception_queue = {}

    def run(self):
        while True:
            self.handle_time()
            self.poll_io()
            time.sleep(self.IDLE_SECONDS)        # save cpu usage

    def handle_time(self):
        now = time.time()
        while not self.time_queue.empty():
            timeout, callback = self.time_queue.get()
            if now < timeout:
                self.time_queue.put((timeout, callback))
                break
            callback()

    def poll_io(self):
        readable_fds, writable_fds, ex_fds = select.select(self.io_read_queue.keys(),
                                                           self.io_write_queue.keys(),
                                                           self.io_exception_queue.keys(),0)
        for fd in readable_fds:
            self.io_read_queue[fd](fd)      # fd as callback argument
        for fd in writable_fds:
            self.io_write_queue[fd](fd)


def set_timeout(seconds, callback):
    global_loop.time_queue.put((time.time()+seconds, callback))

def set_interval(seconds, callbacks):
    next_run = [time.time()+seconds]      # use array because python doesn't support closure..
    def run_and_reschedule():
        next_run[0] += seconds
        global_loop.time_queue.put((next_run[0], run_and_reschedule))
        callbacks()
    global_loop.time_queue.put((next_run[0], run_and_reschedule))

def getlines(callback, fd=0):                   # fd 0 == stdin
    def read(fd):
        line = os.read(fd, 10000).decode()
        callback(line)
    global_loop.io_read_queue[fd] = read

def getHttp(host, port, path, callback):
    s = socket.socket()
    s.connect((host, port))
    s.send(f"GET {path} HTTP/1.1\r\nHost:{host}\r\nUser-Agent: Mozilla/5.0\r\nConnection: close\r\n\r\n".encode())
    buffer = []
    def read_exhaust(fd):
        part = os.read(fd, 4000).decode()   # 4k buffer
        buffer.append(part)
        if not part or part[-4:] == "\r\n\r\n":
            del global_loop.io_read_queue[s.fileno()]
            s.close()
            callback("".join(buffer))
    global_loop.io_read_queue[s.fileno()] = read_exhaust


global_loop = EventLoop()

def main_test():
    set_timeout(30, lambda : exit())
    getHttp("baidu.com", 80, "/", lambda tcpdata: print(tcpdata))
    getlines(lambda line: print(f"stdin: {line}"))
    set_timeout(5, lambda : print("====after 5 seconds"))
    set_timeout(2, lambda : print("====after 2 seconds"))
    cnt = [1]
    set_interval(1, lambda : print(f"----interval {cnt[-1]}") or cnt.append(cnt[-1]+1))

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

推荐阅读更多精彩内容