目录

线程间通信

用于线程安全通信的 Python 数据结构

集合(Set)

示例

装饰器

示例

列表(List)

队列(Queue)

队列的类型

普通队列(先进先出,FIFO)

Python 实现先进先出队列

单线程的先进先出队列

示例

输出结果

多线程的先进先出队列

示例

输出结果

后进先出队列(LIFO)

Python 实现后进先出队列

单线程的后进先出队列

示例

输出结果

多线程的后进先出队列

示例

输出结果

优先级队列

Python 实现优先级队列

单线程的优先级队列

示例

输出结果

多线程的优先级队列

示例

输出结果


线程间通信

在实际场景中,一个团队协作完成共同任务时,成员之间必须通过沟通才能顺利完成工作,这个道理同样适用于线程。在编程中,我们创建多个线程并为每个线程分配不同的子任务,以此减少处理器的空闲时间。因此,线程之间也需要具备通信能力,通过相互交互以同步的方式完成整体任务。

关于线程间通信,需关注以下几个重要点:

  1. 无性能收益:如果线程和进程之间无法实现有效的通信,并发和并行带来的性能提升将毫无意义。
  2. 保障任务完成:缺少完善的线程间通信机制,分配的任务将无法被正确完成。
  3. 比进程间通信更高效:线程间通信比进程间通信更高效、更易用,因为同一进程内的所有线程共享相同的地址空间,无需使用共享内存。

用于线程安全通信的 Python 数据结构

多线程代码中存在一个问题:如何将信息从一个线程传递到另一个线程。标准的通信原语无法解决该问题,因此我们需要实现自定义的复合对象,让线程之间可以安全地共享对象,实现线程安全的通信。以下几种数据结构经过简单改造后,即可支持线程安全的通信:

集合(Set)

要以线程安全的方式使用集合数据结构,我们需要继承集合类,实现自定义的锁机制。

示例

以下 Python 示例演示了如何继承集合类实现线程安全:

class extend_class(set):
    def __init__(self, *args, **kwargs):
        self._lock = Lock()
        super(extend_class, self).__init__(*args, **kwargs)

    def add(self, elem):
        self._lock.acquire()
        try:
            super(extend_class, self).add(elem)
        finally:
            self._lock.release()

    def delete(self, elem):
        self._lock.acquire()
        try:
            super(extend_class, self).delete(elem)
        finally:
            self._lock.release()

在上述示例中,我们定义了一个名为extend_class的类,它继承自 Python 的set类。在该类的构造方法中创建了一个锁对象,同时定义了add()delete()两个线程安全的方法。这两个方法均基于父类的功能实现,唯一的区别是增加了锁的获取和释放逻辑。

装饰器

使用装饰器是实现线程安全通信的另一种重要方法。

示例

以下 Python 示例演示了装饰器的使用方式:

def lock_decorator(method):
    def new_deco_method(self, *args, **kwargs):
        with self._lock:
            return method(self, *args, **kwargs)
    return new_deco_method

class Decorator_class(set):
    def __init__(self, *args, **kwargs):
        self._lock = Lock()
        super(Decorator_class, self).__init__(*args, **kwargs)

    @lock_decorator
    def add(self, *args, **kwargs):
        return super(Decorator_class, self).add(elem)
    
    @lock_decorator
    def delete(self, *args, **kwargs):
        return super(Decorator_class, self).delete(elem)

在上述示例中,我们定义了一个名为lock_decorator的装饰器方法,随后在Decorator_class的构造方法中创建了锁对象,并为add()delete()方法添加了该装饰器,让这两个方法实现了线程安全。它们同样基于父类的功能实现,仅增加了装饰器的锁逻辑。

列表(List)

列表是一种线程安全、高效且易用的数据结构,适合作为临时的内存存储容器。在 CPython 中,全局解释器锁(GIL)会保护列表,防止其被并发访问。但需要注意的是,列表本身是线程安全的,但其内部存储的数据却不受保护

例如,若有另一个线程同时执行L.append(x)操作,该操作无法保证返回预期结果。这是因为尽管append()是原子操作且线程安全,但其他线程对列表数据的并发修改,仍会让输出结果出现竞态条件带来的副作用。

