Export to GitHub

python-safethread - Monitors.wiki


Given how limiting it is to be unable to share mutable objects, a way is needed to get around the restrictions. Monitors are the primary tool for this.

Essentially what a Monitor does is it creates a wall around your object — nothing unshareable gets in or out. It then lets one thread at a time inside this wall, forcing any other threads to wait until the first one leaves.

```

counter.py

from future import shared_module

from threadtools import Monitor, monitormethod

class Counter(Monitor): """A simple counter, shared between threads""" shared = True # More shared_module boilerplate

def __init__(self):
    self.count = 0

@monitormethod
def tick(self):
    self.count += 1

@monitormethod
def value(self):
    return self.count

```

The count attribute is hidden within the Counter instances. @monitormethod indicates that a function should run within the Monitor.

Counter can be used like this:

```

main.py

from future import shared_module

from counter import Counter from threadtools import branch

def work(c): for i in range(20): c.tick()

def main(): c = Counter()

with branch() as children:
    for i in range(10):
        children.add(work, c)

print("Number of ticks:", c.value())

```

When main is called it creates a Counter, then spawns 10 threads to access it. Each of those threads calls c.tick() 20 times. After the threads exit the total number of ticks is printed: 200.

Caveat

Due to some problems with how python runs its startup script, the __future__ import does not work. To work around this you should explicitly import main.py, then run its main function.

$ ./python -c 'import main; main.main()' Number of ticks: 200 $

Conditions

In addition to allowing you to share objects between threads, Monitors also provide a facility for waiting until you can do a certain activity on those those objects - called a condition.

```

queue.py

from future import shared_module

from collections import deque from threadtools import Monitor, monitormethod, condition, wait

class Queue(Monitor): """A simple thread-safe queue""" shared = True # More shared_module boilerplate

def __init__(self, limit=None):
    self.data = deque()
    self.limit = limit

@condition
def _notfull(self):
    if self.limit is None:
        return True
    return len(self.data) < self.limit

@condition
def _notempty(self):
    return bool(self.data)

@monitormethod
def put(self, value):
    wait(self._notfull)
    self.data.append(value)

@monitormethod
def get(self):
    wait(self._notempty)
    return self.data.popleft()

```

Although similar to traditional conditions used for threading, these are defined as part of the class, which gives them some interesting properties: * You cannot have unique wakeup predicates for each thread in a Monitor - but you could give them each their own Monitor to work around that * There's a fixed number of conditions for each Monitor, giving them a fixed cost, regardless of the number of waiting threads * Leaving the Monitor (by returning from a monitormethod) triggers reevaluation of the conditions - changes in another Monitor may not wake up this one * No loop is required to reevaluate the predicate - it is guaranteed to be true if wait() returns without raising an exception * Only the waiting function specifies which conditions it cares about. Other functions need not concern themselves with explicitly waking certain threads after they change the state

Additionally, waiting on a condition is a cancellable operation.