并发-线程 threading in Python, 2022-07-08

(2022.07.08 Fri)

GIL

python中的线程由于历史原因,即使在多核cpu的情况下并不能达真正的并行。这个原因就是全局解释器锁GIL(global interpreter lock),准确的说GIL不是python的特性,而是cpython引入的一个概念。cpython解释器在解析多线程时,会上GIL锁,保证同一时刻只有一个线程获取CPU使用权。

为什么需要GIL python中一切都是对象,Cpython中对象的回收,是通过对象的引用计数来判断,当对象的引用计数为0时,就会进行垃圾回收,自动释放内存。但是如果多线程的情况,引用计数就变成了一个共享的变量 Cpython是当下最流行的Python的解释器,使用引用计数来管理内存,在Python中,一切都是对象,引用计数就是指向对象的指针数,当这个数字变成0,则会进行垃圾回收,自动释放内存。但是问题是Cpython是线程不安全的。
考虑下如果有两个线程A和B同时引用一个对象obj,这个时候obj的引用计数为2;A打算撤销对obj的引用,完成第一步时引用计数减去1时,这时发生了线程切换,A挂起等待,还没执行销毁对象操作。B进入运行状态,这个时候B也对obj撤销引用,并完成引用计数减1,销毁对象,这个时候obj的引用数为0,释放内存。如果此时A重新唤醒,要继续销毁对象,可是这个时候已经没有对象了。所以为了保证不出现数据污染,才引入GIL。

每个线程使用前都会去获取GIL权限,使用完释放GIL权限。释放线程的时机由python的另一个机制check_interval来决定。

在多核cpu时,因为需要获取和释放GIL锁,会存在性能上额外的损耗。特别是由于调度控制的原因,比如一个线程释放了锁,调度接着又分配cpu资源给同一个线程,该线程发起申请时,又重新获得GIL,而其他线程实际上都在等待,白白浪费了申请和释放锁的操作耗时。

python中的线程比较适合I/O密集型的操作(磁盘IO或者网络IO)。


GIL illustration

线程和进程的实现

线程

import os  
import time  
import sys  
from concurrent import futures  
def to_do(info):    
    for i in range(100000000):  
        pass  
    return info[0]  
MAX_WORKERS = 10  
param_list = []  
for i in range(5):  
    param_list.append(('text%s' % i, 'info%s' % i))  
workers = min(MAX_WORKERS, len(param_list))  
# with 默认会等所有任务都完成才返回,所以这里会阻塞  
with futures.ThreadPoolExecutor(workers) as executor:  
    results = executor.map(to_do, sorted(param_list))  
# 打印所有  
for result in results:  
    print(result)  
# 非阻塞的方式,适合不需要返回结果的情况  
workers = min(MAX_WORKERS, len(param_list))  
executor = futures.ThreadPoolExecutor(workers)  
results = []  
for idx, param in enumerate(param_list):  
    result = executor.submit(to_do, param)  
    results.append(result)  
    print('result %s' % idx)  
# 手动等待所有任务完成  
executor.shutdown()  
print('='*10)  
for result in results:  
    print(result.result()) 

进程

import os  
import time  
import sys  
from concurrent import futures  
def to_do(info):    
    for i in range(10000000):  
        pass  
    return info[0]  
start_time = time.time()  
MAX_WORKERS = 10  
param_list = []  
for i in range(5):  
    param_list.append(('text%s' % i, 'info%s' % i))  
workers = min(MAX_WORKERS, len(param_list))  
# with 默认会等所有任务都完成才返回,所以这里会阻塞  
with futures.ProcessPoolExecutor(workers) as executor:  
    results = executor.map(to_do, sorted(param_list))    
 # 打印所有  
for result in results:  
    print(result)  
print(time.time()-start_time)  
# 耗时0.3704512119293213s, 而线程版本需要14.935384511947632s 

(2022.09.20 Tues)

Daemon 守护进程

在计算机科学中,Daemon是运行于后台的进程(process)。

