How to Execute a Task with a Delay in a Child Process in Python

March 5, 2023 Python Multiprocessing

You can execute a task in a child process with a delay using a wrapper function that sleeps first before executing the target function.

A more elaborate approach can be developed that extends the Process class allowing an arbitrary target function to be executed after a delay. The delayed task can also be canceled by interrupting it before it has started to execute, mimicking the capabilities for the threading.Timer class, but for processes in this case, instead of threads.

In this tutorial, you will discover how to execute a task in a child process with a delay.

Let's get started.

Need a Delayed Task

We can execute target functions in a new child process using the multiprocessing.Process class.

This requires creating an instance of the class and specifying the target function to execute via the "target" argument.

For example:

...
# execute the target function in a child process
process = Process(target=function)

Once created, the process can be started by calling the start() method.

For example:

...
# start the child process
process.start()

This will run the target function in a new child process and leave the caller free to resume other tasks.

If the caller needs to wait for the target function to complete, the join() method on the process can be called which will block until the child process has terminated.

...
# wait for the child process
process.join()

You can learn more about running functions in a child process in the tutorial:

We can also execute code in a new process by extending the Process class and overriding the run() method.

This method will be called automatically when the start() method of the process is called.

You can learn more about extending the process class in the tutorial:

We may want to run a function in a child process, but wait for a fixed delay before executing the task.

For example, we may want to perform some calculations, but only after a delay of 30 seconds.

How can we execute a target function in a child process with a delay?

How to Run a Delayed Task

There are many ways to run a target function in a child process after a delay.

We will explore two approaches, they are:

  1. Define a custom function to run the function after a delay.
  2. Develop a custom Timer class.

Let's take a closer look at both.

Custom Function to Run Target Function After a Delay

We can execute a target function in a child process after a delay by defining a custom function.

The custom function will take the delay in seconds and the function to execute and will call the sleep() function for the given number of seconds before creating a child process and using it to execute the target function.

For example:

# delayed task function
def delayed(interval, function):
    # wait a while
    sleep(interval)
    # execute the target function in a child process
    process = Process(target=function)
    # start the child process
    process.start()
    # wait for the child process
    process.join()

The function can be updated to take arguments for the target function.

The function can also be updated to return the process instead of joining it, allowing the caller to choose what to do while the task is running.

You can learn more about the sleep() function in the tutorial:

Timer Class for Child Processes

Alternatively, we can develop a Timer class.

The threading module provides the threading.Timer class that extends the threading.Thread class and allows a function to be executed after a fixed delay.

You can learn more about the Timer class in the tutorial:

No such class exists in the multiprocessing module for use with processes.

As such, we can develop our own version of the class.

This requires extending the Process class and defining a constructor that takes a delay interval in seconds and a target function to execute (as well as any arguments for the target function).

The run() method of the Process can be overridden to first delay by the interval with a call to sleep, then call the target function.

For example:

# execute a target function in a child process after a delay
class Timer(Process):
    def __init__(self, interval, function):
        # call the parent constructor
        Process.__init__(self)
        # store the details
        self._interval = interval
        self._function = function

    # execute the target function after a delay
    def run(self):
        # wait a while
        sleep(self._interval)
        # execute the target function
        self._function()

The class can then be updated to offer more features.

A key feature of the threading.Timer class is that the task can be canceled before it is started, e.g. during the delay period.

This can be achieved using an event.

The constructor of the class can define a multiprocessing.Event.

A cancel() method can be added that will set the event.

The run() method can then call the wait() method on the event to wait for the interval in seconds. If the event is set by another process, the run() method will stop waiting. It can then check if the event was set and if so, not call the target function, e.g. cancel.

Put another way, the run() method will only execute the target function if the event is not set.

The updated multiprocessing Timer class with this capability is listed below.

# execute a target function in a child process after a delay
class Timer(Process):
    def __init__(self, interval, function):
        # call the parent constructor
        Process.__init__(self)
        # store the details
        self._interval = interval
        self._function = function
        # create the event
        self._event = Event()

    # execute the target function after a delay
    def run(self):
        # wait a while on the event
        self._event.wait(self._interval)
        # confirm that the event was not set
        if not self._event.is_set():
            # execute the target function
            self._function()
            # set the event so we cannot run it again
            self._event.set()

    # cancel the task
    def cancel(self):
        self._event.set()

You can learn more about the multiprocessing.Event in the tutorial:

Now that we know how to execute a target function in a child process after a delay, let's look at some worked examples.

Example of Executing a Delayed Task in a Child Process

We can explore how to use a custom function to delay the execution of a target function in a child process.

In this example, we will define a custom function named delayed() that takes a delay in seconds and the name of the target function to execute.

This delayed() function will sleep for a given number of seconds, then configure, start and wait for a new child process that executes the target function.

# delayed task function
def delayed(interval, function):
    # wait a while
    sleep(interval)
    # execute the target function in a child process
    process = Process(target=function)
    # start the child process
    process.start()
    # wait for the child process
    process.join()

The delayed() function does not have to wait for the target function. Instead, it could return the process instance and allow the caller to choose what to do while the target function is executing.

We can then define a task to execute after a delay.

In this case, our task will report a message, sleep for 2 seconds to simulate computational effort, then report a final message.

# task to delay
def task():
    # report a message
    print('Task is running...', flush=True)
    # simulate work
    sleep(2)
    # report final message
    print('Task is done', flush=True)

Finally, we can execute our target task function after a delay from the main process.

# protect the entry point
if __name__ == '__main__':
    # run the task after a delay
    delayed(1.0, task)

Tying this together, the complete example is listed below.

# SuperFastPython.com
# example of a delayed multiprocessing task
from time import sleep
from multiprocessing import Process

# delayed task function
def delayed(interval, function):
    # wait a while
    sleep(interval)
    # execute the target function in a child process
    process = Process(target=function)
    # start the child process
    process.start()
    # wait for the child process
    process.join()

# task to delay
def task():
    # report a message
    print('Task is running...', flush=True)
    # simulate work
    sleep(2)
    # report final message
    print('Task is done', flush=True)

# protect the entry point
if __name__ == '__main__':
    # run the task after a delay
    delayed(1.0, task)

Running the example calls the delayed() function with a delay of one second and the name of the target function.

The delayed() function runs, sleeps for 2 seconds, then executes the target function in a child process.

The main process blocks while the target function runs, by design.

The task reports a message, sleeps for 2 seconds, then reports the final message.

This highlights how we can use a wrapper function or custom function to execute a target function after a delay.

Task is running...
Task is done

Next, let's explore achieving the same effect using a Timer class.

Example of Multiprocessing Timer Class

We can develop a Timer class to run a target function after a delay using a child process, similar to the threading.Timer class.

In this case, we can keep things simple and assume no arguments to the target function and no ability to cancel the delayed task.

# execute a target function in a child process after a delay
class Timer(Process):
    def __init__(self, interval, function):
        # call the parent constructor
        Process.__init__(self)
        # store the details
        self._interval = interval
        self._function = function

    # execute the target function after a delay
    def run(self):
        # wait a while
        sleep(self._interval)
        # execute the target function
        self._function()

We can use the same target task() function as the previous example.

Finally, the main process can create a Timer object, specify a delay and the name of the target function, then start the process and wait for it to complete.

# protect the entry point
if __name__ == '__main__':
    # create the timer
    timer = Timer(1.0, task)
    # start the timer
    timer.start()
    # wait for he timer to complete
    timer.join()

Tying this together, the complete example is listed below.

# SuperFastPython.com
# example of a multiprocessing timer class for running delayed tasks
from time import sleep
from multiprocessing import Process

# execute a target function in a child process after a delay
class Timer(Process):
    def __init__(self, interval, function):
        # call the parent constructor
        Process.__init__(self)
        # store the details
        self._interval = interval
        self._function = function

    # execute the target function after a delay
    def run(self):
        # wait a while
        sleep(self._interval)
        # execute the target function
        self._function()

# task to delay
def task():
    # report a message
    print('Task is running...', flush=True)
    # simulate work
    sleep(2)
    # report final message
    print('Task is done', flush=True)

# protect the entry point
if __name__ == '__main__':
    # create the timer
    timer = Timer(1.0, task)
    # start the timer
    timer.start()
    # wait for he timer to complete
    timer.join()

Running the example first creates an instance of the Timer class with a one-second delay to execute the task() function.

The process is then started and the main process waits for the child process to terminate.

The child process runs, first sleeping for an interval of one second, then executing the target function.

This highlights how we can extend the multiprocessing.Process class to execute a target function after a delay.

Task is running...
Task is done

Next, let's look at how we might add the ability to cancel a delayed task.

Example of a Delayed Task That Can Be Canceled

We can explore how to cancel a delayed task using the custom Timer class.

This involves first adding a multiprocessing.Event object to the Timer class constructor.

def __init__(self, interval, function):
    # call the parent constructor
    Process.__init__(self)
    # store the details
    self._interval = interval
    self._function = function
    # create the event
    self._event = Event()

Next, we can add a cancel() method that will set the event when called.

# cancel the task
def cancel(self):
    self._event.set()

Finally, we can update the run() method to wait for the interval on the event directly.

This will block until either the event is set or the time elapses.

