One Python standard library per week

Keywords: Python Programming network less

Technology blog: https://github.com/yongxinz/tech-blog

At the same time, welcome to pay attention to my WeChat public number AlwaysBeta, more wonderful content waiting for you to come.

In fact, in Python, multithreading is not recommended. Unless you explicitly do not support the scenario of using multiple processes, you can use multiple processes if you can. The purpose of this article can be compared with the multi process article. There are many similarities. After reading this article, you may have a better understanding of concurrent programming.

GIL

Python's multithreaded code doesn't take advantage of multi-core, but is handled by the famous global interpretation lock (GIL). If it is a computational task, using multi-threaded Gil will make multi-threaded slow. Let's take an example of calculating Fibonacci series:

# coding=utf-8
import time
import threading


def profile(func):
    def wrapper(*args, **kwargs):
        import time
        start = time.time()
        func(*args, **kwargs)
        end   = time.time()
        print 'COST: {}'.format(end - start)
    return wrapper


def fib(n):
    if n<= 2:
        return 1
    return fib(n-1) + fib(n-2)


@profile
def nothread():
    fib(35)
    fib(35)


@profile
def hasthread():
    for i in range(2):
        t = threading.Thread(target=fib, args=(35,))
        t.start()
    main_thread = threading.currentThread()
    for t in threading.enumerate():
        if t is main_thread:
            continue
        t.join()

nothread()
hasthread()

# output
# COST: 5.05716490746
# COST: 6.75599503517

What do you think of the result of the operation? It's better not to multithread!

GIL is required, which is the problem of Python design: the Python interpreter is non thread safe. This means that there is a global mandatory lock when trying to access Python objects safely from within a thread. At any time, only a single thread can get Python objects or C API s. Every 100 bytes of Python instruction interpreter reacquires the lock, which (potentially) blocks I/O operations. Because of locks, CPU intensive code will not get performance improvement when using thread library (but when it uses the multi process library introduced later, performance can be improved).

Is it because of the existence of GIL that multithreading library is a "chicken rib"? Of course not. In fact, we usually contact many programs related to network communication or data input / output, such as web crawler, text processing, etc. At this time, due to the limitations of network conditions and I/O performance, the Python interpreter will wait for the function call to read and write data to return. At this time, the multithreaded library can be used to improve the concurrency efficiency.

Thread object

Let's start with a very simple way to instantiate the objective function directly using Thread and then call start() to execute it.

import threading


def worker():
    """thread worker function"""
    print('Worker')


threads = []
for i in range(5):
    t = threading.Thread(target=worker)
    threads.append(t)
    t.start()
    
# output
# Worker
# Worker
# Worker
# Worker
# Worker

When generating a thread, parameters can be passed to the thread. Any type of parameters can be used. The following example only passes one number:

import threading


def worker(num):
    """thread worker function"""
    print('Worker: %s' % num)


threads = []
for i in range(5):
    t = threading.Thread(target=worker, args=(i,))
    threads.append(t)
    t.start()
    
# output
# Worker: 0
# Worker: 1
# Worker: 2
# Worker: 3
# Worker: 4

There is also a way to create a Thread. By inheriting the Thread class and then overriding the run() method, the code is as follows:

import threading
import logging


class MyThread(threading.Thread):

    def run(self):
        logging.debug('running')


logging.basicConfig(
    level=logging.DEBUG,
    format='(%(threadName)-10s) %(message)s',
)

for i in range(5):
    t = MyThread()
    t.start()
    
# output
# (Thread-1  ) running
# (Thread-2  ) running
# (Thread-3  ) running
# (Thread-4  ) running
# (Thread-5  ) running

Because args and kwargs passed to the Thread constructor are saved as private variables with a prefix of "UU", they cannot be accessed in the child Thread, so in the custom Thread class, the constructor should be re constructed.

import threading
import logging


class MyThreadWithArgs(threading.Thread):

    def __init__(self, group=None, target=None, name=None,
                 args=(), kwargs=None, *, daemon=None):
        super().__init__(group=group, target=target, name=name,
                         daemon=daemon)
        self.args = args
        self.kwargs = kwargs

    def run(self):
        logging.debug('running with %s and %s',
                      self.args, self.kwargs)


logging.basicConfig(
    level=logging.DEBUG,
    format='(%(threadName)-10s) %(message)s',
)

