Skip to main content

A Python library for hierarchical thread group management with event-based coordination.

Project description

Thread Mangement Library

This library addresses the below requirements:

  • Logically group Thread objects, with thread pooling functionality, to limit the number of concurrent threads.
  • Maintaining a parent-child hierarchy of threads, addressing the limitation of Python's built-in library wherein all threads are equal.
  • Allowing threads to communicate with their siblings or children through an abstracted event based system.

Threads are grouped by ThreadGroup objects. A ThreadGroup object is created in the parent thread, and tasks are added as per the use case. We can specify the maximum number of concurrent threads this group should hold, at instantiation time.

Example usage:

thread_group = ThreadGroup("Verifying Users", max_concurrent_threads=10)  
# Thread group to verify users in a data structure parallely

for user in users:
    thread_group.add_task(verify_user, args=(user))
    # If number of users are greater than 10, 
    # ThreadGroup will automatically limit the number of active threads

thread_group.join(600)  #  Waits for threads to terminate with a timeout of 600 seconds.

What if we wanted just one verified user? There is no default way for a thread to be aware that this condition is met. Neither is there a mechanism to kill an active thread, as that would be unsafe.

Every time a ThreadGroup object is created there is an associated Event object that is maintained. The parent thread has this context through the ThreadGroup object, and all the threads spawned by the thread group (i.e. the child threads) are aware of this object too. This is faciliated by maintaining an n-ary tree to track the hierarchy.

If two thread group objects are created inside the main thread, the tree's state will be

         MainThread(ThreadEventTreeNode)
                    /    \
                   /      \
                  /        \
                 /          \
      ThreadGroup1         ThreadGroup2

Each node has an Event object associated with it. And each thread group would have spawned several worker threads which are logically encapsulated inside the ThreadGroup object.

         MainThread(ThreadEventTreeNode)
                    /    \
                   /      \
                  /        \
                 /          \
      ThreadGroup1         ThreadGroup2
       ~ Thread1            ~ Thread4
       ~ Thread2            ~ Thread5
       ~ Thread3            ~ Thread6

Each thread group has 3 threads. There are 2 use cases for thread communication:

  • A parent thread signalling to its children.
  • A child thread signalling to its siblings.

Let's consider the first case and the earlier example of User verification.

valid_users = []

def verify_user(user):
    retries = 3
    while retries > 0:
        try:
            verify_user_internal()  # internal call made to verify user
            valid_users.append(user)
        except UserVerificationException:
            logger.info("Failed to verify user, sleeping for 5 seconds and retrying")

            # At this stage, if the event is set in the next 5 seconds, we will exit the verification loop.
            if not thread_utils.event_aware_sleep(5):  
                break

Note that checks for the state of the event can be added in key places as required, to ensure that we are not unnecessarily performing operations, and we are safely terminating threads.

When threadutils methods are used by child threads, it automatically fetches the event associated with the thread group of the currently executing child thread. If no event is found, i.e. this is the main thread with no parent group, then thread_utils.event_aware_sleep will perform a regular sleep.

thread_group = ThreadGroup("Verifying Users")
for user in users:
    thread_group.add_task(verify_user, args=(user))

threadutils cannot auto-fetch the event associated with the current (parent) thread, as it could have created several different ThreadGroups. It needs context of which ThreadGroup to perform operations on, so its usage would be through the ThreadGroup object.

thread_group.wait_for_condition(lambda: len(valid_users) > 0, timeout=600)

The parent thread will wait for the length of the list valid_users to be greater than zero, and then inform the child threads to terminate, since we need just one valid user.

thread_group.join(600)

The ThreadEvents Controller maintains the hierarchy of all the thread groups created.

def worker_method():
    # We are creating multiple thread groups inside this method, each executing respective tasks in parallel
    thread_group_2_1 = ThreadGroup()
    thread_group_2_2 = ThreadGroup()
    thread_group_2_3 = ThreadGroup()

thread_group_2 = ThreadGroup()
thread_group_2.add_task(worker_method)
         MainThread(ThreadEventTreeNode)
                    /    \
                   /      \
                  /        \
                 /          \
      ThreadGroup1         ThreadGroup2
                            /    |    \
                           /     |     \
                          /      |      \
                         /       |       \
           ThreadGroup2.1        |        ThreadGroup2.3
                                 |
                           ThreadGroup2.2

Let's say ThreadGroup2 realizes it needs to abort all operations, We can simply call

thread_group_2.set_event_for_child_tasks()

and all events for the thread group and its child thread groups will be set, effectively signalling all threads spawned as part of this set of operations.

The hierarchy is maintained and computed, completely abstracted. It doesn't require any additional consideration when creating a ThreadGroup object.

It is also possible for the n-ary tree to become a forest, in case we spawn daemon threads from a thread group, and they still need to communicate amongst each other. Information about this disjoint tree will also be maintained automatically.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

threadgroup_manager-0.1.1.tar.gz (8.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

threadgroup_manager-0.1.1-py3-none-any.whl (9.8 kB view details)

Uploaded Python 3

File details

Details for the file threadgroup_manager-0.1.1.tar.gz.

File metadata

  • Download URL: threadgroup_manager-0.1.1.tar.gz
  • Upload date:
  • Size: 8.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.9.6

File hashes

Hashes for threadgroup_manager-0.1.1.tar.gz
Algorithm Hash digest
SHA256 e79141405d2ae3fad06fdd2e6931cc6211b6933577825664172b8c462e40bd48
MD5 4f6b4f43f3a694719235cf75f0c615ae
BLAKE2b-256 6dbd340f9d02f8b12814ae7739bfc3baf0d7a74186b73c90ffc9ecd8d65b40c7

See more details on using hashes here.

File details

Details for the file threadgroup_manager-0.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for threadgroup_manager-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 4ef8b3d480a2091e71de7009eaef87f244a69469c828db48b15b53f402c80331
MD5 19a7875ee6bd81db33590f0ab1e258cd
BLAKE2b-256 10565f3ba2db7cbfa0396895805268d86036e5e1e1ea25949f6d973bd27a6b81

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page