Once the wait is completed, if the event is not set, we can execute the target function and set the event. Otherwise, the task has been canceled and we do not execute the target function.

# execute the target function after a delay
def run(self):
    # wait a while on the event
    self._event.wait(self._interval)
    # confirm that the event was not set
    if not self._event.is_set():
        # execute the target function
        self._function()
        # set the event so we cannot run it again
        self._event.set()

We set the event after running the function because the task can only be run once. A process class cannot be reused.

You can learn more about the inability to reuse process classes in the tutorial:

Tying this together, the complete example of the updated Timer class that can be canceled is listed below.

# execute a target function in a child process after a delay
class Timer(Process):
    def __init__(self, interval, function):
        # call the parent constructor
        Process.__init__(self)
        # store the details
        self._interval = interval
        self._function = function
        # create the event
        self._event = Event()

    # execute the target function after a delay
    def run(self):
        # wait a while on the event
        self._event.wait(self._interval)
        # confirm that the event was not set
        if not self._event.is_set():
            # execute the target function
            self._function()
            # set the event so we cannot run it again
            self._event.set()

    # cancel the task
    def cancel(self):
        self._event.set()

Next, we can explore how to use this class.

First, we can run the normal case to confirm that a target function can still be executed after a delay.

# SuperFastPython.com
# example of a multiprocessing timer class that can be canceled
from time import sleep
from multiprocessing import Process
from multiprocessing import Event

# execute a target function in a child process after a delay
class Timer(Process):
    def __init__(self, interval, function):
        # call the parent constructor
        Process.__init__(self)
        # store the details
        self._interval = interval
        self._function = function
        # create the event
        self._event = Event()

    # execute the target function after a delay
    def run(self):
        # wait a while on the event
        self._event.wait(self._interval)
        # confirm that the event was not set
        if not self._event.is_set():
            # execute the target function
            self._function()
            # set the event so we cannot run it again
            self._event.set()

    # cancel the task
    def cancel(self):
        self._event.set()

# task to delay
def task():
    # report a message
    print('Task is running...', flush=True)
    # simulate work
    sleep(2)
    # report final message
    print('Task is done', flush=True)

# protect the entry point
if __name__ == '__main__':
    # create the timer
    timer = Timer(1.0, task)
    # start the timer
    timer.start()
    # wait for he timer to complete
    timer.join()

Running the example creates the timer class, starts it, then waits for it to complete.

The task runs normally, first delaying for 1 second, then executing the task as we did in the previous section.

Task is running...
Task is done

Next, we can test the cancel case.

In this case, we will start the timer child process, wait a moment during the delay period, then cancel the task.

# protect the entry point
if __name__ == '__main__':
    # create the timer
    timer = Timer(1.0, task)
    # start the timer
    timer.start()
    # wait a moment
    sleep(0.5)
    # cancel the task
    print(Canceling...')
    timer.cancel()
    # wait for the child process to terminate
    timer.join()

Tying this together, the complete example of canceling the delayed task is listed below.

# SuperFastPython.com
# example of a multiprocessing timer class that can be canceled
from time import sleep
from multiprocessing import Process
from multiprocessing import Event

# execute a target function in a child process after a delay
class Timer(Process):
    def __init__(self, interval, function):
        # call the parent constructor
        Process.__init__(self)
        # store the details
        self._interval = interval
        self._function = function
        # create the event
        self._event = Event()

    # execute the target function after a delay
    def run(self):
        # wait a while on the event
        self._event.wait(self._interval)
        # confirm that the event was not set
        if not self._event.is_set():
            # execute the target function
            self._function()
            # set the event so we cannot run it again
            self._event.set()

    # cancel the task
    def cancel(self):
        self._event.set()

# task to delay
def task():
    # report a message
    print('Task is running...', flush=True)
    # simulate work
    sleep(2)
    # report final message
    print('Task is done', flush=True)

# protect the entry point
if __name__ == '__main__':
    # create the timer
    timer = Timer(1.0, task)
    # start the timer
    timer.start()
    # wait a moment
    sleep(0.5)
    # cancel the task
    print(Canceling...')
    timer.cancel()
    # wait for the child process to terminate
    timer.join()

Running the example first creates the timer class and then starts the child process.

The child process runs and starts the waiting process on the event.

The main process sleeps for half a second then resumes and cancels the task.

This causes the child process to stop waiting immediately and exit the run() method without executing the target function, having the desired effect.

This highlights how we can cancel a delayed task.

Canceling...

Takeaways

You now know how to execute a task in a child process with a delay.



If you enjoyed this tutorial, you will love my book: Python Multiprocessing Jump-Start. It covers everything you need to master the topic with hands-on examples and clear explanations.