for i in range(5):
    t = MyThreadWithArgs(args=(i,), kwargs={'a': 'A', 'b': 'B'})
    t.start()
    
# output
# (Thread-1  ) running with (0,) and {'b': 'B', 'a': 'A'}
# (Thread-2  ) running with (1,) and {'b': 'B', 'a': 'A'}
# (Thread-3  ) running with (2,) and {'b': 'B', 'a': 'A'}
# (Thread-4  ) running with (3,) and {'b': 'B', 'a': 'A'}
# (Thread-5  ) running with (4,) and {'b': 'B', 'a': 'A'}

Determine current thread

Each Thread has a name, which can be used by default or specified when creating a Thread.

import threading
import time


def worker():
    print(threading.current_thread().getName(), 'Starting')
    time.sleep(0.2)
    print(threading.current_thread().getName(), 'Exiting')


def my_service():
    print(threading.current_thread().getName(), 'Starting')
    time.sleep(0.3)
    print(threading.current_thread().getName(), 'Exiting')


t = threading.Thread(name='my_service', target=my_service)
w = threading.Thread(name='worker', target=worker)
w2 = threading.Thread(target=worker)  # use default name

w.start()
w2.start()
t.start()

# output
# worker Starting
# Thread-1 Starting
# my_service Starting
# worker Exiting
# Thread-1 Exiting
# my_service Exiting

Daemon thread

By default, the main program does not exit until all child threads exit. Sometimes it's useful to start a background thread without preventing the main program from exiting, such as generating a "heartbeat" task for a monitoring tool.

To mark a thread as a daemons, pass the daemon=True or call set ˊ (true) at creation time. By default, a thread is not a daemons.

import threading
import time
import logging


def daemon():
    logging.debug('Starting')
    time.sleep(0.2)
    logging.debug('Exiting')


def non_daemon():
    logging.debug('Starting')
    logging.debug('Exiting')


logging.basicConfig(
    level=logging.DEBUG,
    format='(%(threadName)-10s) %(message)s',
)

d = threading.Thread(name='daemon', target=daemon, daemon=True)

t = threading.Thread(name='non-daemon', target=non_daemon)

d.start()
t.start()

# output
# (daemon    ) Starting
# (non-daemon) Starting
# (non-daemon) Exiting

The output does not contain the Exiting of the daemons because other threads, including the main program, have exited before the daemons wake up from sleep().

If you want to wait for the daemons to finish their work, you can use the join() method.

import threading
import time
import logging


def daemon():
    logging.debug('Starting')
    time.sleep(0.2)
    logging.debug('Exiting')


def non_daemon():
    logging.debug('Starting')
    logging.debug('Exiting')


logging.basicConfig(
    level=logging.DEBUG,
    format='(%(threadName)-10s) %(message)s',
)

d = threading.Thread(name='daemon', target=daemon, daemon=True)

t = threading.Thread(name='non-daemon', target=non_daemon)

d.start()
t.start()

d.join()
t.join()

# output
# (daemon    ) Starting
# (non-daemon) Starting
# (non-daemon) Exiting
# (daemon    ) Exiting

The output information already includes the Exiting of the daemons.

By default, join() is blocked indefinitely. You can also pass a floating-point value that represents the number of seconds to wait for the thread to become inactive. If the thread does not complete within the timeout period, join() returns anyway.

import threading
import time
import logging


def daemon():
    logging.debug('Starting')
    time.sleep(0.2)
    logging.debug('Exiting')


def non_daemon():
    logging.debug('Starting')
    logging.debug('Exiting')


logging.basicConfig(
    level=logging.DEBUG,
    format='(%(threadName)-10s) %(message)s',
)

d = threading.Thread(name='daemon', target=daemon, daemon=True)

t = threading.Thread(name='non-daemon', target=non_daemon)

d.start()
t.start()

d.join(0.1)
print('d.isAlive()', d.isAlive())
t.join()

# output
# (daemon    ) Starting
# (non-daemon) Starting
# (non-daemon) Exiting
# d.isAlive() True

Because the timeout of delivery is less than the time when the daemons thread sleeps, the thread is still "active" after join() returns.

Enumerate all threads

The enumerate() method returns a list of active Thread instances. Because the list includes the current Thread, and because joining the current Thread introduces a deadlock condition, it must be skipped.

import random
import threading
import time
import logging


def worker():
    """thread worker function"""
    pause = random.randint(1, 5) / 10
    logging.debug('sleeping %0.2f', pause)
    time.sleep(pause)
    logging.debug('ending')


logging.basicConfig(
    level=logging.DEBUG,
    format='(%(threadName)-10s) %(message)s',
)

for i in range(3):
    t = threading.Thread(target=worker, daemon=True)
    t.start()

main_thread = threading.main_thread()
for t in threading.enumerate():
    if t is main_thread:
        continue
    logging.debug('joining %s', t.getName())
    t.join()
    
# output
# (Thread-1  ) sleeping 0.20
# (Thread-2  ) sleeping 0.30
# (Thread-3  ) sleeping 0.40
# (MainThread) joining Thread-1
# (Thread-1  ) ending
# (MainThread) joining Thread-3
# (Thread-2  ) ending
# (Thread-3  ) ending
# (MainThread) joining Thread-2

timer thread

Timer() starts working after the delay time and can be cancelled at any point in the delay time period.

import threading
import time
import logging


def delayed():
    logging.debug('worker running')


logging.basicConfig(
    level=logging.DEBUG,
    format='(%(threadName)-10s) %(message)s',
)

t1 = threading.Timer(0.3, delayed)
t1.setName('t1')
t2 = threading.Timer(0.3, delayed)
t2.setName('t2')

logging.debug('starting timers')
t1.start()
t2.start()

logging.debug('waiting before canceling %s', t2.getName())
time.sleep(0.2)
logging.debug('canceling %s', t2.getName())
t2.cancel()
logging.debug('done')

# output
# (MainThread) starting timers
# (MainThread) waiting before canceling t2
# (MainThread) canceling t2
# (MainThread) done
# (t1        ) worker running

The second timer in this example does not run, and the first timer appears to run after the main program completes. Because it is not a daemonic thread, it is implicitly called when the main thread completes.

Synchronization mechanism

Semaphore

In multithreaded programming, in order to prevent different threads from modifying a common resource (such as all variables) at the same time, the number of simultaneous accesses (usually 1) is required. Semaphore synchronization is based on the internal counter. For each call to acquire(), the counter is decreased by 1; for each call to release(), the counter is increased by 1. When the counter is 0, the acquire() call is blocked.

import logging
import random
import threading
import time


class ActivePool:

    def __init__(self):
        super(ActivePool, self).__init__()
        self.active = []
        self.lock = threading.Lock()

    def makeActive(self, name):
        with self.lock:
            self.active.append(name)
            logging.debug('Running: %s', self.active)

    def makeInactive(self, name):
        with self.lock:
            self.active.remove(name)
            logging.debug('Running: %s', self.active)


def worker(s, pool):
    logging.debug('Waiting to join the pool')
    with s:
        name = threading.current_thread().getName()
        pool.makeActive(name)
        time.sleep(0.1)
        pool.makeInactive(name)


logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s (%(threadName)-2s) %(message)s',
)

pool = ActivePool()
s = threading.Semaphore(2)
for i in range(4):
    t = threading.Thread(
        target=worker,
        name=str(i),
        args=(s, pool),
    )
    t.start()
    
# output
# 2016-07-10 10:45:29,398 (0 ) Waiting to join the pool
# 2016-07-10 10:45:29,398 (0 ) Running: ['0']
# 2016-07-10 10:45:29,399 (1 ) Waiting to join the pool
# 2016-07-10 10:45:29,399 (1 ) Running: ['0', '1']
# 2016-07-10 10:45:29,399 (2 ) Waiting to join the pool
# 2016-07-10 10:45:29,399 (3 ) Waiting to join the pool
# 2016-07-10 10:45:29,501 (1 ) Running: ['0']
# 2016-07-10 10:45:29,501 (0 ) Running: []
# 2016-07-10 10:45:29,502 (3 ) Running: ['3']
# 2016-07-10 10:45:29,502 (2 ) Running: ['3', '2']
# 2016-07-10 10:45:29,607 (3 ) Running: ['2']
# 2016-07-10 10:45:29,608 (2 ) Running: []

In this case, the ActivePool() class is just to show that at most two threads are running at the same time.

Lock

Lock can also be called a mutex, which is equivalent to a semaphore of 1. Let's take a look at an example without lock:

import time
from threading import Thread

value = 0


def getlock():
    global value
    new = value + 1
    time.sleep(0.001)  # Using sleep to give threads a chance to switch
    value = new


threads = []

