aiomultiprocess

repository·main·Indexed 23 days ago

https://github.com/omnilib/aiomultiprocess

A Python library that combines AsyncIO and multiprocessing to achieve high levels of concurrency. It runs a full AsyncIO event loop in each child process, allowing multiple coroutines to run concurrently across multiple CPU cores and bypassing the GIL limitations of single-process AsyncIO applications. Key features include the Pool class for distributing tasks via map and starmap, the Process and Worker classes for managing child process lifecycles, and customizable task distribution via the Scheduler and RoundRobin classes.

Tokens
4.5K
Snippets
9
Records
38
Agent score
83%

What's inside aiomultiprocess

  1. How aiomultiprocess works: AsyncIO and Multiprocessing together

    main

    The core concept of aiomultiprocess is to combine the benefits of asyncio and multiprocessing.

    • AsyncIO alone is limited by the Global Interpreter Lock (GIL).
    • Multiprocessing alone typically works on one task at a time per process.

    aiomultiprocess runs a full AsyncIO event loop on each child process. This allows each child process to execute multiple coroutines at once, enabling high levels of concurrency that scale with the number of available CPU cores.

  2. Handle multiple results with PoolResult

    main

    The map and starmap methods return a PoolResult object. This object is both an Awaitable and an AsyncIterable, providing two ways to consume results:

    1. Await it: Returns the entire sequence of results once all tasks are complete.
    2. Async iterate: Yields results one-by-one as they become available (though note that results_generator currently iterates through the task_ids in order).

    If any task raises an exception, awaiting the PoolResult or iterating through it will raise a ProxyException containing the traceback.

  3. Tune Process Pool performance

    main

    You can optimize Pool performance using the following configuration options:

    OptionDescription
    processesNumber of worker processes. Defaults to None (all CPU cores).
    maxtasksperchildNumber of tasks a worker performs before being replaced. Use a positive integer to mitigate memory leaks. Defaults to 0 (infinite).
    queuecountNumber of queues used. Increasing this can reduce contention in high-throughput scenarios.
    childconcurrencyMax number of concurrent jobs a single worker picks up from its queue.
    schedulerControls job distribution. Default is aiomultiprocess.RoundRobin.

    Performance Formulas:

    • total_concurrency = processes * childconcurrency
    • throughput = total_concurrency / job_time
    • contention = throughput / queuecount
  4. Run asynchronous jobs on a pool of worker processes

    main

    You can use aiomultiprocess.Pool to run asynchronous jobs across multiple worker processes. Each worker process runs its own full AsyncIO event loop, allowing multiple coroutines to execute concurrently within each process. This bypasses the GIL limitations of a single AsyncIO loop by distributing the workload across multiple CPU cores.

    To use it, create a Pool as an asynchronous context manager and use pool.map() to distribute a coroutine function over a collection of items. Since pool.map() returns an asynchronous generator, you should iterate over it using async for.

    import asyncio
    from aiohttp import request
    from aiomultiprocess import Pool
    
    async def get(url):
        async with request("GET", url) as response:
            return await response.text("utf-8")
    
    async def main():
        urls = ["https://noswap.com", ...]
        async with Pool() as pool:
            async for result in pool.map(get, urls):
                ...  # process result
                
    if __name__ == '__main__':
        # Python 3.7+
        asyncio.run(main())
  5. Queue multiple jobs with Pool.map() and Pool.starmap()

    main

    The map() and starmap() methods queue one job for each element in the provided iterable. You can await these methods to get all results as a list in the original input order, or iterate over them with async for to process results as they complete (while still maintaining order).

    import math
    from aiomultiprocess import Pool
    
    async with Pool() as pool:
        data = [1, 4, 9, 16, 25]
        
        # Get all results as a list (awaited)
        results = await pool.map(math.sqrt, data)
        # [1, 2, 3, 4, 5]
    
        # Process results as they complete (iterated)
        async for value in pool.map(math.sqrt, data):
            ...
  6. Queue individual jobs with Pool.apply()

    main

    Use pool.apply() to queue individual jobs into the process pool. This is useful when you want to use asyncio.gather to manage multiple specific tasks being distributed across the pool.

    from asyncio import gather
    from aiomultiprocess import Pool
    
    async def get(url):
        async with request("GET", url) as response:
            return await response.text("utf-8")
    
    async with Pool() as pool:
        a, b, c = gather(
            pool.apply(get, "https://github.com"),
            pool.apply(get, "https://noswap.com"),
            pool.apply(get, "https://omnilib.dev"),
        )
  7. Run a single coroutine in a dedicated process with Worker

    main

    If you need a dedicated process for a specific asynchronous job, use the aiomultiprocess.Worker class. It runs the target coroutine in a fresh child process and returns the result to the main process.

    You can either await the Worker directly or manually call .start() and then await .join() to retrieve the result.

    from aiohttp import request
    from aiomultiprocess import Worker
    
    async def get(url, method="GET"):
        async with request(method, url) as response:
            return await response.text("utf-8")
    
    async def main():
        # Option 1: Direct await
        result = await Worker(
            target=get,
            args=("https://noswap.com",),
            kwargs={"method": "GET"}
        )
    
        # Option 2: Manual start/join
        worker = Worker(
            target=get,
            args=("https://noswap.com",),
            kwargs={"method": "GET"}
        )
        worker.start()
        result = await worker.join()
  8. Run coroutines on multiple processes using Pool.map()

    main

    The aiomultiprocess.Pool class is the primary way to run multiple coroutines concurrently across a pool of worker processes. You can use pool.map() to iterate over results as they complete, which maintains the original input order.

    Note: Pool is best used as an asynchronous context manager to ensure proper cleanup.

    from aiohttp import request
    from aiomultiprocess import Pool
    
    async def get(url):
        async with request("GET", url) as response:
            return await response.text("utf-8")
    
    async def main():
        urls = ["https://noswap.com", ...]
        async with Pool() as pool:
            async for result in pool.map(get, urls):
                ...  # process result
  9. Initialize child processes with custom code

    main

    Use the initializer and initargs parameters in Pool to run arbitrary code in each child process after its async event loop has been created. This is useful for setting up logging, database connections, or other per-process resources.

    import logging
    from aiomultiprocess import Pool
    
    def setup_logging(level=logging.WARNING):
        logging.basicConfig(level=level)
    
    async with Pool(
        initializer=setup_logging, initargs=(logging.DEBUG,)
    ) as pool:
        ...
  10. Handle exceptions in worker processes

    main

    Exceptions raised in workers are wrapped in aiomultiprocess.types.ProxyException when they reach the main process. To handle exceptions within the worker process itself (e.g., for external monitoring), use the exception_handler hook.

    import sentry_sdk
    from aiomultiprocess import Pool
    
    async with Pool(
        exception_handler=sentry_sdk.capture_exception
    ) as pool:
        ...