簡體   English   中英

從同步代碼調用Tornado協同程序

[英]Calling Tornado coroutines from synchronous code

這將是一個很長的問題,所以:

TL; DR:我有一個帶有請求處理程序的Python 2.7線程網絡服務器,調用堆棧如下所示:

WorkerThread -> requestHandler -> func1 -> func2 -> .. -> func10 -> doStuff -> BlockingIO

我想使用Tornado 3.0 IOLoop並僅更改服務器和IO部分:

(IOLoop) -> requestHandler -> func1 -> func2 -> .. -> func10 -> (doStuff) -> (AsyncIO)

所以requestHandler()和func10()之間的所有代碼堆都不會改變。 事實上,即使doStuff()的界面也不會改變,它似乎會被阻止。 但是,在內部它將使用AsyncIO對象(作為Tornado協程),並在異步IO操作期間產生IOLoop以執行其他協同程序,直到IO操作完成。

這可能嗎?




現在來一個幾乎真實的例子:

我有一個網絡服務器接收請求並使用線程池(或進程池,處理它們,就此示例而言無關緊要):

def main():

    # Main entry point, called below.

    # Fake class, you can imagine the internals. We register a request 
    # handler here - handleRequest()
    server = ThreadedServer(handler=handleRequest) 

    # Server has a thread pool, each request is handled on a worker thread. 
    # One thread handles network stuff and pushes requests to worker threads
    Server.start()

def handleRequest(server_address):

    # This is the request handler, called in the context of a worker 
    # thread, after a network request was received.

    # We call the function below. It blocks the thread until it finishes.
    # Not very optimal, since the blocking is network IO bound
    result = doStuff(server_address)

    # We use the result somehow, here we print it
    print "Request handled with result: %s" % result

def doStuff(server_address):

    # This is called by the request handler

    # This is a network bound object, most of its time is spent waiting
    # for the network IO
    net_bound_object = NetBoundSyncObject(server_address)

    # This would block, waiting on the network, preventing the thread from 
    # handling other requests
    result = net_bound_object.do_something()

    # We have the result, return it
    return result

if __name__ == "__main__":

    main()

很簡單,真的。

現在,假設我已經決定要重構我的服務器以使用Tornado,使用tornado.gen來支持異步操作,因此不會受到網絡IO的限制。 所以,這是我的新代碼:

def main():

    # Start Tornado's IOLoop, first entering TornadoServer.start() to begin
    # initializing the server and accept requests.
    # server.start is a coroutine that waits for network IO, yielding 
    # control back to the IOLoop until something
    # happens. When something does, it is awakened and schedules a 
    # request handler - handleRequest, and goes back to network IO, 
    # yielding control. Thus, handleRequest is called.
    server = TornadoServer(handler=handleRequest) # fake class again
    IOLoop.instance().add_callback(server.start)
    IOLoop.instance().start()

def handleRequest(server_address):

    # This part of the code has not been changed - just the comments.
    # It is now run in the context of an IOLoop callback.

    # We call the function above. The interface remains the same. It also seems
    # to block - which is fine, we want to wait for its result to continue processing.
    # However, we want the IOLoop to continue running somehow.
    result = doStuff(server_address)

    # We use the result somehow, here we print it
    print "Request handled with result: %s" % result            

def doStuff(server_address):

    # This is a network bound object, most of its time is spent waiting for
    # the network IO, however all its methods are coroutines and it yields 
    # while waiting for network IO
    net_bound_object = NetBoundAsyncObject(server_address)

    # Now to the problem.
    # doStuff() is a facade - I don't want it to be a coroutine, I want it to hide
    # the implementation details and keep its previous interface.

    # However, NetBoundAsyncObject.do_something_async() is a coroutine, and calls
    # coroutines inside it. So it should be called in the context of
    # another coroutine:
    result = yield net_bound_object.do_something_async()
    # but this is wrong here, since we are not a coroutine.

    # To properly call it asynchronously, I would need to make doStuff()
    # a coroutine as well, breaking its interface, which would mean that 
    # handleRequest too should now be a coroutine. Not a big change, but imagine
    # that instead of calling doStuff() directly, I had code like:
    # handleRequest -> func1 -> func2 -> func3 -> ... -> func10 -> doStuff
    # so now I'd have to change all these functions to be coroutines as well.

    # All of these functions, handleRequest and func1..10, represent a big stack 
    # of code in my real system which is completely synchronous, CPU bound code, 
    # so it has no IO waits anywhere, just code that needs to be run BEFORE and
    # AFTER the network IO bound code finishes, to properly handle the request. 
    # It is well tested, production proven code that requires no functional change,
    # and that doesn't need to be a coroutine. This would be a big refactor.       

    # In the code as it is now, result is now returned as a Future:
    result = net_bound_object.do_something_async()
    # I want to be able to do something like:
    IOLoop.instance().wait_for_future(result)
    # Letting the IOLoop run and handle other things in the meanwhile, like
    # network requests, and also my asynchronous code. 
    # When it finishes, I want my wait_for_future() to return and to continue
    # execution with the result accessible in the future object.

    # Thus, the changes would be at the top (the TornadoServer vs ThreadedServer)
    # and the bottom (doStuff to use either NetBoundObject or NetBoundAsyncObject),
    # but the middle stack will remain unchanged.

    # Return the result of the operation
    return result

if __name__ == "__main__":

    main()

我知道這在許多方面都存在問題,主要是因為調用堆棧。 當我們做類似的事情:

IOLoop.instance().wait_for_future(result)

我們有一個看起來像這樣的調用堆棧:

IOLoop.main_loop.start() -> handleRequest -> IOLoop.main_loop.wait_for_future() -> other_callbacks..

所以我們可能(甚至可能)遇到這樣的情況:

IOLoop.main_loop.start() -> handleRequest -> IOLoop.main_loop.wait_for_future() -> handleRequest -> IOLoop.main_loop.wait_for_future() -> handleRequest -> IOLoop.main_loop.wait_for_future() -> ...

很明顯,如果handleRequest本身成為一個協程,那么當它本身產生時,我們就沒有這么深的堆棧問題。

在我曾經使用的嵌入式系統中,使用非搶先式調度程序,在任何時候都沒有問題將控制權返回給調度程序而沒有堆棧問題。 調度程序將獲取執行上下文並調用堆棧並存儲它們,並更改為另一個上下文/堆棧並從那里繼續執行。 在等待事件/ IO時,將觸發調度程序並運行IO循環中的任何內容。 我想在我的系統中使用這樣的東西,而不是必須更改上面的整個調用堆棧 - 將所有內容轉換為協同程序。

有沒有任何提示,任何想法?

您可以使用以下命令同步運行@ gen.coroutine修飾函數:

@gen.coroutine
def main():
    # do stuff...

if __name__ == '__main__':
    IOLoop.instance().run_sync(main)

這將啟動'IOLoop',運行該函數,並停止循環。 https://github.com/facebook/tornado/blob/master/tornado/ioloop.py

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM