Concurrent Programming
Concurrent Programming
php/learn/courses/82-intermediate-advanced-python
Concurrent Programming
INTRODUCTION
In this section, we'll talk about multithreading as well as multiprocessing. Threads are lightweight when
compared to processes. All threads of a process share the same memory space. They are faster to create and
destroy. Threads of a process can communicate more easily via queues and events. Processes use separate
memory spaces and they communicate using system-level interprocess communication mechanisms.
Switching from one process to another is an expensive affair.
Despite these advantages of threads, multithreaded applications are more difficult to develop since all
threads share the same memory space. One thread can corrupt another's work. Threads have to coordinate,
via semaphores for example, access to shared resources. It's for this reason that Python's default
implementation CPython does not support threads that can execute at the same time. It implements
something called the Global Interpreter Lock (GIL), which basically means that CPython allows only one
thread to run at a time even if the system has multiple CPUs. It still supports multithreading at the
"application level". When multiple threads can run at the same time, it's called parallel computing. This is
possible only if the system has multiple CPUs. When multiple threads can exist at the same time but they
take turns to run, it's called concurrent computing. This begs the question, "What use is concurrency when
you have only one CPU?"
Concurrency is still useful for tasks that are not compute intensive and involve a lot of IO. For example, a
thread may be waiting for disk access or response from a web server. During this wait period, another thread
is given the chance to execute. If the program used only a single thread, other tasks will get held up because
of the wait. In fact, the CPU itself may be given to another ready-to-run process on the system. With
Python's multithreading, the underlying thread will be mapped to a ready-to-run application-level thread
when the current application-level thread gets into a wait.
We start with a simple example where function sleeper() is the entry point for each child thread created by
the main thread. Arguments can be passed to this function when the thread is created. The function starts
execution when start() method is called on the thread object. The threads do nothing more than sleep for
predetermined periods of time. The prints help us understand when the threads start and exit the system.
What's interesting in this example is that the main thread exits after it has created all the child threads. Since
these child threads are non-daemon, the program continues to run until all threads finish. If child threads are
created as daemons, you will notice that the program exits as soon as the main thread finishes. Daemon
threads will be forcefully terminated.
import threading
import datetime
1
import time
def strtimenow():
def threadids():
def sleeper(sleepfor=5):
print("[{:s}] [{}] [{:s}] Entering function: sleeping for {:d} secs. Main thread alive: {}".
format(\
time.sleep(sleepfor)
if __name__ == '__main__':
numThreads = 4
sleeptimes = [4, 6, 3, 5]
t.start()
2
54
In the preceding example, we were forced to obtain the thread's context within the function. In the next
example, we take an object-oriented approach. The thread's sleep time is specified at the time of creation via
the constructor. This is saved within the thread object and used by the run() method. This method is
automatically called when the thread object's start() is called. It will be obvious that the PIDs remain the
same for all threads because they are all part of the same process. The main thread that created the child
threads will wait for children to finish using the join() method.
import threading
import datetime
import time
import os
def strtimenow():
def pids():
class SleeperThread(threading.Thread):
self.sleepfor = sleepfor
super().__init__()
def run(self):
print("[{:s}] [{}] Sleeping thread {:d} for {:d} secs".format(strtimenow(), pids(), self
.ident, self.sleepfor))
time.sleep(self.sleepfor)
3
print("[{:s}] [{}] Exiting thread: ID {:d}".format(strtimenow(), pids(), self.ident))
if __name__ == '__main__':
numThreads = 4
sleeptimes = [4, 6, 3, 5]
childthrds = []
childthrds.append(SleeperThread(sleepfor))
childthrds[-1].start()
child.join()
55
In the preceding example, the main thread does not reap the child threads as soon as they are done. Reaping
is done in a linear order since join() is a blocking call. We improve this in the next example by using a
timeout. We also enhance the constructor so that extra arguments such as "name" can be passed to the thread
object.
import threading
import datetime
import time
import os
def strtimenow():
4
return datetime.datetime.strftime(datetime.datetime.now(), "%H:%M:%S")
def pids():
class SleeperThread(threading.Thread):
self.parent_ident = threading.current_thread().ident
self.sleepfor = sleepfor
super().__init__(*args, **kwargs)
def run(self):
print("[{:s}] [{}] [{}] [{}] Sleeping thread for {:d} secs".format(strtimenow(), pids(),
self.threadids(), self.name, self.sleepfor))
time.sleep(self.sleepfor)
def threadids(self):
if __name__ == '__main__':
numThreads = 4
sleeptimes = [4, 6, 3, 5]
5
threadid = threading.current_thread().ident
print("[{:s}] [{}] [{:d}] Starting {:d} child threads ...".format(strtimenow(), pids(), thre
adid, numThreads))
childthrds = []
childthrds.append(SleeperThread(sleepfor, name=name))
childthrds[-1].start()
print("[{:s}] [{}] [{:d}] Waiting for child threads ...".format(strtimenow(), pids(), thread
id))
while any(childthrds):
child.join(timeout=0.5)
childthrds[i] = None
56
All the above examples show threads that go to sleep, which is why every thread gets a chance to run. What
happens if we have a thread that does only computations, doesn't sleep or wait for IO? Will it hog the CPU
and lock out other threads in the Python process? The following example shows that this is not the case.
Python interpreter does not do any thread scheduling but it does preempt the active thread at regular
intervals so that another thread that's ready to run will acquire the GIL. Thus, the interpreter facilitates time
slicing of threads but it's the OS that does the actual scheduling. In fact, slicing is not based on time. It's
based on number of bytecodes. This is set to a default value of 100, which can be checked
at sys.getcheckinterval().
import threading
import datetime
import time
import math
6
def strtimenow():
def threadids():
def hogger(n):
result = func(n)
if __name__ == '__main__':
numThreads = 4
sleeptimes = [4, 6, 3, 5]
t.start()
7
time.sleep(1)
t.start()
57
import threading
import datetime
import time
def strtimenow():
def threadids():
8
strtimenow(), threadids(), threading.current_thread().name))
sem.acquire()
print("[{:s}] [{}] [{:s}] Entering function: sleeping for {:d} secs. Main thread alive: {}".
format(\
time.sleep(sleepfor)
sem.release()
if __name__ == '__main__':
numThreads = 4
sleeptimes = [4, 6, 3, 5]
sem = threading.Semaphore(2)
t.start()
58
Here are a few interesting changes you can do to the example above:
Create the semaphore with its default value and observe how the threads run: sem = threading.Semaphore()
In the sleeper() function, release the semaphore twice. Python will not complain but this should be treated
as a bug.
Repeat the above but change the semaphore to a bounded semaphore: sem = threading.BoundedSemaphore(2)
In fact, to ensure that semaphores can be used with context managers. This means that the following is a
better way to write the sleeper() function:
print("[{:s}] [{}] [{:s}] Entering function: sleeping for {:d} secs. Main thread alive:
{}".format(\
time.sleep(sleepfor)
59
Often threads need to communicate with one another. Queues make it easier to do this. Some threads may
write to the queue while others are reading from it. Queues automatically manage the locking mechanisms.
In the example below the main thread creates a queue and adds a number of URLs to the queue. Each URL
point to an image on the internet. The main thread then starts child threads. The child threads remove items
from the queue and start processing them. When a thread sees that the queue is empty, it terminates. The
main thread waits on the queue to ensure that all items in the queue have been processed.
import threading
import queue
import requests
import os
import shutil
import datetime
class HttpImgThread(threading.Thread):
super().__init__(*args, **kwargs)
self.__urlqueue = urlqueue
def run(self):
while True:
try:
url = self.__urlqueue.get_nowait()
10
if url:
self.__download_img(url)
self.__urlqueue.task_done()
except queue.Empty:
print("[{:s}] Exiting.".format(self.name))
return
"""Fetches raw contents given the image URL and saves it."""
imgname = os.path.basename(url)
shutil.copyfileobj(rsp.raw, ofile)
def __make_http_header(self):
return {
'Accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language' : 'en-us,en;q=0.5',
if __name__ == '__main__':
start_ts = datetime.datetime.now()
urls = f.readlines()
11
# Initialize the queue
urlqueue = queue.Queue()
numthreads = 4
threads = []
for i in range(numthreads):
threads.append(newThrd)
newThrd.start()
urlqueue.join()
end_ts = datetime.datetime.now()
duration = (end_ts-start_ts).total_seconds()
60
Here are a few interesting changes you can do to the example above:
Use a LIFO queue and see how the order of processing changes: queue.LifoQueue()
Use a priority queue and see how the order of processing changes: queue.PriorityQueue(). For this to work,
items have to added to the queue as tuples: (priority_num, url). Smaller numbers have higher priorities.
Create the queue with a limit: queue.Queue(4). You will see that the main thread blocks indefinitely.
Create the queue, start the threads and then add URLs to the queue. You will see that some child threads will
exit immediately. Results will also vary depending on the number of URLs.
The threads in the previous example can be said to be autonomous. They start working on items in the queue
and terminate when the queue is empty. This is possible because we know in advance what should go into
the queue. What happens if the URLs to be processed are determined dynamically after the threads have
already started? In that case, the above code will not work because threads will start and terminate as soon as
they see an empty queue. What about when the queue has a limit and the main thread attempts to put more
items into the queue? We can solve these scenarios using events.
12
import threading
import queue
import requests
import os
import shutil
import datetime
class HttpImgThread(threading.Thread):
super().__init__(*args, **kwargs)
self.__urlqueue = urlqueue
self.__done_event = done_event
def run(self):
try:
url = self.__urlqueue.get(timeout=1)
if url:
self.__download_img(url)
self.__urlqueue.task_done()
except queue.Empty:
continue
print("[{:s}] Exiting.".format(self.name))
"""Fetches raw contents given the image URL and saves it."""
imgname = os.path.basename(url)
13
with open(imgname, 'wb') as ofile:
shutil.copyfileobj(rsp.raw, ofile)
def __make_http_header(self):
return {
'Accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language' : 'en-us,en;q=0.5',
if __name__ == '__main__':
start_ts = datetime.datetime.now()
urls = f.readlines()
urlqueue = queue.Queue()
done_event = threading.Event()
numthreads = 4
threads = []
for i in range(numthreads):
threads.append(newThrd)
newThrd.start()
14
# Add items to the queue
urlqueue.join()
end_ts = datetime.datetime.now()
duration = (end_ts-start_ts).total_seconds()
61
TRUE MULTITHREADING
While true multithreading in the sense of parallel computing is not possible with the default Python
implementation, there are other implementations that allow this. JPython (Java) and IronPython (C#) are
alternatives that don't have the GIL. Unfortunately JPython is available only for Python 2. IronPython
supports Python 3 but community interest seems to be limited. IronPython3 also appears to be in
development mode. Going by their GitHub account, no formal releases have been made.
Building IronPython3 on Windows 10 is quite easy. Two things are needed to build it: .NET Framework,
version 3.5 SP1 and above; Microsoft Visual Studio. Using the Developer Command Prompt for VS,
navigate to the folder where IronPython has been unzipped. Type make to build. Add IronPython3 base path
+ '\bin\Debug' to your environment PATH variable. IronPython3 can then be launched by typing "ipy".
However, it's not usable! Trying to import many modules will fail!
This basically means that true multithreading is possible only with JPython or IronPython for Python 2. If
you really want to use all your CPU cores, the next best option (perhaps a better option) is to use multiple
processes.
def sqr(x):
15
if __name__ == "__main__":
with Pool(5) as p:
time.sleep(1)
print(p.map(sqr, range(3)))
62
Remove the sleep in the main thread. Are you getting different child PIDs? Explain.
Without the sleep, increase the job queue to range(20). What happens to the child PIDs? Explain.
The Pool class creates a pool of processes, takes input data and assigns them to available processes in its
pool. If we wish to have greater control on individual processes, then the following example shows how to
do just that. The example also shows how processes can communicate using the multiprocessing.Queue class.
import time
import multiprocessing
while True:
msg = p2c.get()
print("Child:", reply)
c2p.put(reply)
else:
print("Child:", reply)
c2p.put(reply)
break
if __name__ == '__main__':
16
p2c, c2p = multiprocessing.Queue(), multiprocessing.Queue() # two unidirectional queues
p = multiprocessing.Process(target=child, args=(p2c,c2p))
p.start()
msgs = [
time.sleep(1)
print("Papa:", msg)
p2c.put(msg)
ret = p.join(timeout=2)
if p.is_alive():
continue
elif ret:
break
else:
break
p.join()
17
# Queue status
63
In the previous example we used two unidirectional queues. The alternative is to use
a multiprocessing.Pipe(), which encapsulate a pair of connections. By default, the connections are
bidirectional. Connections can be thought of as ends of the pipe connecting the two threads. In fact, pipes
can connect only two threads while queues can have multple producers and consumers. We rewrite the
above example in terms of a pipe.
import time
import multiprocessing
while True:
msg = cend.recv()
print("Child:", reply)
cend.send(reply)
else:
print("Child:", reply)
cend.send(reply)
break
if __name__ == '__main__':
p = multiprocessing.Process(target=child, args=(pend,cend))
p.start()
18
print("Current children:", multiprocessing.active_children())
msgs = [
time.sleep(1)
print("Papa:", msg)
pend.send(msg)
ret = p.join(timeout=2)
if p.is_alive():
continue
elif ret:
break
else:
break
p.join()
64
SCHEDULING EVENTS
Event in this context could be threading.Event or anything important that happens within the system. The
scheduler schedules events to happen at a time in the future. When that event happens, an associated
19
function is called to process the event. We may call this processing function an event handler. In the field of
simulation, discrete event scheduling is perhaps the perfect example where the sched module becomes
useful. For example, if one wishes to simulate customers arriving at a post office that has three queues, the
arrivals are all events and can be managed by this module. The following example illustrates how to use
some functions of the module:
# Create scheduler
s = sched.scheduler(time.time, time.sleep)
now = time.time()
future = int(now) + 4
# Start execution
In the above example, what would happen if you try to schedule an event in the past? What would happen if
an event handler takes too long that the processing of the next event doesn't happen at the scheduled time?
These are scenarios you can try out to understand the working of the scheduler better.
import subprocess
import sys
import os
print("="*80)
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print("="*80)
# This is not a built-in shell command: hence shell arg is not needed
21
print("Result: {:s}".format(res.stdout.decode())) # byte -> str
os.remove('another.py')
print("="*80)
66
We should note that the subprocess.run() module sits on top of the lower-level subprocess.Popen interface. By
design, subprocess.run() is blocking. If you require non-blocking execution, then subprocess.Popen() can be
used directly. The following shows how to use it:
import subprocess
import time
import psutil
# You can use your Task Manager (Windows) or System Monitor (Ubuntu)
# On some systems (Windows 10) the command will run in the child process
time.sleep(2)
22
time.sleep(1)
child.terminate()
child.wait()
# Error in initiating the child process will not terminate this process
time.sleep(2)
time.sleep(1)
child.terminate()
child.wait()
# Source: http://stackoverflow.com/questions/4789837/how-to-terminate-a-python-subprocess-launch
ed-with-shell-true
def killall(pid):
process = psutil.Process(pid)
child.kill()
process.kill()
time.sleep(2)
try:
child.wait(timeout=1)
except subprocess.TimeoutExpired:
killall(child.pid)
67
23