在Python的threading中,Daemon有着更加特别的意义。Python中的daemon线程会在程序退出是立刻关闭,无需开发者对其有特别关注。

如果程序中运行的线程不是守护线程daemon,则程序将等待这些线程结束才会终止。而守护线程在程序结束/退出时将会被立刻终止(killed)。

比较下面的代码,注意运行结果。线程采用默认的守护进程。

import logging
import threading
import time

def thread_function(name):
    logging.info("Thread %s: starting", name)
    time.sleep(2)
    logging.info("Thread %s: finishing", name)

if __name__ == "__main__":
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO,
                        datefmt="%H:%M:%S")
    logging.info("Main    : before creating thread")
    x = threading.Thread(target=thread_function, args=(1,)) 
    logging.info("Main    : before running thread")
    x.start()
    logging.info("Main    : wait for the thread to finish")
    # x.join()
    logging.info("Main    : all done")

运行结果

16:16:30: Main    : before creating thread
16:16:30: Main    : before running thread
16:16:30: Thread 1: starting
16:16:30: Main    : wait for the thread to finish
16:16:30: Main    : all done
>>> 16:16:32: Thread 1: finishing

这段运行结果中,注意到__main__程序已经运行完成,返回Main : all done,之后(守护)线程1才返回结果Thread 1: finishing。在返回Thread 1的结果之前停顿了两秒。这个停顿是Python在等待非守护线程(non-daemonic thread)执行完成。当程序完成,关闭进程(shutdown process)清理了thread routine。

查看threadingthreading._shutdown()的源代码,注意到所有运行中的非守护线程被遍历,并被调用.join()方法应用于这些线程上。

def _shutdown():
    # Obscure:  other threads may be waiting to join _main_thread.  That's
    # dubious, but some code does it.  We can't wait for C code to release
    # the main thread's tstate_lock - that won't happen until the interpreter
    # is nearly dead.  So we release it here.  Note that just calling _stop()
    # isn't enough:  other threads may already be waiting on _tstate_lock.
    if _main_thread._is_stopped:
        # _shutdown() was already called
        return
    tlock = _main_thread._tstate_lock
    # The main thread isn't finished yet, so its thread state lock can't have
    # been released.
    assert tlock is not None
    assert tlock.locked()
    tlock.release()
    _main_thread._stop()
    t = _pickSomeNonDaemonThread()
    while t:
        t.join()
        t = _pickSomeNonDaemonThread()

建议调整daemon参数值查看结果。

join()线程

守护线程容易操作,随着程序结束而退出。但如果需要等待线程结束该怎么办?并且等待线程结束后程序才退出?我们来看代码中被注释掉的命令。

x.join()

调用.join()方法,意在通知一个线程等待另一个线程结束。如果这个命令去掉注释并运行,主线程/主程序将会暂停并等待线程x运行结束。

多线程

刚刚一个案例仅仅给出单个线程的运行结果,下面考虑两个线程的情况,这一部分使用比较原始的方式调用多线程。

import logging
import threading
import time

def thread_function(name):
    logging.info("Thread %s: starting", name)
    time.sleep(2)
    logging.info("Thread %s: finishing", name)

if __name__ == "__main__":
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO,
                        datefmt="%H:%M:%S")

    threads = list()
    for index in range(3):
        logging.info("Main    : create and start thread %d.", index)
        x = threading.Thread(target=thread_function, args=(index,))
        threads.append(x)
        x.start()

    for index, thread in enumerate(threads):
        logging.info("Main    : before joining thread %d.", index)
        thread.join()
        logging.info("Main    : thread %d done", index)

运行结果

19:55:00: Main    : create and start thread 0.
19:55:00: Thread 0: starting
19:55:01: Main    : create and start thread 1.
19:55:01: Thread 1: starting
19:55:01: Main    : create and start thread 2.
19:55:01: Thread 2: starting
19:55:01: Main    : before joining thread 0.
19:55:03: Thread 0: finishing
19:55:03: Thread 2: finishing
19:55:03: Thread 1: finishing
19:55:03: Main    : thread 0 done
19:55:03: Main    : before joining thread 1.
19:55:03: Main    : thread 1 done
19:55:03: Main    : before joining thread 2.
19:55:03: Main    : thread 2 done

观察返回结果,三个线程按设定的顺序启动,但是却以相反的顺序结束。多次运行上面代码会产生不同的顺序。

线程运行的顺序由操作系统来决定,并难以预测,每次运行的结果都不尽相同。因此在使用多线程设计算法时应记得这件事 。Python中 提供了原生支持(primitives)协调不同的线程使其一起运行。

我们下面来看一种方便的运行多线程的方式。

使用ThreadPoolExecutor运行多线程

上面的多线程运行方式相对复杂,需要更多设置。Python的标准库concurrent.futures提供了ThreadPoolExecutor工具以简便的方式运行多线程。

使用前后文管理器(context manager),即with命令来管理线程池的创建和毁灭。

上面的多线程调用方式用ThreadPoolExecutor重写。

import concurrent.futures

# [rest of code]

if __name__ == "__main__":
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO,
                        datefmt="%H:%M:%S")
    with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
        executor.map(thread_function, range(3))

前后文管理器的方式调用多线程,可以避免忘记在线程运行中使用.join()方法带来的潜在问题。

这个例子中使用了executor对象中的map()方法,用于将不同的函数/线程与不同的输入参数相匹配。

运行结果如下

20:09:11: Thread 0: starting
20:09:11: Thread 1: starting
20:09:11: Thread 2: starting
.result_iterator at 0x7ff6eb868200>
20:09:13: Thread 0: finishing
20:09:13: Thread 2: finishing
20:09:13: Thread 1: finishing

Race Conditions 竞态条件

竞态条件发生在两个或更多线程访问,特别是修改,共享数据或资源时。大多数竞态条件并不会发生的非常明显,而且他们只是偶尔发生并产生令人困惑的结果且难于debug。

一个常见的竞态条件案例是不同的线程都试图修改数据库的某个数据。如每个线程都在当前基础上对该数据做自增操作,产生竞态条件时,数据的结果和预期将不一致。

代码如下。注意这里使用了executor对象的submit()方法,用于对比map()方法。

class FakeDatabase:
    def __init__(self):
        self.value = 0

    def update(self, name):
        logging.info("Thread %s: starting update", name)
        local_copy = self.value
        local_copy += 1
        time.sleep(0.1)
        self.value = local_copy
        logging.info("Thread %s: finishing update", name)
if __name__ == "__main__":
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO,
                        datefmt="%H:%M:%S")
    database = FakeDatabase()
    logging.info("Testing update. Starting value is %d.", database.value)
    with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
        for index in range(2):
            executor.submit(database.update, index)
    logging.info("Testing update. Ending value is %d.", database.value)

解决竞态条件的最基本方案是采用同步机制Synchronisation。

使用Lock的同步机制Synchronisation

锁是解决race conditions的常见方案。为了避免race condition发生,应只允许代码的读写部分每次只运行一个线程。Python的常规方式的使用Lock。在其他语言中,这个方案也称mutex, a.k.a., MUTual EXclusion互斥锁。

锁的性质包括,每次只有一个线程可以获得锁,而其他试图获得锁的线程需要等待直到获得锁的线程释放锁。

实现锁的获取和释放的基本函数是.acquire().release()方法。线程调用my_lock.acquire()方法获得锁,如果锁被一个线程获得,其他试图获得该锁的线程只能等待释放。如果线程获得了锁却不释放,程序将会阻塞。

Python的锁可通过前后文管理器调用,避免了忘记调用或无法调用.release()带来的潜在结果。

class FakeDatabase:
    def __init__(self):
        self.value = 0
        self._lock = threading.Lock()

    def locked_update(self, name):
        logging.info("Thread %s: starting update", name)
        logging.debug("Thread %s about to lock", name)
        with self._lock:
            logging.debug("Thread %s has lock", name)
            local_copy = self.value
            local_copy += 1
            time.sleep(0.1)
            self.value = local_copy
            logging.debug("Thread %s about to release lock", name)
        logging.debug("Thread %s after release", name)
        logging.info("Thread %s: finishing update", name)

该对象中加入了self._lock成员,是threading.Lock()对象。该成员用于在解锁状态下加锁和释放。

在该案例中,修改数据库的线程将会获得锁,如果锁可被获得,之后执行对数据库的操作,并释放锁。在这个过程中其他修改数据库的线程无法获得锁,因此等待该线程释放。

设置logging set到DEBUG查看更详细结果。

logging.getLogger().setLevel(logging.DEBUG)

代码如下

if __name__ == "__main__":
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO,
                        datefmt="%H:%M:%S")
    logging.getLogger().setLevel(logging.DEBUG)
    database = FakeDatabase()
    logging.info("Testing update. Starting value is %d.", database.value)
    with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
        for index in range(3):
            executor.submit(database.locked_update, index)
    logging.info("Testing update. Ending value is %d.", database.value)

运行结果如下

20:44:04: Testing update. Starting value is 0.
20:44:04: Thread 0: starting update
20:44:04: Thread 0 about to lock
20:44:04: Thread 0 has lock

20:44:04: Thread 1: starting update
20:44:04: Thread 1 about to lock

20:44:04: Thread 2: starting update
20:44:04: Thread 2 about to lock

20:44:04: Thread 0 about to release lock
20:44:04: Thread 0 after release
20:44:04: Thread 0: finishing update
20:44:04: Thread 1 has lock
20:44:04: Thread 1 about to release lock
20:44:04: Thread 1 after release
20:44:04: Thread 1: finishing update
20:44:04: Thread 2 has lock
20:44:04: Thread 2 about to release lock
20:44:04: Thread 2 after release
20:44:04: Thread 2: finishing update
20:44:04: Testing update. Ending value is 3.

结果显示,加锁不仅解决了race condition,也保证了线程的执行顺序。

(2022.09.21 Wed)

Deadlock 死锁

我们来看使用Lock过程中一个经常遇到的问题。如果一个线程已经获得了锁,当有线程再次试图用.acquire()方法获取该锁时,需要等待持有该锁的线程释放,即.release()。我们来看下面这段代码。

import threading

l = threading.Lock()
print("before first acquire")
l.acquire()
print("before second acquire")
l.acquire()
print("acquired lock twice")

当第二次出现I.acquire()命令,线程将会阻塞并等待该锁被释放,也就是形成了deadlock。解决deadlock,可去除第二个acquire()方法。然而deadlock经常在下面两种情况下发生:

  • 实现过程中出现bug,锁没有被恰当的释放
  • 设计出现问题,utility函数被其他有锁或没有锁的函数调用

第一种情况可以使用前后文管理器,即with命令解决,可避免因未写.release()命令导致的锁未释放的问题。

第二种情况的设计问题在一些语言中略显棘手。针对这个问题,Python threading提供了一个RLock对象。RLock对象允许线程多次获取锁。同时在释放时也许执行.release()相同的次数。

>>> import threading
>>> rl = threading.RLock()
>>> rl.acquire()
True
>>> rl.acquire()
True
>>> rl.__str__()
''
>>> rl._is_owned()
True
>>> rl.release()
>>> rl.release()
>>> rl.release()
Traceback (most recent call last):
  File "", line 1, in 
RuntimeError: cannot release un-acquired lock

RLock的内置方法可以查看该锁是否被占用._is_owned(),锁被获取的次数,即.__str__()中的count字段。

LockRLock是多线程编程中两个常用的基本工具,其他工具包括SemaphoreTimerBarrier

  • Semaphore信号量是个计数器。其第一个特殊属性是计数的原子性(atomic),原子性表明系统不会在计数器自增或自减时将线程清除(swap out)。内部计数器在调用.release()是自增,在调用.acquire()是自减。另一个特殊属性是当计数器为0而一个线程试图用.acquire()获取锁时,该线程将被阻塞(block)直到有线程做释放动作使得计数器加1。信号量常用于保护容量受限的资源,比如连接池中会限制对该对象的连接数目。
  • Timer计时器用于定时执行一个函数,执行命令形如
t = threading.Timer(30.0, my_function)

调用.start()方法开始计时器,函数my_function将在特定时间之后被新线程调用。注意,无法严格保证函数在设定的时间被执行。取消计时器使用.cancel()命令。在计时器出发后调用该命令不返回结果不生成异常。计时器用于在特定时间后执行特定动作,如果想在计时器的时间到达前(expire)执行,则调用.cancel()方法。

  • Barrier线程用于在同步中保存固定数字的线程。创建Barrier时,调用者(caller)需要指定保持同步的线程数字。每个线程在Barrier上调用.wait()方法。等待的线程保持阻塞,直到等待的线程数达到预设数值,之后同时获得同步和锁(?)。线程由操作系统统筹,即便线程被同时释放,他们仍然每次只有一个在运行。Barrier的用途之一是允许一系列线程对自身初始化。线程初始化后在Barrier上等待会确保所有线程都能在完成初始化后再运行。

Producer-Consumer 生产者-消费者模式

该设计模式可通过LockQueue两种方式实现。

Lock实现P-C模式

这里使用一个案例,生产者线程从虚拟网络中读数据并将消息推进pipeline。

import random 

SENTINEL = object()

def producer(pipeline):
    """Pretend we're getting a message from the network."""
    for index in range(10):
        message = random.randint(1, 101)
        logging.info("Producer got message: %s", message)
        pipeline.set_message(message, "Producer")
    # Send a sentinel message to tell consumer we're done
    pipeline.set_message(SENTINEL, "Producer")

SENTINEL对象表示停止点。消费者的实现如下

def consumer(pipeline):
    """Pretend we're saving a number in the database."""
    message = 0
    while message is not SENTINEL:
        message = pipeline.get_message("Consumer")
        if message is not SENTINEL:
            logging.info("Consumer storing message: %s", message)

消费者从pipeline中读信息并写入假数据库,实则只是打印出来。如果遇到SENTINEL值,则从函数返回,终结线程。主程序如下

from concurrent import futures
if __name__ == "__main__":
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO,
                        datefmt="%H:%M:%S")
    # logging.getLogger().setLevel(logging.DEBUG)
    pipeline = Pipeline()
    with futures.ThreadPoolExecutor(max_workers=2) as executor:
        executor.submit(producer, pipeline)
        executor.submit(consumer, pipeline)

来看Pipeline对象

class Pipeline:
    """
    Class to allow a single element pipeline between producer and consumer.
    """
    def __init__(self):
        self.message = 0
        self.producer_lock = threading.Lock()
        self.consumer_lock = threading.Lock()
        self.consumer_lock.acquire()

    def get_message(self, name):
        logging.debug("%s:about to acquire getlock", name)
        self.consumer_lock.acquire()
        logging.debug("%s:have getlock", name)
        message = self.message
        logging.debug("%s:about to release setlock", name)
        self.producer_lock.release()
        logging.debug("%s:setlock released", name)
        return message

    def set_message(self, message, name):
        logging.debug("%s:about to acquire setlock", name)
        self.producer_lock.acquire()
        logging.debug("%s:have setlock", name)
        self.message = message
        logging.debug("%s:about to release getlock", name)
        self.consumer_lock.release()
        logging.debug("%s:getlock released", name)

上面的实现有过多的logging信息,一份简明版如下

class Pipeline:
    """
    Class to allow a single element pipeline between producer and consumer.
    """
    def __init__(self):
        self.message = 0
        self.producer_lock = threading.Lock()
        self.consumer_lock = threading.Lock()
        self.consumer_lock.acquire()

    def get_message(self, name):
        self.consumer_lock.acquire()
        message = self.message
        self.producer_lock.release()
        return message

    def set_message(self, message, name):
        self.producer_lock.acquire()
        self.message = message
        self.consumer_lock.release()