为了解决这类问题并安全地修改数据,我们必须实现完善的锁机制,确保多个线程不会陷入竞态条件。可以像前面的示例一样,通过继承列表类来实现自定义的锁机制。

列表的其他原子操作如下:

L.append(x)
L1.extend(L2)
x = L[i]
x = L.pop()
L1[i:j] = L2
L.sort()
x = y
x.field = y
D[x] = y
D1.update(D2)
D.keys()

说明:

  • L、L1、L2 均为列表
  • D、D1、D2 均为字典
  • x、y 为对象
  • i、j 为整数

队列(Queue)

如果列表内的数据不受保护,我们可能会因竞态条件获取或删除错误的数据项,因此推荐使用队列数据结构

队列的现实例子有很多:单车道的单向公路遵循 “先进先出” 规则,售票处、公交站的排队队伍也是如此。

队列是 Python默认线程安全的数据结构,我们无需手动实现复杂的锁机制。Python 提供了queue模块,让我们可以在应用中使用不同类型的队列。

队列的类型

Python 的queue模块提供了三种队列类型,本节将分别介绍:

  • 普通队列(先进先出,FIFO)
  • 后进先出队列(LIFO)
  • 优先级队列

普通队列(先进先出,FIFO)

这是 Python 最常用的队列实现,遵循 “先进先服务” 的原则,也被直接称为先进先出队列

Python 实现先进先出队列

Python 中,先进先出队列可基于单线程实现,也可基于多线程实现。

单线程的先进先出队列

实现单线程 FIFO 队列时,可使用queue.Queue类实现基础的先进先出容器:通过put()方法向序列的一端添加元素,通过get()方法从另一端移除元素。

示例

以下 Python 程序实现了单线程的先进先出队列:

import queue

q = queue.Queue()

for i in range(8):
    q.put("item-" + str(i))

while not q.empty():
    print(q.get(), end = " ")
输出结果

plaintext

item-0 item-1 item-2 item-3 item-4 item-5 item-6 item-7

上述输出表明,单线程环境下,队列中元素的移除顺序与插入顺序完全一致。

多线程的先进先出队列

实现多线程 FIFO 队列时,需先定义基于queue模块的myqueue()函数,其中get()put()方法的作用与单线程实现中一致;随后声明并实例化多个线程,这些线程会以先进先出的方式消费队列中的元素。

示例

以下 Python 程序实现了多线程的先进先出队列:

import threading
import queue
import random
import time
def myqueue(queue):
    while not queue.empty():
        item = queue.get()
        if item is None:
            break
        print("{} 从队列中取出 {}".format(threading.current_thread(), item))
        queue.task_done()
        time.sleep(2)
q = queue.Queue()
for i in range(5):
    q.put(i)
threads = []
for i in range(4):
    thread = threading.Thread(target=myqueue, args=(q,))
    thread.start()
    threads.append(thread)
for thread in threads:
    thread.join()
输出结果

plaintext

<Thread(Thread-3654, started 5044)> 从队列中取出 0
<Thread(Thread-3655, started 3144)> 从队列中取出 1
<Thread(Thread-3656, started 6996)> 从队列中取出 2
<Thread(Thread-3657, started 2672)> 从队列中取出 3
<Thread(Thread-3654, started 5044)> 从队列中取出 4

后进先出队列(LIFO)

该队列的规则与先进先出队列完全相反,遵循 **“后进先服务”** 的原则,其实现逻辑与栈(Stack)数据结构一致。后进先出队列在实现人工智能中的深度优先搜索等算法时非常实用。

Python 实现后进先出队列

Python 中,后进先出队列同样可基于单线程和多线程实现。

单线程的后进先出队列

实现单线程 LIFO 队列时,使用queue.LifoQueue类实现基础的后进先出容器:通过put()方法将元素添加到容器的头部,通过get()方法也从头部移除元素。

示例

以下 Python 程序实现了单线程的后进先出队列:

import queue

q = queue.LifoQueue()

for i in range(8):
    q.put("item-" + str(i))

while not q.empty():
    print(q.get(), end=" ")
输出结果

plaintext

item-7 item-6 item-5 item-4 item-3 item-2 item-1 item-0

上述输出表明,单线程环境下,队列中元素的移除顺序与插入顺序完全相反。

