As a rule of thumb, class-based threads may be better if your threads require per-thread
state, or can leverage any of OOP’s many benefits in general. Your thread classes don’t
necessarily have to subclass Thread, though. In fact, just as in the _thread module, the
thread’s target in threading may be any type of callable object. When combined with
techniques such as bound methods and nested scope references, the choice between
coding techniques becomes even less clear-cut:
# a non-thread class with state, OOP
class Power:
def __init__(self, i):
self.i = i
def action(self):
print(self.i ** 32)
obj = Power(2)
threading.Thread(target=obj.action).start() # thread runs bound method
# nested scope to retain state
def action(i):
def power():
print(i ** 32)
return power
threading.Thread(target=action(2)).start() # thread runs returned function
# both with basic thread module
_thread.start_new_thread(obj.action, ()) # thread runs a callable object
_thread.start_new_thread(action(2), ())
As usual, the threading APIs are as flexible as the Python language itself.
Synchronizing access to shared objects and names revisited
Earlier, we saw how print operations in threads need to be synchronized with locks to
avoid overlap, because the output stream is shared by all threads. More formally,
threads need to synchronize their changes to any item that may be shared across thread
in a process—both objects and namespaces. Depending on a given program’s goals,
this might include:
- Mutable object in memory (passed or otherwise referenced objects whose lifetimes
span threads) - Names in global scopes (changeable variables outside thread functions and classes)
- The contents of modules (each has just one shared copy in the system’s module
table)
For instance, even simple global variables can require coordination if concurrent up-
dates are possible, as in Example 5-12.
202 | Chapter 5: Parallel System Tools