Python Concurrency for Senior Engineering Interviews
Python Concurrency for Senior Engineering Interviews
# declare a variable
some_var = "Educative"
print sys.getrefcount(some_var)
If you run the above snippet, you'll see the reference count of the
variable some_var increase. When references to an object are
removed, the reference count for an object is decremented. When the
reference count becomes zero, the object is deallocated. The
interpreter executes a single thread in order to ensure that the
reference count for objects is safe from race conditions.
Thread Safety
The primary motivation behind using multiple threads is improving
program performance that may be measured with metrics such as
throughput, responsiveness, latency, etc. Whenever threads are
introduced in a program, the shared state amongst the threads
becomes vulnerable to corruption. If a class or a program has
immutable state then the class is necessarily thread-safe.
CONFIDENTIAL & RESTRICTED
class Counter:
def __init__(self):
self.count = 0
def increment(self):
for _ in range(100000):
self.count += 1
if __name__ == "__main__":
numThreads = 5
threads = [0] * numThreads
counter = Counter()
if counter.count != 500000:
print(" count = {0}".format(counter.count), flush=True)
else:
print(" count = 50,000 - Try re-running the program.")
class Counter:
def __init__(self):
self.count = 0
self.lock = Lock()
def increment(self):
for _ in range(100000):
self.lock.acquire()
self.count += 1
self.lock.release()
if __name__ == "__main__":
numThreads = 5
threads = [0] * numThreads
counter = Counter()
if counter.count != 500000:
print(" If this line ever gets printed, " + \
"the author is a complete idiot and " + \
"you should return the course for a full refund!")
else:
print(" count = {0}".format(counter.count))
Barrier
CONFIDENTIAL & RESTRICTED
def thread_task():
time.sleep(random.randint(0, 7))
print("\nCurrently {0} threads blocked on barrier
".format(barrier.n_waiting))
barrier.wait()
num_threads = 5
barrier = Barrier(num_threads)
threads = [0] * num_threads
for i in range(num_threads):
threads[i - 1] = Thread(target=thread_task)
for i in range(num_threads):
threads[i].start()
def thread_task():
time.sleep(random.randint(0, 5))
print("\nCurrently {0} threads blocked on barrier
".format(barrier.n_waiting))
barrier.wait()
def when_all_threads_released():
CONFIDENTIAL & RESTRICTED
num_threads = 5
barrier = Barrier(num_threads, action=when_all_threads_released)
threads = [0] * num_threads
for i in range(num_threads):
threads[i - 1] = Thread(target=thread_task)
for i in range(num_threads):
threads[i].start()
Lock
As we have read GIL , It allows one thread at a time , then do
we still needs a lock?
GIL protects the Python interals. That means:
But GIL does not protect your own code. For example, if you have this
code:
self.some_number += 1
That is going to read value of self.some_number,
calculate some_number+1 and then write it back to self.some_number.
If you do that in two threads, the operations (read, add, write) of one
thread and the other may be mixed, so that the result is wrong.
Acquire
Release
The release() method will change the state of the Lock object to
unlocked and give a chance to other waiting threads to acquire the
lock. If multiple threads are already blocked on the acquire call then
only one arbitrarily chosen (varies across implementations) thread is
allowed to acquire the Lock object and proceed.
Deadlock
Consider the example below, where two threads are instantiated and
each tries to invoke release() on the lock acquired by the other thread,
resulting in a deadlock.
from threading import *
import time
CONFIDENTIAL & RESTRICTED
if __name__ == "__main__":
lock1 = Lock()
lock2 = Lock()
t1.start()
t2.start()
t1.join()
t2.join()
RLock
A reentrant lock is defined as a lock which can be reacquired by the
same thread. A RLock object carries the notion of ownership. If a
thread acquires a RLock object, it can chose to reacquire it as many
times as possible. Consider the following snippet:
Reentrant lock
rlock.acquire()
Condition Variables
Synchronization mechanisms need more than just mutual exclusion; a
general need is to be able to wait for another thread to do something.
Condition variables provide mutual exclusion and the ability for threads
to wait for a predicate to become true.
def printer_thread_func():
global prime_holder
global found_prime
if not exit_prog:
print(prime_holder)
prime_holder = None
found_prime = False
def is_prime(num):
CONFIDENTIAL & RESTRICTED
if num == 2 or num == 3:
return True
div = 2
return True
def finder_thread_func():
global prime_holder
global found_prime
i = 1
prime_holder = i
found_prime = True
i += 1
found_prime = False
prime_holder = None
exit_prog = False
printer_thread = Thread(target=printer_thread_func)
printer_thread.start()
finder_thread = Thread(target=finder_thread_func)
finder_thread.start()
printer_thread.join()
finder_thread.join()
CONFIDENTIAL & RESTRICTED
cond_var = Condition()
lock = Lock()
cond_var = Condition(lock) # pass custom lock to condition variable
cond_var.acquire()
cond_var.wait()
cond_var = Condition()
cond_var.acquire()
cond_var.wait()
Rewriting the above prime number finding and printing code using the
condition variable as below:
def printer_thread_func():
global prime_holder
global found_prime
cond_var.acquire()
while not found_prime and not exit_prog:
cond_var.wait()
cond_var.release()
if not exit_prog:
print(prime_holder)
prime_holder = None
cond_var.acquire()
found_prime = False
cond_var.notify()
cond_var.release()
def is_prime(num):
if num == 2 or num == 3:
return True
div = 2
return True
def finder_thread_func():
global prime_holder
global found_prime
i = 1
prime_holder = i
cond_var.acquire()
found_prime = True
cond_var.notify()
cond_var.release()
cond_var.acquire()
while found_prime and not exit_prog:
cond_var.wait()
cond_var.release()
i += 1
cond_var = Condition()
found_prime = False
prime_holder = None
exit_prog = False
printerThread = Thread(target=printer_thread_func)
printerThread.start()
finderThread = Thread(target=finder_thread_func)
finderThread.start()
cond_var.acquire()
cond_var.notifyAll()
cond_var.release()
printerThread.join()
finderThread.join()
def printer_thread_func():
global prime_holder
global found_prime
cond_var.acquire()
while not found_prime and not exit_prog:
cond_var.wait()
cond_var.release()
A thread that has been woken up does not imply that the conditions for
it to move forward hold. The thread must test the conditions again for
validity before moving forward. In conclusion, we must always check
for conditions in a loop and wait() inside it. The correct idiomatic usage
of a condition variable appears below:
acquire lock
while(condition_to_test is not satisfied):
wait
release lock
Quiz1:
Consider an abridged version of the code we discussed in this lesson.
The child_task method exits without releasing the lock. What would be
the outcome of running the program? The changed program is shown
below:
CONFIDENTIAL & RESTRICTED
flag = False
lock = Lock()
cond_var = Condition(lock)
def child_task():
global flag
name = current_thread().getName()
cond_var.acquire()
while not flag:
cond_var.wait()
print("\n{0} woken up \n".format(name))
print("\n{0} exiting\n".format(name))
if __name__ == "__main__":
thread1 = Thread(target=child_task, name="thread1")
thread1.start()
cond_var.acquire()
flag = True
cond_var.notify_all()
cond_var.release()
thread1.join()
print("main thread exits")
Semaphore:
Rewriting the prime find and print program using Semaphore
from threading import Thread
from threading import Semaphore
CONFIDENTIAL & RESTRICTED
import time
def printer_thread():
global primeHolder
def is_prime(num):
if num == 2 or num == 3:
return True
div = 2
def finder_thread():
global primeHolder
i = 1
primeHolder = i
CONFIDENTIAL & RESTRICTED
i += 1
sem_find = Semaphore(0)
sem_print = Semaphore(0)
primeHolder = None
exitProg = False
printerThread = Thread(target=printer_thread)
printerThread.start()
finderThread = Thread(target=finder_thread)
finderThread.start()
exitProg = True
printerThread.join()
finderThread.join()
Concurrent Package
CONFIDENTIAL & RESTRICTED
ThreadPoolExecutor
ProcessPoolExecutor
ThreadPoolExecutor
The ThreadPoolExecutor uses threads for executing submitted tasks.
Let's look at a very simple example.
def say_hi(item):
if __name__ == '__main__':
executor = ThreadPoolExecutor(max_workers=10)
lst = list()
for i in range(1, 10):
lst.append(executor.submit(say_hi, "guest" + str(i)))
executor.shutdown()
threading.Future
You can think of Future as an entity that represents a deferred
computation that may or may not have been completed. It is an object
that represents the outcome of a computation to be completed in
future. We can query about the status of the deferred computation
using the methods exposed by the Future class. Some of the useful
ones are:
def square(item):
# simulate a computation by sleeping
time.sleep(5)
return item * item
if __name__ == '__main__':
executor = ThreadPoolExecutor(max_workers=10)
future = executor.submit(square, 7)
executor.shutdown()
CONFIDENTIAL & RESTRICTED
Exception in Future
def square(item):
item = None
return item * item
if __name__ == '__main__':
executor = ThreadPoolExecutor(max_workers=1)
lst = list()
future = executor.submit(square, 7)
ex = future.exception()
print(ex)
executor.shutdown()
Adding Callbacks
future = executor.submit(square, 7)
future.add_done_callback(my_special_callback)
future.add_done_callback(my_other_special_callback)
def square(item):
return item * item
def my_special_callback(ftr):
res = ftr.result()
print("my_special_callback invoked " + str(res))
def my_other_special_callback(ftr):
res = ftr.result()
print("my_other_special_callback invoked " + str(res * res))
if __name__ == '__main__':
executor = ThreadPoolExecutor(max_workers=10)
future = executor.submit(square, 7)
future.add_done_callback(my_special_callback)
future.add_done_callback(my_other_special_callback)
executor.shutdown(wait=False)
Async.io
Sending and Receiving in a Generator:
Consider the snippet below:
def generate_numbers():
i = 0
while True:
i += 1
yield i
k = yield
print(k)
You may be surprised how this snippet behaves when we send and
receive data.
generator = generate_numbers()
item = next(generator)
print(item)
4. Note that in the above code the generator doesn't receive 5 when
we send() it. The value 5 is lost as the generator isn't suspended
at a yield assignment statement. In fact, the generator resumes
execution from the first yield statement and immediately blocks
at the second yield statement. In between, the two yield
statements no other line of code is executed. The main method
CONFIDENTIAL & RESTRICTED
def generate_numbers():
i = 0
while True:
i += 1
yield i
k = yield
print("Received in generator function: " + str(k))
if __name__ == "__main__":
generator = generate_numbers()
item = next(generator)
print("Received in main script: " + str(item))
# Nothing is received by the generator function
item = generator.send(5)
print("Received in main script: " + str(item))
# The second send is successful
item = generator.send(5)
print("Received in main script: " + str(item))
We can instead use a single one to do both and without the need to do
noop operations. The generator method to do this appears below:
def generate_numbers():
i = 0
while True:
i += 1
k = (yield i)
print(k)
Coroutine
Coroutine isn't a concept specific to Python. In fact, it is a general
programming concept also found in other programming languages. A
coroutine can be defined as a special function that can give up control
to its caller without losing its state.
Native coroutines
CONFIDENTIAL & RESTRICTED
Yield From
The yield from syntax is as follows:
def nested_generator():
i = 0
while i < 5:
i += 1
yield i
def outer_generator():
nested_gen = nested_generator()
if __name__ == "__main__":
gen = outer_generator()
isinstance(nested_gen, collections.abc.Iterable)
def nested_generator():
i = 0
while i < 5:
i += 1
yield i
def outer_generator_with_yield_from():
nested_gen = nested_generator()
yield from nested_gen
if __name__ == "__main__":
gen_using_yield_from = outer_generator_with_yield_from()
def nested_generator():
for _ in range(5):
k = yield
print("inner generator received = " + str(k))
def outer_generator():
nested_gen = nested_generator()
next(nested_gen)
for _ in range(5):
# receive the value from the caller
k = yield
try:
# send the value to the inner generator
nested_gen.send(k)
CONFIDENTIAL & RESTRICTED
except StopIteration:
pass
if __name__ == "__main__":
gen = outer_generator()
next(gen)
for i in range(5):
try:
gen.send(i)
except StopIteration:
pass
def nested_generator():
for _ in range(5):
k = yield
print("inner generator received = " + str(k))
def outer_generator():
nested_gen = nested_generator()
yield from nested_gen
if __name__ == "__main__":
gen = outer_generator()
next(gen)
for i in range(5):
try:
gen.send(i)
except StopIteration:
pass
except Exception:
print("caught an exception")
def outer_generator():
nested_gen = nested_generator()
next(nested_gen)
for _ in range(5):
try:
k = yield
except Exception as e:
nested_gen.throw(e)
try:
nested_gen.send(k)
except StopIteration:
pass
if __name__ == "__main__":
gen = outer_generator()
next(gen)
for i in range(5):
try:
if i == 1:
gen.throw(Exception("delibrate exception"))
else:
gen.send(i)
except StopIteration:
pass
So
import inspect
var = None
def nested_generator():
for _ in range(5):
k = yield
print("inner generator received = " + str(k))
def outer_generator():
global var
nested_gen = nested_generator()
var = nested_gen
next(nested_gen)
for _ in range(5):
k = yield
try:
nested_gen.send(k)
except StopIteration:
pass
if __name__ == "__main__":
gen = outer_generator()
next(gen)
try:
gen.close()
print("Outer generator state: " + inspect.getgeneratorstate(gen))
print("Inner generator state: " + inspect.getgeneratorstate(var))
except StopIteration:
pass
Output
Outer generator state: GEN_CLOSED
Inner generator state: GEN_SUSPENDED
CONFIDENTIAL & RESTRICTED
Contrast the above output with the output we get when we use yield
from. Both the generators are closed.
import inspect
var = None
def nested_generator():
for _ in range(5):
k = yield
print("inner generator received = " + str(k))
def outer_generator():
global var
nested_gen = nested_generator()
var = nested_gen
yield from nested_gen
if __name__ == "__main__":
gen = outer_generator()
next(gen)
try:
gen.close()
print("Outer generator state: " + inspect.getgeneratorstate(gen))
print("Inner generator state: " + inspect.getgeneratorstate(var))
except StopIteration:
pass
Output
Outer generator state: GEN_CLOSED
Inner generator state: GEN_CLOSED
CONFIDENTIAL & RESTRICTED
@asyncio.coroutine
def hello_world():
print("hello world")
Native Coroutines
Native coroutines can be defined using the async/await syntax. Before
getting into further details, here is an example of a very simple native
coroutine:
The above coroutine can be run with an event loop as follows:
import asyncio
if __name__ == "__main__":
# run the coroutine
loop = asyncio.get_event_loop()
CONFIDENTIAL & RESTRICTED
loop.run_until_complete(coro())
Async
We can create a native coroutine by using async def. A method
prefixed with async def automatically becomes a native coroutine.
Await
can be used to obtain the result of a coroutine object's execution.
await
We use await as:
await <expr>
import asyncio
import types
import inspect
from collections.abc import Iterable, Awaitable
# Ordinary Function
CONFIDENTIAL & RESTRICTED
def ordinary_function():
pass
# Simple Generator
def simple_generator():
assign_me = yield 0
# Generator-based coroutine
def generator_based_coroutine():
yield from asyncio.sleep(1)
# Native coroutine
async def native_coroutine():
pass
CONFIDENTIAL & RESTRICTED
if __name__ == "__main__":
of_aio_dec = ordinary_function_with_asyncio_coroutine_dec()
print(of_aio_dec)
print("simple generator instance of collections.abc.Iterable : " + st
r(isinstance(of_aio_dec, Iterable)))
print("simple generator instance of collections.abc.Awaitable : " + s
tr(isinstance(of_aio_dec, Awaitable)))
print("simple generator instance of types.Generator : " + str(isinsta
nce(of_aio_dec, types.GeneratorType)))
print("simple generator instance of types.CoroutineType : " + str(isi
nstance(of_aio_dec, types.CoroutineType)))
print("simple generator instance of asyncio.iscoroutine : " + str(asy
ncio.iscoroutine(of_aio_dec)))
print("simple generator instance of asyncio.iscoroutinefunction : " +
str(
asyncio.iscoroutinefunction(ordinary_function_with_asyncio_corout
ine_dec)))
print("simple generator instance of inspect.iscoroutine : " + str(ins
pect.iscoroutine(of_aio_dec)))
print("generator instance of inspect.iscoroutinefunction : " + str(
inspect.iscoroutinefunction(ordinary_function_with_asyncio_corout
ine_dec)))
print("simple generator instance of inspect.isawaitable : " + str(ins
pect.isawaitable(of_aio_dec)))
print("\n\n")
of_types_dec = ordinary_function_with_asyncio_coroutine_dec()
print(of_types_dec)
print("simple generator instance of collections.abc.Iterable : " + st
r(isinstance(of_types_dec, Iterable)))
print("simple generator instance of collections.abc.Awaitable : " + s
tr(isinstance(of_types_dec, Awaitable)))
print("simple generator instance of types.Generator : " + str(isinsta
nce(of_types_dec, types.GeneratorType)))
print("simple generator instance of types.CoroutineType : " + str(isi
nstance(of_types_dec, types.CoroutineType)))
print("simple generator instance of asyncio.iscoroutine : " + str(asy
ncio.iscoroutine(of_types_dec)))
print("simple generator instance of asyncio.iscoroutinefunction : " +
str(
asyncio.iscoroutinefunction(ordinary_function_with_types_coroutin
e_dec)))
print("simple generator instance of inspect.iscoroutine : " + str(ins
pect.iscoroutine(of_types_dec)))
print("generator instance of inspect.iscoroutinefunction : " + str(
CONFIDENTIAL & RESTRICTED
inspect.iscoroutinefunction(ordinary_function_with_types_coroutin
e_dec)))
print("simple generator instance of inspect.isawaitable : " + str(ins
pect.isawaitable(of_aio_dec)))
print("\n\n")
sg = simple_generator()
print(sg)
print("simple generator instance of collections.abc.Iterable : " + st
r(isinstance(sg, Iterable)))
print("simple generator instance of collections.abc.Awaitable : " + s
tr(isinstance(sg, Awaitable)))
print("simple generator instance of types.Generator : " + str(isinsta
nce(sg, types.GeneratorType)))
print("simple generator instance of types.CoroutineType : " + str(isi
nstance(sg, types.CoroutineType)))
print("simple generator instance of asyncio.iscoroutine : " + str(asy
ncio.iscoroutine(sg)))
print("simple generator instance of asyncio.iscoroutinefunction : " +
str(
asyncio.iscoroutinefunction(simple_generator)))
print("simple generator instance of inspect.iscoroutine : " + str(ins
pect.iscoroutine(sg)))
print("generator instance of inspect.iscoroutinefunction : " + str(
inspect.iscoroutinefunction(simple_generator)))
print("simple generator instance of inspect.isawaitable : " + str(ins
pect.isawaitable(sg)))
print("\n\n")
sg_aio_dec = simple_generator_with_asyncio_coroutine_dec()
print(sg_aio_dec)
print("simple generator instance of collections.abc.Iterable : " + st
r(isinstance(sg_aio_dec, Iterable)))
print("simple generator instance of collections.abc.Awaitable : " + s
tr(isinstance(sg_aio_dec, Awaitable)))
print("simple generator instance of types.Generator : " + str(isinsta
nce(sg_aio_dec, types.GeneratorType)))
print("simple generator instance of types.CoroutineType : " + str(isi
nstance(sg_aio_dec, types.CoroutineType)))
print("simple generator instance of asyncio.iscoroutine : " + str(asy
ncio.iscoroutine(sg_aio_dec)))
print("simple generator instance of asyncio.iscoroutinefunction : " +
str(
CONFIDENTIAL & RESTRICTED
asyncio.iscoroutinefunction(simple_generator_with_asyncio_corouti
ne_dec)))
print("simple generator instance of inspect.iscoroutine : " + str(ins
pect.iscoroutine(sg_aio_dec)))
print("generator instance of inspect.iscoroutinefunction : " + str(
inspect.iscoroutinefunction(simple_generator_with_asyncio_corouti
ne_dec)))
print("simple generator instance of inspect.isawaitable : " + str(ins
pect.isawaitable(sg_aio_dec)))
print("\n\n")
sg_types_dec = simple_generator_with_types_coroutine_dec()
print(sg_types_dec)
print("simple generator instance of collections.abc.Iterable : " + st
r(isinstance(sg_types_dec, Iterable)))
print("simple generator instance of collections.abc.Awaitable : " + s
tr(isinstance(sg_types_dec, Awaitable)))
print("simple generator instance of types.Generator : " + str(isinsta
nce(sg_types_dec, types.GeneratorType)))
print("simple generator instance of types.CoroutineType : " + str(isi
nstance(sg_types_dec, types.CoroutineType)))
print("simple generator instance of asyncio.iscoroutine : " + str(asy
ncio.iscoroutine(sg_types_dec)))
print("simple generator instance of asyncio.iscoroutinefunction : " +
str(
asyncio.iscoroutinefunction(simple_generator_with_types_coroutine
_dec)))
print("simple generator instance of inspect.iscoroutine : " + str(ins
pect.iscoroutine(sg_types_dec)))
print("generator instance of inspect.iscoroutinefunction : " + str(
inspect.iscoroutinefunction(simple_generator_with_types_coroutine
_dec)))
print("simple generator instance of inspect.isawaitable : " + str(ins
pect.isawaitable(sg_types_dec)))
print("\n\n")
gbc = generator_based_coroutine()
print(gbc)
print("generator instance of collections.abc.Iterable : " + str(isins
tance(gbc, Iterable)))
print("generator instance of collections.abc.Awaitable : " + str(isin
stance(gbc, Awaitable)))
print("generator instance of types.Generator : " + str(isinstance(gbc
, types.GeneratorType)))
CONFIDENTIAL & RESTRICTED
gbc_aio_dec = generator_based_coroutine_with_asyncio_coroutine_dec()
print(gbc_aio_dec)
print("generator instance of collections.abc.Iterable : " + str(isins
tance(gbc_aio_dec, Iterable)))
print("generator instance of collections.abc.Awaitable : " + str(isin
stance(gbc_aio_dec, Awaitable)))
print("generator instance of types.Generator : " + str(isinstance(gbc
_aio_dec, types.GeneratorType)))
print("generator instance of types.CoroutineType : " + str(isinstance
(gbc_aio_dec, types.CoroutineType)))
print("generator instance of asyncio.iscoroutine : " + str(asyncio.is
coroutine(gbc_aio_dec)))
print("generator instance of asyncio.iscoroutinefunction : " + str(
asyncio.iscoroutinefunction(generator_based_coroutine_with_asynci
o_coroutine_dec)))
print("generator instance of inspect.iscoroutine : " + str(inspect.is
coroutine(gbc_aio_dec)))
print("generator instance of inspect.iscoroutinefunction : " + str(
inspect.iscoroutinefunction(generator_based_coroutine_with_asynci
o_coroutine_dec)))
print("generator instance of inspect.isawaitable : " + str(inspect.is
awaitable(gbc_aio_dec)))
print("\n\n")
gbc_types_dec = generator_based_coroutine_with_types_coroutine_dec()
print(gbc_types_dec)
print("generator instance of collections.abc.Iterable : " + str(isins
tance(gbc_types_dec, Iterable)))
print("generator instance of collections.abc.Awaitable : " + str(isin
stance(gbc_types_dec, Awaitable)))
CONFIDENTIAL & RESTRICTED
nc = native_coroutine()
print("native coro instance of collections.abc.Iterable : " + str(isi
nstance(nc, Iterable)))
print("native coro instance of collections.abc.Awaitable : " + str(is
instance(nc, Awaitable)))
print("native coro instance of types.Generator : " + str(isinstance(n
c, types.GeneratorType)))
print("native coro instance of types.CoroutineType : " + str(isinstan
ce(nc, types.CoroutineType)))
print("native coro instance of asyncio.iscoroutine : " + str(asyncio.
iscoroutine(nc)))
print("native coro instance of asyncio.iscoroutinefunction : " + str(
asyncio.iscoroutinefunction(native_coroutine)))
print("native coro instance of inspect.iscoroutine : " + str(inspect.
iscoroutine(nc)))
print("generator instance of inspect.iscoroutinefunction : " + str(
inspect.iscoroutinefunction(native_coroutine)))
print("native coro instance of inspect.isawaitable : " + str(inspect.
isawaitable(nc)))
print(nc)
print("\n\n")
CONFIDENTIAL & RESTRICTED
def gen_based_coro():
yield from asyncio.sleep(10)
@asyncio.coroutine
def gen_based_coro():
yield from asyncio.sleep(10)
@asyncio.coroutine
Adding the @asyncio.coroutine decorator makes generator based
coroutines compatible with native coroutines. Without the
decorator it would not be possible to yield from a native coroutine
inside of a generator based coroutine. Consider the example below:
import asyncio
@asyncio.coroutine
def gen_based_coro():
yield from asyncio.sleep(1)
CONFIDENTIAL & RESTRICTED
if __name__ == "__main__":
gen = gen_based_coro()
next(gen)
import asyncio
@asyncio.coroutine
def gen_based_coro():
return 10
if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
Chaining Coroutines
def coro3(k):
yield (k + 3)
def coro2(j):
j = j * j
yield from coro3(j)
def coro1():
i = 0
while True:
yield from coro2(i)
i += 1
if __name__ == "__main__":
# The first 100 natural numbers evaluated for the following expression
# x^2 + 3
cr = coro1()
CONFIDENTIAL & RESTRICTED
for v in range(100):
print("f({0}) = {1}".format(v, next(cr)))
def coro3(k):
f = Future()
f.set_result(k + 3)
f.done()
return f
if __name__ == "__main__":
# The first 100 natural numbers evaluated for the following expression
# x^2 + 3
cr = coro1()
loop = asyncio.get_event_loop()
loop.run_until_complete(cr)
Event Loop
The event loop is a programming construct that waits for
events to happen and then dispatches them to an event
handler. An event can be a user clicking on a UI button or a
process initiating a file download. At the core of
asynchronous programming, sits the event loop. The concept
isn't novel to Python. In fact, many programming languages
enable asynchronous programming with event loops. In
Python, event loops run asynchronous tasks and callbacks,
perform network IO operations, run subprocesses and
delegate costly function calls to pool of threads.
One of the most common use cases you'll find in the wild is of
webservers implemented using asynchronous design. A webserver
waits for an HTTP request to arrive and returns the matching
resource. Folks familiar with JavaScript would recall NodeJS works
on the same principle. It is a webserver that runs an event loop to
receive web requests in a single thread. Contrast that to
webservers which create a new thread or worse fork a new
process, to handle each web request. In some benchmarks, the
asynchronous event loop based webservers outperformed
multithreaded ones, which may seem counterintuitive.
CONFIDENTIAL & RESTRICTED
With Python 3.7+ the preferred way to run the event loop is to use
the asyncio.run() method. The method is a blocking call till the
passed-in coroutine finishes. A sample program appears below:
if __name__ == "__main__":
asyncio.run(do_something_important())
If you are working with Python 3.5, then the asyncio.run() API isn't
available. In that case, we explicitly retrieve the event loop
using asyncio.new_event_loop() and run our desired coroutine
using run_until_complete() defined on the loop object.
We can immediately realize that our solution will need a count variable
to track the number of threads that have arrived at the barrier. If we
have n threads, then n-1 threads must wait for the nth thread to arrive.
This suggests we have the n-1 threads execute the wait method and
the nth thread wakes up all the asleep n-1 threads.
class Barrier(object):
def __init__(self, size):
self.barrier_size = size
self.reached_count = 0
self.cond = Condition()
def arrived(self):
self.cond.acquire()
self.reached_count += 1
if self.reached_count == self.barrier_size:
self.cond.notifyAll()
self.reached_count = 0
CONFIDENTIAL & RESTRICTED
else:
self.cond.wait()
self.cond.release()
def thread_process(sleep_for):
time.sleep(sleep_for)
print("Thread {0} reached the barrier".format(current_thread().getNam
e()))
barrier.arrived()
time.sleep(sleep_for)
print("Thread {0} reached the barrier".format(current_thread().getNam
e()))
barrier.arrived()
time.sleep(sleep_for)
print("Thread {0} reached the barrier".format(current_thread().getNam
e()))
barrier.arrived()
if __name__ == "__main__":
barrier = Barrier(3)
t1 = Thread(target=thread_process, args=(0,))
t2 = Thread(target=thread_process, args=(0.5,))
t3 = Thread(target=thread_process, args=(1.5,))
t1.start()
t2.start()
t3.start()
t1.join()
t2.join()
t3.join()
The above code has a subtle but very crucial bug! Can you
spot the bug and try to fix it before reading on?
The condition the while loop can check for is simply how many threads
have incremented the reached_count variable so far. A thread that
wakes up spuriously should go back to waiting if the reached_count is
less than the size of the barrier. We can check for this condition as
follows:
class Barrier(object):
1. def __init__(self, size):
2. self.barrier_size = size
3. self.reached_count = 0
4. self.released_count = self.barrier_size
5. self.cond = Condition()
6.
7. def arrived(self):
8.
9. self.cond.acquire()
10.
11. self.reached_count += 1
12.
13. if self.reached_count == self.barrier_size:
14. self.released_count = self.barrier_size
15.
16. else:
17. while self.reached_count < self.barrier_size:
18. self.cond.wait()
19.
20. self.released_count -= 1
21.
22. if self.released_count == 0:
23. self.reached_count = 0
24.
25. self.cond.notifyAll()
26. self.cond.release()
There is still a bug in the above code! Can you guess what it is?
Final Version
class Barrier(object):
def __init__(self, size):
self.barrier_size = size
self.reached_count = 0
self.released_count = self.barrier_size
self.cond = Condition()
def arrived(self):
CONFIDENTIAL & RESTRICTED
self.cond.acquire()
self.reached_count += 1
if self.reached_count == self.barrier_size:
self.released_count = self.barrier_size
else:
while self.reached_count < self.barrier_size:
self.cond.wait()
self.released_count -= 1
if self.released_count == 0:
self.reached_count = 0
def thread_process(sleep_for):
time.sleep(sleep_for)
print("Thread {0} reached the barrier".format(current_thread().getNam
e()), flush=True)
barrier.arrived()
time.sleep(sleep_for)
print("Thread {0} reached the barrier".format(current_thread().getNam
e()))
barrier.arrived()
time.sleep(sleep_for)
print("Thread {0} reached the barrier".format(current_thread().getNam
e()))
barrier.arrived()
if __name__ == "__main__":
barrier = Barrier(3)
t1 = Thread(target=thread_process, args=(0,))
CONFIDENTIAL & RESTRICTED
t2 = Thread(target=thread_process, args=(0.5,))
t3 = Thread(target=thread_process, args=(1.5,))
t1.start()
t2.start()
t3.start()
t1.join()
t2.join()
t3.join()
Imagine you have a bucket that gets filled with tokens at the rate of
1 token per second. The bucket can hold a maximum of N tokens.
Implement a thread-safe class that lets threads get a token when
one is available. If no token is available, then the token-requesting
threads should block.
One naive way to solve this problem is to have a busy thread that
continuously loops over the list of actions and executes them as
they become due. However, the challenge here is to design a
solution which doesn't involve a busy thread.
Consumer threads can come and add their desired actions in the
min-heap within the critical section. The caveat here is that the
execution thread will need to be woken up to recalculate the
minimum duration it would sleep for before an action is due for
execution. An action with an earlier due timestamp might have
been added while the executor thread was sleeping on a duration
calculated for an action due later than the one just added.
class DeferredCallbackExecutor():
def __init__(self):
self.actions = list()
CONFIDENTIAL & RESTRICTED
self.cond = Condition()
self.sleep = 0
self.cond.acquire()
heapq.heappush(self.actions, action)
self.cond.notify()
self.cond.release()
def start(self):
while True:
self.cond.acquire()
while len(self.actions) is 0:
self.cond.wait()
self.cond.wait(timeout=sleep_for)
action_to_execute_now = heapq.heappop(self.actions)
action_to_execute_now.action(*(action_to_execute_now,))
self.cond.release()
class DeferredAction(object):
def __init__(self, exec_secs_after, name, action):
self.exec_secs_after = exec_secs_after
self.action = action
self.name = name
def say_hi(action):
print("hi, I am {0} executed at {1} and required at {2}".format(a
ction.name, math.floor(time.time()),
math.
floor(action.execute_at)))
if __name__ == "__main__":
action1 = DeferredAction(3, ("A",), say_hi)
action2 = DeferredAction(2, ("B",), say_hi)
action3 = DeferredAction(1, ("C",), say_hi)
action4 = DeferredAction(7, ("D",), say_hi)
executor = DeferredCallbackExecutor()
t = Thread(target=executor.start, daemon=True)
t.start()
executor.add_action(action1)
executor.add_action(action2)
executor.add_action(action3)
executor.add_action(action4)
class BlockingQueue:
def enqueue(self):
pass
def dequeue(self):
pass
The complete code appears in the code widget below along with an
example.
from threading import Thread
from threading import Condition
from threading import current_thread
import time
import random
class BlockingQueue:
def dequeue(self):
CONFIDENTIAL & RESTRICTED
self.cond.acquire()
while self.curr_size == 0:
self.cond.wait()
item = self.q.pop(0)
self.curr_size -= 1
self.cond.notifyAll()
self.cond.release()
return item
self.cond.acquire()
while self.curr_size == self.max_size:
self.cond.wait()
self.q.append(item)
self.curr_size += 1
self.cond.notifyAll()
print("\ncurrent size of queue {0}".format(self.curr_size), flush
=True)
self.cond.release()
def consumer_thread(q):
while 1:
item = q.dequeue()
print("\n{0} consumed item {1}
".format(current_thread().getName(), item), flush=True)
time.sleep(random.randint(1, 3))
if __name__ == "__main__":
CONFIDENTIAL & RESTRICTED
blocking_q = BlockingQueue(5)
consumerThread1.start()
consumerThread2.start()
producerThread1.start()
producerThread2.start()
time.sleep(15)
print("Main thread exiting")
Follow Up Question
Non-Blocking Queue
We have seen the blocking version of a queue in the previous
question that blocks a producer or a consumer when the queue is
CONFIDENTIAL & RESTRICTED
First Cut
class NonBlockingQueue:
def dequeue(self):
with self.lock:
curr_size = len(self.q)
if curr_size != 0:
return self.q.pop(0)
CONFIDENTIAL & RESTRICTED
else:
return False
with self.lock:
curr_size = len(self.q)
if curr_size == self.max_size:
return False
else:
self.q.append(item)
return True
def consumer_thread(q):
while 1:
item = q.dequeue()
if item == False:
print("Consumer couldn't dequeue an item")
else:
print("\n{0} consumed item {1}
".format(current_thread().getName(), item), flush=True)
time.sleep(random.randint(1, 3))
def producer_thread(q):
item = 1
while 1:
result = q.enqueue(item)
if result is True:
print("\n {0} produced item
".format(current_thread().getName()), flush=True)
item += 1
if __name__ == "__main__":
no_block_q = NonBlockingQueue(5)
consumerThread1.start()
producerThread1.start()
time.sleep(15)
print("Main thread exiting")
Second Cut
class NonBlockingQueue:
def dequeue(self):
result = None
CONFIDENTIAL & RESTRICTED
future = None
with self.lock:
curr_size = len(self.q)
if curr_size != 0:
result = self.q.pop()
if len(self.q_waiting_puts) > 0:
self.q_waiting_puts.pop().set_result(True)
else:
future = Future()
self.q_waiting_gets.append(future)
future = None
with self.lock:
curr_size = len(self.q)
if curr_size == self.max_size:
future = Future()
self.q_waiting_puts.append(future)
else:
self.q.append(item)
if len(self.q_waiting_gets) != 0:
future_get = self.q_waiting_gets.pop()
future_get.set_result(True)
return future
def consumer_thread(q):
while 1:
item, future = q.dequeue()
if item is None:
print("Consumer received a future but we are ignoring it")
else:
CONFIDENTIAL & RESTRICTED
def producer_thread(q):
item = 1
while 1:
future = q.enqueue(item)
if future is not None:
while future.done() == False:
print("waiting for future to resolve")
time.sleep(0.1)
else:
item += 1
if __name__ == "__main__":
no_block_q = NonBlockingQueue(5)
consumerThread1.start()
producerThread1.start()
time.sleep(15)
print("Main thread exiting")
CONFIDENTIAL & RESTRICTED