Skip to content

Suggestion: Decouple Threading Logic for a More Flexible and Robust Design #3

@mpryahin

Description

@mpryahin

First off, thanks for creating this library! It's a great abstraction for a very common problem.

I've been looking through the implementation and had some thoughts on the threading model that I wanted to share for discussion.

Current Design and Potential Issues

The library currently manages its own threading via the start_async method. While this is convenient, I've noticed a couple of potential issues with this approach:

  1. Race condition. In threaded mode, the _background_capture method writes to self._latest_frame at the same time the main application thread might be reading it via get_latest_frame. (example) Since this access isn't synchronized with a lock, it could lead to race conditions like torn frames, especially under heavy load.

  2. Slightly confusing base class logic: In VideoCaptureBase, the _read_frame_for_thread method contains a potentially confusing call to self.read(), which in turn calls other methods that check the thread's status. It seems to create a tangled execution path that could be hard to debug. The subclasses appear to handle this correctly by using a _read_direct method, but the base implementation itself is a bit confusing.

Proposal: A Synchronous Core with External Concurrency

The real strength of this library is how it handles each individual source type's settings, abstracts away their configurations using the factory method pattern, and implements the reading logic from each of the supported sources.

What if the library's core classes were simplified to be purely synchronous, leaving the concurrency management to the application developer?

My suggestion is to remove the internal threading logic (start_async, _background_capture, etc.) and provide a simple, blocking read() method as the primary way to get a frame.

Benefits of this Approach:

  • The developer gains full control over how to run the reader: in a simple loop, wrap it in a threading.Thread for a producer-consumer pattern, or use multiprocessing.Process to get around the GIL. The library no longer imposes a specific concurrency model.
  • This design eliminates the race condition by its very nature and simplifies the library's internal state, making it easier to maintain and reason about.
  • The public API becomes more focused and predictable. A simple connect(), disconnect(), and read() is very intuitive.

Suggested API and Usage:

The core VideoCapture classes would just have a clean, synchronous API:

class VideoCapture:
    def connect(self):        # Establish a connection
    def disconnect(self):     # Release the resource
    def read(self):           # Synchronously reads a single frame
    def config(self):         # Returns the source configuration 

The application developer would then be responsible for building their own producer logic, giving them full control over the architecture:

def frame_producer(source, frame_queue, stop_event):
    try:
        source.connect()
        while not stop_event.is_set():
            success, frame = source.read()
            if not success:
                break
            frame_queue.put(frame)
    finally:
        source.disconnect()

This frame_producer can then be run directly in the main application thread, or given to a threading.Thread or multiprocessing.Process depending on the application's specific needs.

This change would turn the FrameSource classes into predictable building blocks that can be used in a much wider variety of application architectures.

Would love to hear your thoughts on this idea!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions