billiard Documentation

repository·main·Indexed 19 days ago

https://github.com/celery/billiard

A specialized fork of Python's multiprocessing module providing bug fixes and improvements for Celery and other high-performance multiprocessing applications. It includes tools for spawning processes, inter-process communication via Queues and Pipes, synchronization primitives like Locks and Semaphores, shared memory using Value and Array, and a Manager server process for flexible object sharing.

Tokens
12.2K
Snippets
36
Records
48
Agent score
16%

What's inside billiard

  1. Overview of billiard

    main

    billiard is a standalone fork of the Python multiprocessing package. It is designed to provide fixes, improvements, and patches drawn from python-trunk, as well as specific enhancements used by the Celery project.

    Key features and origins include:

    • A fork of the Python 2.7 multiprocessing package.
    • Includes the no-execv patch.
    • Includes Pool improvements originally located in Celery.
    • It is a dependency for Celery and is maintained by the Celery team.
  2. Avoid deadlocks when joining processes that use Queues

    main

    When using multiprocessing.Queue, a process that has put items into the queue will wait to terminate until all buffered items are fed by the internal "feeder" thread to the underlying pipe. To avoid deadlocks, ensure that all items put on the queue are eventually removed by a consumer before the process is joined.

    Alternatively, a child process can call Queue.cancel_join_thread() to avoid this waiting behavior.

    from multiprocessing import Process, Queue
    
    def f(q):
        q.put('X' * 1000000)
    
    if __name__ == '__main__':
        queue = Queue()
        p = Process(target=f, args=(queue,))
        p.start()
        # WRONG: p.join() will deadlock here because the queue is full
        # p.join() 
        
        # CORRECT: Get the item before joining, or don't join
        obj = queue.get()
        p.join()
  3. Configure authentication keys for connections

    main

    To prevent security risks associated with unpickling data from untrusted sources, Listener and Client use HMAC digest authentication.

    An authkey is a bytes object used as a password. If no authkey is explicitly provided, the connection uses current_process().authkey, which is automatically inherited by any child multiprocessing.Process objects created by the current process. This allows processes in a multi-process program to share a single authentication key by default. You can also generate suitable keys using os.urandom.

  4. Use Namespace objects for shared state

    main

    A Namespace object has no public methods but has writable attributes. Its representation shows the values of its attributes.

    Warning: When using a proxy for a namespace object, any attribute starting with an underscore (_) is treated as an attribute of the proxy itself, not the underlying referent.

    manager = multiprocessing.Manager()
    Global = manager.Namespace()
    Global.x = 10
    Global.y = 'hello'
    Global._z = 12.3    # this is an attribute of the proxy
    print(Global)        # Namespace(x=10, y='hello')
  5. Understand Proxy objects and referents

    main

    A proxy is an object that refers to a referent (the actual shared object) living in a different process.

    Key behaviors:

    • Method Invocation: Calling a method on a proxy invokes the corresponding method on the referent.
    • String Representation: str(proxy) returns the representation of the referent, while repr(proxy) returns the representation of the proxy itself.
    • Pickling: Proxies are picklable and can be passed between processes. If a proxy is sent to the manager process that owns its referent, unpickling it produces the referent itself.
    • Comparisons: Proxy types do not support comparison by value. manager.list([1,2,3]) == [1,2,3] will return False. Use a copy of the referent for comparisons.
    from multiprocessing import Manager
    manager = Manager()
    l = manager.list([i*i for i in range(10)])
    print(l)          # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
    print(repr(l))    # <ListProxy object, typeid 'list' at ...>
    l[4]              # 16
  6. Share data between processes using Managers

    main

    Managers provide a way to create data that can be shared between different processes. A manager object controls a server process which manages shared objects. Other processes can access these objects via proxies.

    Using BaseManager

    To create a custom manager, subclass BaseManager. You must call .start() to spawn the manager process or .get_server().serve_forever() to run it as a server.

    • address: The address the manager process listens on. If None, an arbitrary one is chosen.
    • authkey: A string used to authenticate incoming connections.
    • register(typeid, ...): A classmethod used to register a type or callable with the manager so it can be shared.

    Connecting to a Manager

    • Local: Use manager.start() to create a local subprocess.
    • Remote: Use BaseManager(address=..., authkey=...).connect() to connect to an existing manager process running elsewhere.
    from multiprocessing.managers import BaseManager
    
    # Setting up a manager server
    manager = BaseManager(address=('', 50000), authkey='abc')
    server = manager.get_server()
    server.serve_forever()
    
    # Connecting to a manager
    m = BaseManager(address=('127.0.0.1', 5000), authkey='abc')
    m.connect()
  7. Pass resources explicitly to child processes

    main

    While Unix processes can sometimes access shared resources via global variables, it is best practice to pass objects (like Lock, Semaphore, etc.) as arguments to the child process constructor. This ensures compatibility with Windows and prevents the object from being garbage collected in the parent process while the child is still using it.

    from multiprocessing import Process, Lock
    
    def f(l):
        # ... do something using "l" ...
        pass
    
    if __name__ == '__main__':
        lock = Lock()
        for i in range(10):
            # Pass the lock explicitly via args
            Process(target=f, args=(lock,)).start()
  8. Programming guidelines for multiprocessing

    main

    When using multiprocessing, adhere to these best practices to ensure stability across all platforms:

    • Avoid shared state: Minimize shifting large amounts of data between processes. Use Queue or Pipe for communication instead of low-level synchronization primitives where possible.
    • Picklability: Ensure all arguments passed to proxy methods or Process constructors are picklable.
    • Thread safety of proxies: Do not use a proxy object from multiple threads without a lock (though multiple processes can safely use the same proxy).
    • Avoid Process.terminate: Terminating a process abruptly can leave shared resources (locks, semaphores, pipes, queues) in a broken or unavailable state. Only use terminate() on processes that do not use shared resources.
    • Zombie processes: On Unix, explicitly join() processes to prevent them from becoming zombies. Calling Process.is_alive() will also join a finished process.
  9. Create customized managers with BaseManager.register

    main

    To create a custom manager, subclass BaseManager and use the register classmethod to associate a name with a class or callable. This allows you to expose custom objects through the manager proxy.

    from multiprocessing.managers import BaseManager
    
    class MathsClass:
        def add(self, x, y):
            return x + y
        def mul(self, x, y):
            return x * y
    
    class MyManager(BaseManager):
        pass
    
    # Register the class with a name
    MyManager.register('Maths', MathsClass)
    
    if __name__ == '__main__':
        manager = MyManager()
        manager.start()
        maths = manager.Maths()
        print(maths.add(4, 3))         # prints 7
        print(maths.mul(7, 8))         # prints 56
  10. Run a remote manager server and client

    main

    You can run a manager server on one machine and connect to it from others. The server requires an address (IP/hostname and port) and an authkey for security.

    # --- SERVER SIDE ---
    from multiprocessing.managers import BaseManager
    import queue
    
    q = queue.Queue()
    class QueueManager(BaseManager): pass
    QueueManager.register('get_queue', callable=lambda: q)
    
    # Listen on all interfaces at port 50000
    m = QueueManager(address=('', 50000), authkey='abracadabra')
    s = m.get_server()
    s.serve_forever()
    
    # --- CLIENT SIDE ---
    from multiprocessing.managers import BaseManager
    
    class QueueManager(BaseManager): pass
    QueueManager.register('get_queue')
    
    # Connect to the server
    m = QueueManager(address=('foo.bar.org', 50000), authkey='abracadabra')
    m.connect()
    queue = m.get_queue()
    queue.put('hello')
  11. Configure freeze_support for Windows executables

    main

    If you are creating a frozen Windows executable (using tools like pyinstaller, py2exe, or cx_Freeze) that uses multiprocessing, you must call freeze_support() immediately after the if __name__ == '__main__': block. Failure to do so will result in a RuntimeError when the executable runs.

    On a normal Python interpreter, freeze_support() has no effect.

    from multiprocessing import Process, freeze_support
    
    def f():
        print('hello world!')
    
    if __name__ == '__main__':
        freeze_support()
        Process(target=f).start()