如何实现看起来像模仿async / await的异步代码?

时间:2019-05-26 21:01:00

标签: asynchronous networking scheme chez-scheme

否则,我想依靠epoll(或类似名称)来编写异步网络代码,该代码看起来像不依赖回调的常规代码。

该代码必须看起来像同步代码,但是与同步代码不同,它不是阻塞等待网络io,而是必须挂起当前的协程,并在文件描述符准备好后重新启动。

2 个答案:

答案 0 :(得分:1)

我最初的想法是依靠生成器和yield。但是this was a mistake在某种程度上被python过去滥用yield from的事实所误导。

无论如何,脆性纤维和I adapted it to chez scheme是一种很大的暗示。

这是示例服务器代码:

(define (handler request port)
  (values 200 #f (http-get "https://httpbin.davecheney.com/ip")))

(untangle (lambda ()
            (run-server "127.0.0.1" 8888)))

handler根据httpbin服务返回其IP。该代码在call / cc实际上是call / 1cc的帮助下看起来是同步的。

untangle将使用作为参数传递的lambda启动事件循环!

这是run-server的定义:

(define (run-server ip port handler)
  (log 'info "HTTP server running at ~a:~a" ip port)
  (let* ((sock (socket 'inet 'stream 'ipv4)))
    (socket:setsockopt sock 1 2 1) ;; re-use address
    (socket:bind sock (make-address ip port))
    (socket:listen sock 1024)
    (let loop ()
      (let ((client (accept sock)))
        (let ((port (fd->port client)))
          (spawn (lambda () (run-once handler port)))
          (loop))))))

如您所见,没有回调。与简单的同步Web服务器唯一不同的是spawn过程,它将以自己的协程来处理请求。特别是accept是异步的。

run-once会将方案请求传递给handler并采用其3个值来构建响应。不太有趣。上面http-get看起来是同步的,但实际上是异步的。

鉴于http-get要求引入自定义二进制端口,我只会解释一下accept是如何工作的,但是可以说这是相同的行为...

(define (accept fd)
  (let ((out (socket:%accept fd 0 0)))
    (if (= out -1)
        (let ((code (socket:errno)))
          (if (= code EWOULDBLOCK)
              (begin
                (abort-to-prompt fd 'read)
                (accept fd))
              (error 'accept (socket:strerror code))))
        out)))

如您所见,它调用了一个过程abort-to-prompt,我们可以简单地调用pause来“停止”协程并调用提示处理程序。

abort-to-promptcall-with-prompt合作。

由于chez方案没有提示,因此我使用两个连续拍摄call/1cc

来模拟它
(define %prompt #f)
(define %abort (list 'abort))

(define (call-with-prompt thunk handler)
  (call-with-values (lambda ()
                      (call/1cc
                       (lambda (k)
                         (set! %prompt k)
                         (thunk))))
    (lambda out
      (cond
       ((and (pair? out) (eq? (car out) %abort))
        (apply handler (cdr out)))
       (else (apply values out))))))

(define (abort-to-prompt . args)
  (call/1cc
   (lambda (k)
     (let ((prompt %prompt))
       (set! %prompt #f)
       (apply prompt (cons %abort (cons k args)))))))

call-with-prompt将启动一个名为set!的全局%prompt的延续,这意味着THUNK会有一个提示。如果连续参数OUTcall-with-values的第二个lambda)以唯一对象%abort开始,则意味着通过abort-to-prompt达到了连续。它将调用带有HANDLER延续性的abort-to-prompt以及传递给call-with-prompt延续参数(apply handler (cons k (cdr out)))的任何参数。

abort-to-promp将在代码执行存储在%prompt中的提示的继续符之后,启动一个新的继续符,以便能够返回。

call-with-prompt是事件循环的核心。分为两部分:

(define (exec epoll thunk waiting)
  (call-with-prompt
   thunk
   (lambda (k fd mode) ;; k is abort-to-prompt continuation that
                       ;; will allow to restart the coroutine

     ;; add fd to the correct epoll set
     (case mode
       ((write) (epoll-wait-write epoll fd))
       ((read) (epoll-wait-read epoll fd))
       (else (error 'untangle "mode not supported" mode)))
     (scheme:hash-table-set! waiting fd (make-event k mode)))))

(define (event-loop-run-once epoll waiting)
  ;; execute every callback waiting in queue, 
  ;; call the above exec procedure 
  (let loop ()
    (unless (null? %queue)
      ;; XXX: This is done like that because, exec might spawn
      ;; new coroutine, so we need to cut %queue right now. 
      (let ((head (car %queue))
            (tail (cdr %queue)))
        (set! %queue tail)
        (exec epoll head waiting)
        (loop))))

    ;; wait for ONE event
    (let ((fd (epoll-wait-one epoll (inf))
      (let ((event (scheme:hash-table-ref waiting fd)))
        ;; the event is / will be processed, no need to keep around
        (scheme:hash-table-delete! waiting fd)
        (case (event-mode event)
          ((write) (epoll-ctl epoll 2 fd (make-epoll-event-out fd)))
          ((read) (epoll-ctl epoll 2 fd (make-epoll-event-in fd))))
        ;; here it will schedule the event continuation that is the
        ;; abort-to-prompt continuation that will be executed by the
        ;; next call the above event loop event-loop-run-once
        (spawn (event-continuation event))))))

我认为就这些。

答案 1 :(得分:1)

如果您使用的是chez模式,则有chez-a-sync。它使用POSIX poll而不是epoll(epoll是Linux特定的)。 guile-a-sync2也可用于guile-2.2 / 3.0。