注意到Pipeline中的.get_message().set_message()方法实现的功能相反,但是他们都首先获得consumer/producer的锁,再释放producer/consumer的锁。

consumer释放producer的锁之前,完成对message的处理,producer可以运行。这样的顺序可以保证避免发生race condition。同样的顺序也发生在producer身上。运行结果如下

12:17:02: Producer got message: 83
12:17:02: Producer got message: 25

12:17:02: Consumer storing message: 83
12:17:02: Producer got message: 59
12:17:02: Consumer storing message: 25
12:17:02: Producer got message: 65
12:17:02: Consumer storing message: 59
12:17:02: Producer got message: 44
12:17:02: Consumer storing message: 65

12:17:02: Producer got message: 58
12:17:02: Consumer storing message: 44
12:17:02: Producer got message: 49
12:17:02: Consumer storing message: 58
12:17:02: Consumer storing message: 49
12:17:02: Producer got message: 76
12:17:02: Producer got message: 60
12:17:02: Consumer storing message: 76
12:17:02: Consumer storing message: 60
12:17:02: Producer got message: 20
12:17:02: Consumer storing message: 20

观察运行结果,你可能会觉得奇怪,在consumer运行前producer生成了两个消息。查看producer和.set_message()的代码,注意到producer等待Lock发生在它试图将信息put进入pipeline时。当producer试图发送第二条信息时,调用.set_message()并阻塞。

操作系统可以在任何时候交换线程,但一般来说会允许每个线程有足够的时间运行完成。这也是为什么producer会在运行.set_message()时会阻塞。一旦线程被阻塞,系统会换个线程并找到另一个线程来运行。在这个案例中,仅有的另一个线程是consumer。

consumer调用.get_message(),读取消息,调用producer_lock.release()方法释放producer锁,允许producer在线程交换时再次运行。

第一个消息是83,这是consumer读取的消息,尽管producer已经生成了第二个消息25。

该案例在有限运行次数下有效,但对于producer-consumer问题并不是好的解决方案,因为只允许pipeline中有一个值。当producer突然产生了多个消息,这些消息将无处存放。

Queue实现P-C模式

上面案例每次只生成一个消息,如果pipeline一次进入多个消息,需要用可变的数据结构代替前面的做法。

Python标准库提了Queue类,可用Queue类型代替Lock中的保护变量。同样的,可以用Python threading中的Event停止/控制线程。

先来尝试一下Eventthreading.Event对象允许线程标记一个事件(signal an event)同时其他线程可等待事件发生。重点在于等待事件发生的线程在等待时不需要暂停,他们可以每隔一段时间检测事件状态(every once in a while)。

事件可以多种形式触发。在下面案例中,主线程在sleep中使用.set()方法触发。

if __name__ == "__main__":
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO,
                        datefmt="%H:%M:%S")
    # logging.getLogger().setLevel(logging.DEBUG)

    pipeline = Pipeline()
    event = threading.Event()
    with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
        executor.submit(producer, pipeline, event)
        executor.submit(consumer, pipeline, event)

        time.sleep(0.1)
        logging.info("Min: about to set event")
        event.set()

在上面代码中创建了threading.Event()对象,在主程序sleep之后,用.set()方法触发事件。producer函数做如下修改

def producer(pipeline, event):
    """Pretend we're getting a number from the network."""
    while not event.is_set():
        message = random.randint(1, 101)
        logging.info("Producer got message: %s", message)
        pipeline.set_message(message, "Producer")

    logging.info("Producer received EXIT event. Exiting")

producer不在需要SENTINEL对象。consumer做如下修改

def consumer(pipeline, event):
    """Pretend we're saving a number in the database."""
    while not event.is_set() or not pipeline.empty():
        message = pipeline.get_message("Consumer")
        logging.info(
            "Consumer storing message: %s  (queue size=%s)",
            message,
            pipeline.qsize(),
        )

    logging.info("Consumer received EXIT event. Exiting")

