簡體   English   中英

jupyter notebooks-safe asyncio run wrapper 方法庫

[英]jupyter notebooks-safe asyncio run wrapper method for a library

我正在構建一個在內部利用 asyncio 的庫。 雖然用戶不應該意識到這一點,但內部實現目前使用asyncio.run()陶瓷包裝器包裝異步代碼。

但是,一些用戶將從 jupyter notebook 執行此庫代碼,我正在努力用對任一環境都安全的包裝器替換asyncio.run()

這是我嘗試過的:

ASYNC_IO_NO_RUNNING_LOOP_MSG = 'no running event loop'


def jupyter_safe_run_coroutine(async_coroutine, _test_mode: bool = False)
    try:
        loop = asyncio.get_running_loop()
        task = loop.create_task(async_coroutine)
        result = loop.run_until_complete(task) # <- fails as loop is already running
        # OR
        asyncio.wait_for(task, timeout=None, loop=loop) # <- fails as this is an async method
        result = task.result()
    except RuntimeError as e:
        if _test_mode:
            raise e
        if ASYNC_IO_NO_RUNNING_LOOP_MSG in str(e):
            return asyncio.run(async_coroutine)
    except Exception as e:
        raise e

要求

  1. 我們用的是python 3.8,所以不能用asyncio.Runner context manager
  2. 我們不能使用線程,所以這里建議的解決方案行不通

問題:

我如何等待/等待async_coroutineloop.create_task(async_coroutine)提供的任務/未來完成?

由於評論中所述的原因,上述方法都沒有實際進行等待。


更新

我發現這個nest_asyncio庫是為解決這個問題而構建的:


ASYNC_IO_NO_RUNNING_LOOP_MSG = 'no running event loop'

HAS_BEEN_RUN = False


def jupyter_safe_run_coroutine(async_coroutine, _test_mode: bool = False):
    global HAS_BEEN_RUN
    if not HAS_BEEN_RUN:
        _apply_nested_asyncio_patch()
        HAS_BEEN_RUN = True
    return asyncio.run(async_coroutine)


def _apply_nested_asyncio_patch():
    try:
        loop = asyncio.get_running_loop()
        logger.info(f'as get_running_loop() returned {loop}, this environment has it`s own event loop.\n'
                    f'Patching with nest_asyncio')
        import nest_asyncio
        nest_asyncio.apply()
    except RuntimeError as e:
        if ASYNC_IO_NO_RUNNING_LOOP_MSG in str(e):
            logger.info(f'as get_running_loop() raised {e}, this environment does not have it`s own event loop.\n'
                        f'No patching necessary')
        else:
            raise e

盡管如此,我仍然面臨一些問題:

  1. 根據這個 SO answer ,可能存在飢餓問題
  2. 在 async_coroutine 中寫入的任何日志都不會打印在 jupyter notebook 中
  3. jupyter notebook kernel 偶爾會在任務完成后崩潰

編輯

對於上下文,庫在內部調用外部 API 以豐富用戶提供的 dataframe 的數據:

# user code using the library
import my_lib

df = pd.DataFrame(data='some data')
enriched_df = my_lib.enrich(df)

公開異步 function 通常是個好主意。這樣你會給你的用戶更多的靈活性。

如果您的某些用戶不能(或不想)對您的函數使用異步調用,他們將能夠使用asyncio.run(your_function())調用異步 function。 或者在極少數情況下,他們正在運行事件循環但無法進行異步調用,他們可以使用此處描述create_task + add_one_callback方法。 (我真的不知道為什么會發生這樣的用例,但為了論證我把它包括在內。)

對用戶隱藏異步接口並不是最好的主意,因為它限制了他們的能力。 他們可能會分叉你的 package 來修補它並使暴露的 function 異步或直接調用隱藏的異步 function。 這些對您來說都不是好消息(更難記錄/跟蹤錯誤)。 我真的建議堅持使用最簡單的解決方案,並提供async函數作為主要入口點。

假設以下 package 代碼后跟它的 3 種不同用法:

async def package_code():
    return "package"

客戶代碼

典型的客戶可能只會這樣使用它:

async def client_code_a():
    print(await package_code())

# asyncio.run(client_code_a())

對於某些人來說,以下內容可能是有道理的。 例如,如果您的 package 是他們將使用的唯一異步對象。 或者也許他們還不習慣使用異步代碼(這些你可能會說服嘗試client_code_a而不是):

def client_code_b():
    print(asyncio.run(package_code()))

# client_code_b()

極少數(我很想說沒有):

async def client_code_c():
    # asyncio.run() cannot be called from a running event loop:
    # print(asyncio.run(package_code()))
    loop = asyncio.get_running_loop()
    task = loop.create_task(package_code())
    task.add_done_callback(lambda t: print(t.result()))

# asyncio.run(client_code_c())

我仍然不確定您的目標是什么,但我將用代碼描述我在評論中試圖解釋的內容,以便您可以告訴我您的問題出在哪里。

如果你 package 要求用戶調用一些函數(示例中的your_package_function ),將協程作為 arguments,那么你不應該擔心事件循環。

這意味着 package 不應調用asyncio.runloop.run_until_complete 客戶端應該(在幾乎所有情況下)負責啟動偶數循環。

您的 package 代碼應該假設有一個事件循環正在運行。 因為我不知道你的包的目標我只是做了一個 function 為客戶端傳遞的任何協程提供一個"test"參數:

import asyncio

async def your_package_function(coroutine):
    print("- Package internals start")
    task = asyncio.create_task(coroutine("test"))
    await asyncio.sleep(.5) # Simulates slow tasks within your package
    print("- Package internals completed other task")
    x = await task
    print("- Package internals end")
    return x

客戶端(包用戶)然后應調用以下內容:

async def main():
    x = await your_package_function(return_with_delay)
    print(f"Computed value = {x}")

async def return_with_delay(value):
    print("+ User function start")
    await asyncio.sleep(.2)
    print("+ User function end")
    return value

await main()
# or asyncio.run(main()) if needed

這將打印:

- Package internals start
- Package internals completed other task
+ User function start
+ User function end
- Package internals end
Computed value = test

暫無
暫無

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

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