Simple Guide to Python Threading

July 29, 2019

Why use Threading

  • Use threading for IO bound task, or consider Asyncio.
  • Not suitable for CPU bound task due to GIL and utilizing single core only. Consider Multiprocessing for CPU bound task.
  • Can use shared variable for communication between threads, probably require a lock or thread-safe queue.
  • Very hard to write and maintain correctness of code: think again before attempting a complex solution with threading.

NOTE: Refer Python Threading vs Multiprocessing vs Asyncio.

Launch a thread.

import threading

def run():
    print('run')

threading.Thread(target=run).start()

Pass named argument into thread

def run(name):
    print(f"run: {name}")

threading.Thread(target=run).start(kwargs={'name': 'test'})

Deamon thread (infinite loop)

import threading
import time

is_running = True

def run():
    while is_running:
        print('.', end='', flush=True)
        time.sleep(1)


t = threading.Thread(target=run)
t.daemon = True
t.start()
del t

# stop daemon thread after 10s
time.sleep(10)
is_running = False

Shared variable

Atomic operation should be thread-safe

import time
import random
import threading

current_time = 0
is_loop = True

def run():
    global current_time
    time.sleep(random.random() * 0.1)
    current_time = time.time()

def print_time():
    last_time = 0

    count = 0
    while is_loop:
        if current_time != last_time:
            count += 1
            print(f"{count}={current_time}")
            last_time = current_time
    print('print_time end: ')

for i in range(10):
    threading.Thread(target=run).start()

t = threading.Thread(target=print_time)
t.daemon = True
t.start()
del t

time.sleep(2)
print(f'stop print_time')
is_loop = False

As you have notice above, print_time access to current_time is not truely atomic. We are doing checking (current_time != last_time) followed by printing (print(f"{count}={current_time}")) where the value could have changed in between these operation.

A truly atomic print_time access to current_time would be:

def print_time():
    while True:
        print(f"{current_time}")

NOTE: It is still possible to miss out printing one or two changes to current_time, as current_time could changed twice before print_time is executed.

Lock

  • Acquire a lock just before assign the value (prevent other thread to assign a new value)
  • Release the lock after printing the value (allow other thread to assign a new value)
import time
import random
import threading

current_time = 0
current_time_lock = threading.Lock()
is_loop = True

def run():
    global current_time
    time.sleep(random.random() * 0.1)

    current_time_lock.acquire()
    current_time = time.time()

def print_time():
    last_time = 0

    count = 0
    while is_loop:
        if current_time != last_time:
            count += 1
            print(f"{count}={current_time}")
            last_time = current_time
            current_time_lock.release()
    print('print_time end: ')

for i in range(10):
    threading.Thread(target=run).start()

t = threading.Thread(target=print_time)
t.daemon = True
t.start()
del t

time.sleep(2)
print(f'stop print_time')
is_loop = False

Queue

Python queue is thread-safe.

import time
import random
import threading
import queue

current_time_queue = queue.Queue()
is_loop = True

def run():
    time.sleep(random.random() * 0.1)

    current_time = time.time()
    current_time_queue.put(current_time)

def print_time():
    count = 0
    while is_loop:
        current_time = current_time_queue.get()
        if current_time != None:
            count += 1
            print(f"{count}={current_time}")

    print('print_time end: ')

for i in range(10):
    threading.Thread(target=run).start()

t = threading.Thread(target=print_time)
t.daemon = True
t.start()
del t

time.sleep(2)
print(f'stop print_time')
is_loop = False
This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.