consumer确保queue为空,之后结束线程。如果consumer退出,而pipeline中仍然有消息,有两种后果,1) 丢失最后的消息,2) producer会不停的将信息push进queue中。这种情况发生在producer检测.is_set()后event被触发,且producer没有调用pipeline.set_message()之前 。

一旦这种情况发生,consumer被唤醒和 退出,queue仍然满。producer调用.set_message()等待直到queue有新的空间存放消息。此时 consumer已经退出,所以queue将保持满状态且producer不会退出。

Pipeline部分变动明显

class Pipeline(queue.Queue):
    def __init__(self):
        super().__init__(maxsize=10)

    def get_message(self, name):
        logging.debug("%s:about to get from queue", name)
        value = self.get()
        logging.debug("%s:got %d from queue", name, value)
        return value

    def set_message(self, value, name):
        logging.debug("%s:about to add %d to queue", name, value)
        self.put(value)
        logging.debug("%s:added %d to queue", name, value)

Pipeline继承了queue.Queue,其中有可选参数可用于设置queue的最大尺寸。如果不设置最大尺寸,queue有可能涨到电脑内存的 极限。

注意到.get_message().set_message()方法简洁了很多,因为Queue类中包含了所有锁代码,且Queue是线程安全的(thread-safe)。

运行如下

Producer got message: 32
Producer got message: 51
Producer got message: 25
Producer got message: 94
Producer got message: 29
Consumer storing message: 32 (queue size=3)
Producer got message: 96
Consumer storing message: 51 (queue size=3)
Producer got message: 6
Consumer storing message: 25 (queue size=3)
Producer got message: 31
...
Producer got message: 52
Consumer storing message: 98 (queue size=6)
Main: about to set event
Producer got message: 13
Consumer storing message: 59 (queue size=6)
Producer received EXIT event. Exiting
Consumer storing message: 75 (queue size=6)
Consumer storing message: 97 (queue size=5)
Consumer storing message: 80 (queue size=4)
Consumer storing message: 33 (queue size=3)
Consumer storing message: 48 (queue size=2)
Consumer storing message: 52 (queue size=1)
Consumer storing message: 13 (queue size=0)
Consumer received EXIT event. Exiting

producer创建前5个数据,并将其中的4个 push进pipeline。之后系统将线程转换,producer未将第5个push进pipeline。之后consumer运行从pipeline中拿到第一个消息,并打印queue的长度(这也是我们知道第5个消息没有push进queue的原因)。queue长度为10,所以producer并没有被queue阻塞 ,它只是被系统做转换(swap out)。

随着程序运行,可以看到主线程产生了event,导致producer立刻退出。consumer仍有工作要做,直到pipeline被清空(clean out)。

相比于前面一节,现在的方案是解决producer-consumer问题更好的方案,但仍然可以简化,pipeline可以直接用queue代替。

import concurrent.futures
import logging
import queue
import random
import threading
import time

def producer(queue, event):
    """Pretend we're getting a number from the network."""
    while not event.is_set():
        message = random.randint(1, 101)
        logging.info("Producer got message: %s", message)
        queue.put(message)

    logging.info("Producer received event. Exiting")

def consumer(queue, event):
    """Pretend we're saving a number in the database."""
    while not event.is_set() or not queue.empty():
        message = queue.get()
        logging.info(
            "Consumer storing message: %s (size=%d)", message, queue.qsize()
        )

    logging.info("Consumer received event. Exiting")

if __name__ == "__main__":
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO,
                        datefmt="%H:%M:%S")

    pipeline = queue.Queue(maxsize=10)
    event = threading.Event()
    with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
        executor.submit(producer, pipeline, event)
        executor.submit(consumer, pipeline, event)

        time.sleep(0.1)
        logging.info("Main: about to set event")
        event.set()

Lock和Queue都是解决并发问题的可行方案,除此之外还包括上面介绍的Semaphore,Timer和Barrier等方式。

Reference

1 free AI hub, 5分钟完全掌握Python协程
2 An Intro to Threading in Python, Realpython, by Jim Anderson·

你可能感兴趣的:(并发-线程 threading in Python, 2022-07-08)