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,378