多线程的后进先出队列

其实现方式与多线程先进先出队列基本一致,唯一区别是使用queue.LifoQueue类初始化队列,而非queue.Queue

示例

以下 Python 程序实现了多线程的后进先出队列:

import threading
import queue
import random
import time
def myqueue(queue):
    while not queue.empty():
        item = queue.get()
        if item is None:
            break
        print("{} 从队列中取出 {}".format(threading.current_thread(), item))
        queue.task_done()
        time.sleep(2)
q = queue.LifoQueue()
for i in range(5):
    q.put(i)
threads = []
for i in range(4):
    thread = threading.Thread(target=myqueue, args=(q,))
    thread.start()
    threads.append(thread)
for thread in threads:
    thread.join()
输出结果

plaintext

<Thread(Thread-3882, started 4928)> 从队列中取出 4
<Thread(Thread-3883, started 4364)> 从队列中取出 3
<Thread(Thread-3884, started 6908)> 从队列中取出 2
<Thread(Thread-3885, started 3584)> 从队列中取出 1
<Thread(Thread-3882, started 4928)> 从队列中取出 0

优先级队列

先进先出和后进先出队列中,元素的取出顺序都与插入顺序相关,但实际开发中,很多场景下优先级比插入顺序更重要

举一个现实例子:机场安检时,不同人群的检查顺序并非按到达时间排序,要客、航空公司工作人员、海关人员等会被优先检查,普通乘客则按排队顺序检查。

优先级队列的另一个重要应用是任务调度器的开发,一种常见的设计思路是:优先处理队列中优先级最高的任务。优先级队列可根据元素的优先级值来获取队列中的元素。

Python 实现优先级队列

Python 中,优先级队列同样可基于单线程和多线程实现。

单线程的优先级队列

实现单线程优先级队列时,使用queue.PriorityQueue类实现基于优先级的任务容器:调用put()方法时为元素绑定一个优先级值,值越小,优先级越高,调用get()方法时会优先取出优先级高的元素。

示例

以下 Python 程序实现了单线程的优先级队列:

import queue as Q
p_queue = Q.PriorityQueue()

p_queue.put((2, '紧急'))
p_queue.put((1, '最紧急'))
p_queue.put((10, '无关紧要'))
p_queue.put((5, '重要'))

while not p_queue.empty():
    item = p_queue.get()
    print('%s - %s' % item)
输出结果

plaintext

1 - 最紧急
2 - 紧急
5 - 重要
10 - 无关紧要

上述输出表明,队列会根据优先级值存储元素,值越小,优先级越高。

多线程的优先级队列

其实现方式与多线程先进先出、后进先出队列基本一致,主要有两处区别:

  1. 使用queue.PriorityQueue类初始化队列;
  2. 队列的生成方式不同,以下示例中会向队列中添加两组完全相同的数据集。
示例

以下 Python 程序实现了多线程的优先级队列:

import threading
import queue
import random
import time
def myqueue(queue):
    while not queue.empty():
        item = queue.get()
        if item is None:
            break
        print("{} 从队列中取出 {}".format(threading.current_thread(), item))
        queue.task_done()
        time.sleep(1)
q = queue.PriorityQueue()
for i in range(5):
    q.put(i,1)

for i in range(5):
    q.put(i,1)

threads = []
for i in range(2):
    thread = threading.Thread(target=myqueue, args=(q,))
    thread.start()
    threads.append(thread)
for thread in threads:
    thread.join()
输出结果

plaintext

<Thread(Thread-4939, started 2420)> 从队列中取出 0
<Thread(Thread-4940, started 3284)> 从队列中取出 0
<Thread(Thread-4939, started 2420)> 从队列中取出 1
<Thread(Thread-4940, started 3284)> 从队列中取出 1
<Thread(Thread-4939, started 2420)> 从队列中取出 2
<Thread(Thread-4940, started 3284)> 从队列中取出 2
<Thread(Thread-4939, started 2420)> 从队列中取出 3
<Thread(Thread-4940, started 3284)> 从队列中取出 3
<Thread(Thread-4939, started 2420)> 从队列中取出 4
<Thread(Thread-4940, started 3284)> 从队列中取出 4

更多推荐