Anatomy of a TaskThread

Imports

The necessary imports for writing your TaskThread are:

import asyncio, logging, traceback
from task_thread import TaskThread, reCreate, reSchedule,\
    delete, verbose, signals

Subclassing

You create a custom thread by subclassing from the TaskThread base class.

The methods in TaskThread that you must override are:

def __init__(self, parent = None)

def initVars__(self)

async def enter__(self)

async def exit__(self)

async def signalHandler__(self, signal)

async def childsignalHandler__(self, signal, child)

def getId(self)

def getInfo(self)

Let’s take a closer look at each one of these methods.

In the __init__ you must call the superclass method (similar to threading and multiprocess modules):

def __init__(self, parent = None):
    super().__init__(parent = parent)
    # whatever extra stuff

In initVars you define the rescheduling tasks that define the functionality of your TaskThread:

def initVars__(self):
    """Create & initialize here your tasks with none & create your locks
    """
    self.tasks.writer = None
    self.tasks.reader = None
    # etc.
    self.locks.writer = asyncio.Lock()
    # etc.

Notice how the tasks are organized under their own namespace self.tasks. All tasks are initialized as None. There is a separate convenience namespace self.locks for asyncio locks.

The starting point for your thread is defined in enter__:

@verbose
async def enter__(self):
    self.logger.info("enter__ : %s", self.getInfo())
    self.tasks.writer = await reCreate(self.tasks.writer, self.readerMethod__)
    self.tasks.reader = await reCreate(self.tasks.reader, self.writerMethod__)

Here we use a special decorator @verbose that makes life with asyncio a bit easier - it catches some exceptions explicitly for you.

We start the rescheduled tasks using the convenience function reCreate. The target of the task is self.readerMethod__ where the task is defined (more on this in the section about tasks).

Next, you still remember the hierarchical way the threads are organized and how they communicate? signalHandler__ defines what the TaskThread should do when it gets a message/data from a parent:

@verbose
async def signalHandler__(self, signal):
    self.logger.info("signalHandler__ : got signal %s from parent", signal)

The implementation of this method depends, of course, completely on your TaskThread’s functionality.

A thread must also know what to do when it gets a signal from a child. This is defined in childsignalHandler__:

@verbose
async def childsignalHandler__(self, signal, child):
    self.logger.debug("childsignalHandler__ : got signal %s from child %s", signal, child.getId())

Finally, getId returns some unique string or int corresponding to this TaskThread (nice for search/organizational purposes), while getInfo returns a string representation of the TaskThread (i.e. like __str__).

You could write:

def getId(self):
    return str(id(self))

def getInfo(self):
    return "<MyThread "+str(self.getId())+">"

API Methods

By using subclassing, we have defined what our TaskThread does. Next we take a look at the API methods, i.e. how to use a TaskThread.

Let’s take a quick overview of the available methods in the TaskThread class.

However, in order to know how to really use these methods, you need to go through the examples.

A TaskThread is created like this:

thread = MyThread(parent = parent)

Start running it with:

await thread.run()

Stop with:

await thread.stop()

and wait until it has finished:

await thread.join()

A child thread can terminate itself, by calling self.stop().

Stopping a child automatically deregisters / removes it from any listening parent.

You can add a child thread to a parent thread:

await thread.addChild(child)

After that, parent starts listening any signals from the child.

Finding a child, based on it’s id, as returned by it’s getId() method is done with:

await thread.findChild(_id = _id)

Sending a signal from parent to child, i.e. down/deeper in the hierarchical parent/child structure:

await thread.sigFromParentToChild__(signal, child)

If child is replaced by None, the same signal is sent to all children.

Sending a signal the other way around: from children to parent, i.e. upwards in the tree:

await thread.sigFromChildToParent__(signal)

Signals

Signals are those things that go to and fro between parent and child threads.

A typical signal looks like this:

class MessageSignal(signals.Signal):
    """A generic message message signal, carrying a python object
    """
    def __init__(self, message):
        self.message = message

    def __str__(self):
        return "<MessageSignal with message %s>" % (str(self.message))

    def getMessage(self):
        return self.message

    def __call__(self):
        """syntactic sugar"""
        return self.getMessage()

Signals can carry messages, byte payload, whatever.

Next, let’s take a closer look at tasks.