for i in range(100):
    t = Thread(target=getlock)
    t.start()
    threads.append(t)

for t in threads:
    t.join()

print value	# 16

Without lock, the result will be far less than 100. Let's add the mutex:

import time
from threading import Thread, Lock

value = 0
lock = Lock()


def getlock():
    global value
    with lock:
        new = value + 1
        time.sleep(0.001)
        value = new

threads = []

for i in range(100):
    t = Thread(target=getlock)
    t.start()
    threads.append(t)

for t in threads:
    t.join()

print value	# 100

RLock

acquire() can be called multiple times by the same thread without being blocked. Note, however, that release() requires the same number of calls as acquire () to release the lock.

Let's first look at the use of Lock:

import threading

lock = threading.Lock()

print('First try :', lock.acquire())
print('Second try:', lock.acquire(0))

# output
# First try : True
# Second try: False

In this case, the second call gives acquire() a zero timeout to prevent it from blocking because the first call has been locked.

Let's look at RLock as an alternative.

import threading

lock = threading.RLock()

print('First try :', lock.acquire())
print('Second try:', lock.acquire(0))

# output
# First try : True
# Second try: True

Condition

One thread waits for a particular condition, while another signals that a particular condition is met. The best example is the "producer / consumer" model:

import time
import threading

def consumer(cond):
    t = threading.currentThread()
    with cond:
        cond.wait()  # The wait() method creates a lock named waiter and sets the state of the lock to locked. This waiter lock is used for communication between threads
        print '{}: Resource is available to consumer'.format(t.name)


def producer(cond):
    t = threading.currentThread()
    with cond:
        print '{}: Making resource available'.format(t.name)
        cond.notifyAll()  # Release waiter lock to wake up consumers


condition = threading.Condition()

c1 = threading.Thread(name='c1', target=consumer, args=(condition,))
c2 = threading.Thread(name='c2', target=consumer, args=(condition,))
p = threading.Thread(name='p', target=producer, args=(condition,))

c1.start()
time.sleep(1)
c2.start()
time.sleep(1)
p.start()

# output
# p: Making resource available
# c2: Resource is available to consumer
# c1: Resource is available to consumer

You can see that after the producer sends the notification, the consumer receives it.

Event

One thread sends / delivers the event, and the other waits for the event to trigger. We also use the example of "producer / consumer" model:

# coding=utf-8
import time
import threading
from random import randint


TIMEOUT = 2

def consumer(event, l):
    t = threading.currentThread()
    while 1:
        event_is_set = event.wait(TIMEOUT)
        if event_is_set:
            try:
                integer = l.pop()
                print '{} popped from list by {}'.format(integer, t.name)
                event.clear()  # Reset event status
            except IndexError:  # In order to make it fault-tolerant at first startup
                pass


def producer(event, l):
    t = threading.currentThread()
    while 1:
        integer = randint(10, 100)
        l.append(integer)
        print '{} appended to list by {}'.format(integer, t.name)
        event.set()	 # Set events
        time.sleep(1)


event = threading.Event()
l = []

threads = []

for name in ('consumer1', 'consumer2'):
    t = threading.Thread(name=name, target=consumer, args=(event, l))
    t.start()
    threads.append(t)

p = threading.Thread(name='producer1', target=producer, args=(event, l))
p.start()
threads.append(p)

for t in threads:
    t.join()
    
# output
# 77 appended to list by producer1
# 77 popped from list by consumer1
# 46 appended to list by producer1
# 46 popped from list by consumer2
# 43 appended to list by producer1
# 43 popped from list by consumer2
# 37 appended to list by producer1
# 37 popped from list by consumer2
# 33 appended to list by producer1
# 33 popped from list by consumer2
# 57 appended to list by producer1
# 57 popped from list by consumer1

You can see that the event was received and processed by two consumers on average. If we use the wait() method, the thread will wait for us to set the event, which also helps to ensure the completion of the task.

Queue

Queues are most commonly used in concurrent development. We use the "producer / consumer" model to understand that the producer puts the produced "message" into the queue, and the consumer executes the corresponding message from the queue.

You are mainly concerned about the following four methods:

  1. put: adds an item to the queue.
  2. get: removes and returns an item from the queue.
  3. Task? Done: called when a task is completed.
  4. join: block until all items are processed.
# coding=utf-8
import time
import threading
from random import random
from Queue import Queue

q = Queue()


def double(n):
    return n * 2


