Thinking concurrently
By Andrea Stagi, deveLover @ Nephila
Threads
Thread_1
PPID: 1
PID: 2
Thread_2
PPID: 1
PID: 3
Thread_3
PPID: 1
PID: 4
My process
PPID: 0
PID: 1
Parallel Execution
https://github.com/DjangoBeer/concurrently/tree/master/filler
import time
def fill(array, position):
array[position] += 1
array_to_fill = [0, 0, 0, 0, 0, 0, 0, 0]
while True:
for i in range(len(array_to_fill)):
fill(array_to_fill, i)
print array_to_fill
time.sleep(1)
Import thread, import threading
import thread
import time
def fill(array, position):
while True:
array[position] += 1
array_to_fill = [0, 0, 0, 0, 0, 0, 0, 0]
for i in range(len(array_to_fill)):
thread.start_new_thread(fill, (array_to_fill, i))
while True:
print array_to_fill
time.sleep(1)
Concurrent Execution
The result is correct... 99.9% of the time
READ - my_var
EXEC - my_var + 1
STORE - result in my_var
my_var += 1
Low level
Critical sections
var tickets = 10;
function buyTicket() {
if (tickets > 0) {
tickets--;
console.log("Purchased!");
} else {
console.log("No tickets left!");
}
console.log("I'm done!");
}
function buyTicket() {
// ...
}
LOCK();
buyTicket();
UNLOCK();
PARALLEL "Capture the flag"
https://github.com/DjangoBeer/concurrently/tree/master/capturetheflag
class Player(Thread):
def __init__(self, name, flag):
self._name = name
self._flag = flag
Thread.__init__(self)
def run(self):
while not self._flag.available:
pass
if not self._flag.captured:
time.sleep(randint(0,3))
self._flag.set_captured()
print ('{0} WIN!'.format(self._name))
else:
print ('{0} IS A LOSER!'.format(self._name))
flag = Flag()
player_1 = Player('A', flag)
player_2 = Player('B', flag)
player_3 = Player('C', flag)
player_1.start()
player_2.start()
player_3.start()
flag.set_available()
$ python capturetheflag/parallel_ctf.py
C WIN!
A IS A LOSER!
B WIN!
$ python capturetheflag/parallel_ctf.py
A WIN!
C IS A LOSER!
B WIN!
$ python capturetheflag/parallel_ctf.py
C WIN!
A IS A LOSER!
B IS A LOSER!
Concurrent "Capture the flag"
from threading import Thread, Lock
lock = Lock()
class Player(Thread):
def run(self):
while not self._flag.available:
pass
lock.acquire()
if not self._flag.captured:
time.sleep(randint(0,3))
self._flag.set_captured()
print ('{0} WIN!'.format(self._name))
else:
print ('{0} IS A LOSER!'.format(self._name))
lock.release()
$ python capturetheflag/concurrent_ctf.py
B WIN!
A IS A LOSER!
C IS A LOSER!
$ python capturetheflag/concurrent_ctf.py
B WIN!
C IS A LOSER!
A IS A LOSER!
$ python capturetheflag/concurrent_ctf.py
C WIN!
B IS A LOSER!
A IS A LOSER!
WaSHER/DRyers
Producer/Consumers
https://github.com/DjangoBeer/concurrently/tree/master/dishes
emanuela = DishWasher('Emanuela')
andrea = DishDryer('Andrea', emanuela)
ambra = DishDryer('Ambra', emanuela)
emanuela.start()
andrea.start()
ambra.start()
from threading import condition
The DISHwasher IN ACTION
class DishWasher(Thread):
def __init__(self, name):
self._name = name
self._ended = False
Thread.__init__(self)
def run(self):
global queue
for i in range(10):
condition.acquire()
num = random.choice(range(5))
queue.append(num)
print ("{0} says: Washed {1}".format(self._name, num))
condition.notifyAll()
condition.release()
time.sleep(random.random())
condition.acquire()
self._ended = True
condition.notifyAll()
condition.release()
global queue
for i in range(10):
condition.acquire()
num = random.choice(range(5))
queue.append(num)
# ... PRINT STUFF ...
condition.notifyAll()
condition.release()
time.sleep(random.random())
condition.acquire()
self._ended = True
condition.notifyAll()
condition.release()
End stuff
notify all?
The DISHDRYER in action
class DishDryer(Thread):
def __init__(self, name, washer):
self._name = name
self._washer = washer
Thread.__init__(self)
def run(self):
global queue
while True:
condition.acquire()
if not queue:
print (
"{0} says: Nothing to dry, I'm waiting".format(self._name)
)
if not self._washer.ended:
condition.wait()
if self._washer.ended:
condition.release()
print "{0} says: Bye!".format(self._name)
break
print (
"{0} says: Looks like there's something to dry!".format(self._name)
)
if queue:
num = queue.pop(0)
print ("{0} says: {1} dryed!".format(self._name, num))
condition.release()
time.sleep(random.random())
global queue
while True:
condition.acquire()
if not queue:
print (
"{0} says: Nothing to dry, I'm waiting".format(self._name)
)
if not self._washer.ended:
condition.wait()
if self._washer.ended:
condition.release()
print "{0} says: Bye!".format(self._name)
break
print (
"{0} says: Looks like there's something to dry!".format(self._name)
)
if queue:
num = queue.pop(0)
print ("{0} says: {1} dryed!".format(self._name, num))
condition.release()
time.sleep(random.random())
GIL
The global interpreter Lock
IT's not a LAnguage feature
Some Python's implementations have a GIL, some don't
The problem of GIL
def count(n):
while n > 0:
n -= 1
count(100000000)
count(100000000)
Sequential : ~25s
from threading import Thread
def count(n):
while n > 0:
n -= 1
t1 = Thread(target=count,args=(100000000,))
t1.start()
t2 = Thread(target=count,args=(100000000,))
t2.start()
t1.join(); t2.join()
Threaded : ~45s (~2X slower!)
UHM...WHY A GIL?
Easier to implement than fine-grained locks
Easy integration of C/C++ Extensions (USUALLY NOT THREAD SAFE)...
...That can disable GIL! (E.G. NumPY)
Py_BEGIN_ALLOW_THREADS
... Do stuff ...
Py_END_ALLOW_THREADS
GIL ON I/O BOUND THREADS
GIL ON CPU BOUND THREADS
/* Python/ceval.c */
//...
if (--_Py_Ticker < 0) {
// ... Reset ticks ...
_Py_Ticker = _Py_CheckInterval;
//...
if (things_to_do) {
if (Py_MakePendingCalls() < 0) {
//...
}
}
if (interpreter_lock) {
/* Give another thread a chance */
PyThread_release_lock(interpreter_lock);
/* Other threads may run now */
PyThread_acquire_lock(interpreter_lock, 1);
}
}
RELEase/ACquire GIL On Multiprocessor Arch.
New GIL (Python 3)
No MORE TICKS
static volatile int gil_drop_request = 0;
Voluntary GIL RELEASE
Forced GIL RELEASE (w/ack)
Twitter: @4Stagi
github.com/astagi
Thinking concurrently
By Andrea Stagi
Thinking concurrently
- 3,411