def producer():
    while 1:
        wt = random()
        time.sleep(wt)
        q.put((double, wt))


def consumer():
    while 1:
        task, arg = q.get()
        print arg, task(arg)
        q.task_done()


for target in(producer, consumer):
    t = threading.Thread(target=target)
    t.start()

This is the simplest queue architecture.

The Queue module also comes with two special queues: PriorityQueue (with priority) and LifoQueue (last in, first out). Here we show the usage of thread safe priority Queue. The format of the data that PriorityQueue requires us to put is (priority u number, data). Let's take a look at the following example:

import time
import threading
from random import randint
from Queue import PriorityQueue


q = PriorityQueue()


def double(n):
    return n * 2


def producer():
    count = 0
    while 1:
        if count > 5:
            break
        pri = randint(0, 100)
        print 'put :{}'.format(pri)
        q.put((pri, double, pri))  # (priority, func, args)
        count += 1


def consumer():
    while 1:
        if q.empty():
            break
        pri, task, arg = q.get()
        print '[PRI:{}] {} * 2 = {}'.format(pri, arg, task(arg))
        q.task_done()
        time.sleep(0.1)


t = threading.Thread(target=producer)
t.start()
time.sleep(1)
t = threading.Thread(target=consumer)
t.start()

# output
# put :84
# put :86
# put :16
# put :93
# put :14
# put :93
# [PRI:14] 14 * 2 = 28
# 
# [PRI:16] 16 * 2 = 32
# [PRI:84] 84 * 2 = 168
# [PRI:86] 86 * 2 = 172
# [PRI:93] 93 * 2 = 186
# [PRI:93] 93 * 2 = 186

In order to save space, only 5 random results are generated. It can be seen that the number in put is random, but in get, it is first obtained from higher priority (small number means high priority).

Thread pool

In object-oriented development, it is known that it takes time to create and destroy objects, because to create an object, you need to obtain memory resources or other resources. Creating and destroying threads without restraint is a great waste. Can we reuse the thread that has completed the task without destroying it? It's like putting these threads into a pool. On the one hand, we can control the number of threads working at the same time, and on the other hand, we can avoid the cost of creation and destruction.

The thread pool is actually embodied in the standard library, but it is not mentioned in the official articles:

In : from multiprocessing.pool import ThreadPool
In : pool = ThreadPool(5)
In : pool.map(lambda x: x**2, range(5))
Out: [0, 1, 4, 9, 16]

Of course, we can also achieve one by ourselves:

# coding=utf-8
import time
import threading
from random import random
from Queue import Queue


def double(n):
    return n * 2


class Worker(threading.Thread):
    def __init__(self, queue):
        super(Worker, self).__init__()
        self._q = queue
        self.daemon = True
        self.start()
    def run(self):
        while 1:
            f, args, kwargs = self._q.get()
            try:
                print 'USE: {}'.format(self.name)  # Thread name
                print f(*args, **kwargs)
            except Exception as e:
                print e
            self._q.task_done()


class ThreadPool(object):
    def __init__(self, num_t=5):
        self._q = Queue(num_t)
        # Create Worker Thread
        for _ in range(num_t):
            Worker(self._q)
    def add_task(self, f, *args, **kwargs):
        self._q.put((f, args, kwargs))
    def wait_complete(self):
        self._q.join()


pool = ThreadPool()
for _ in range(8):
    wt = random()
    pool.add_task(double, wt)
    time.sleep(wt)
pool.wait_complete()

# output
# USE: Thread-1
# 1.58762376489
# USE: Thread-2
# 0.0652918738849
# USE: Thread-3
# 0.997407997138
# USE: Thread-4
# 1.69333900685
# USE: Thread-5
# 0.726900613676
# USE: Thread-1
# 1.69110052253
# USE: Thread-2
# 1.89039743989
# USE: Thread-3
# 0.96281118122

The thread pool will guarantee 5 threads to work at the same time, but we have 8 tasks to be completed, and we can see that threads are recycled in order.

Related documents:

https://pymotw.com/3/threading/index.html

http://www.dongwm.com/archives/%E4%BD%BF%E7%94%A8Python%E8%BF%9B%E8%A1%8C%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B-%E7%BA%BF%E7%A8%8B%E7%AF%87/

26 original articles published, 30 praised, 40000 visitors+
Private letter follow

Posted by Franko126 on Tue, 21 Jan 2020 03:20:23 -0800