109、Python并发编程:多线程组件如何选,适用的才是最好的

南宫理的日志录 2024-12-02 16:46:44
引言

前面通过几篇文章的篇幅,把Python中关于多线程编程的相关核心组件介绍了一遍,在介绍各个组件的过程中,也把各自的设计理念、内部实现和应用场景进行了相应的介绍。但是,内容相对离散。本文打算把所有的多线程编程组件串在一起,进行系统性的梳理,从而更加清晰在多线程编程需求中,如何选择最适合的组件。

本文的主要内容有:

1、Lock的设计理念、适用场景及主要操作方法

2、Condition的设计理念、适用场景及主要操作方法

3、Semaphore的设计理念、适用场景及主要操作方法

4、Event的设计理念、适用场景及主要操作方法

5、Queue的设计理念、适用场景及主要操作方法

6、ThreadPoolExecutor的设计理念、适用场景及主要操作方法

1、Lock

设计理念

多线程编程在提高执行效率(不考虑GIL的情况下)的同时,带来了临界资源的竞争问题。为了保护临界资源,一个最简单的思路就是:临界资源的访问修改进行串行化执行,也就是临界区的代码执行是串行的,临界区之外的代码可以随意并发执行。由此诞生了对临界区进行保护的锁的概念。

Lock是最核心也是最简单的同步机制,基于CPU的原子指令来实现,比如Test-And-Set指令等。

使用场景及要解决的问题

为了避免多线程编程中访问和修改共享资源的一致性问题,通过Lock可以控制对共享资源的访问。

当多个线程需要访问同一个资源,比如文件、变量等时,使用Lock来保证同一时刻只有一个线程可以访问该资源。

主要操作方法

1)acquire(blocking=True, timeout=-1):请求锁,在进入临界区之前,应该进行该方法的调用,确保只有一个线程获得锁资源,从而进入临界区。

2)release():释放锁,在线程完成临界区的执行后,应当释放锁,从而允许其他线程对临界区的访问。

2、Condition

设计理念

Condition是一个更高级的同步原语,用于线程间实现通知的机制,它允许一个或多个线程等待某个条件的发生。

Condition的内部实现,需要借助于Lock/RLock的核心同步原语。

使用场景及要解决的问题

适用于线程之间进行相对复杂的交互同步,比如轮流来,有一来一往的交互等。因为在某些情况下,确实存在有些线程需要等待某些条件(比如数据可用),才能继续执行。而Condition基于Lock实现的临界资源保护的同时,还提供了线程间通知的机制,从而使得线程可以有效地等待和通知执行。

主要操作方法

1)wait(timeout=None):释放锁并等待条件的通知,直到条件被满足或者超时。

2)notify(n=1):通知一个或者多个等待的线程,条件已经满足。

3)notify_all():通知所有等待该条件的线程。

3、Semaphore

设计理念

相对于Condition提供的同步原语,Semaphore提供更加复杂的交互同步的实现。

Semaphore是一种信号量,用于控制对共享资源的访问数量,内部维护了一个计数器,标识可用资源的数量。

从源码中,可以看到Semaphore的实现基于Condition而来。

使用场景及要解决的问题

适用于对某些有限资源(比如数据库连接、线程池等)的并发访问,不再单纯是一种信号、状态的机制。

在某些情况下,允许多个线程同时访问某个资源,但是,该资源的数量有限,不可能无限制的允许所有线程访问。这种场景下,特别适合使用Semaphore。

主要操作方法

1)acquire(blocking=True, timeout=-1):请求获得信号量,若信号量计数器为0,则等待。

2)release():释放信号量,增加计数,使得其他等待该信号量的线程被唤醒。

4、Event

设计理念

Event是一种用于线程间同步的通信对象,被设计为维持一个状态变量的对象,用于标识某个事件的发生与否。多个线程之间的先后执行协作,可以基于该事件标志来执行。

从源码中,可以看到相较于Semaphore持有一个资源的计数器,Event更加聚焦于一个事件发生与否的状态维护,所以,持有的是一个布尔变量,用于标识状态。

使用场景及要解决的问题

Event适用于某个线程需要等待其他线程的事件或者信号的发生场景中。比如,在测试或者初始化多个线程的场景中。

主要操作方法

1)wait(timeout=None):阻塞当前线程,直到事件发生,状态被设置。

2)set():设置事件标志,唤醒所有等待该事件的线程。

3)clear():清除事件标识,重新进入等待状态。

5、Queue

设计理念

Queue是一个线程安全的FIFO数据结构,用于在多线程之间安全地进行数据的传递。通过队列,可以同时实现有限资源共享的语义、同步协调的语义等。同时作为一个消息中间件,还可以起到设计上的解耦,以及性能匹配上的削峰填谷的机制。

从源码中,可以看出Queue相关同步语义的实现,也是借助于内部的Lock和Condition。

使用场景及要解决的问题

Queue可以很自然地应用于生产者-消费者模式中,涉及到一方生产资源、一方消耗资源的场景中,都可以自然地选择队列。

当然,前面文章中,已经介绍过不同类型的队列,只要是线程安全的,根据业务规则需要,自行选择即可。

主要操作方法

1)put(item, block=True, timeout=None):将元素/资源放入到队列中,如果队列已满则阻塞。内部维持了一个not_full的条件变量,可以在该条件触发时唤醒阻塞的线程。

2)get(block=True, timeout=None):从队列中获取元素/资源,如果队列为空则阻塞。内部维持了一个not_empty的条件变量,可以在该条件触发时唤醒阻塞的线程。

6、ThreadPoolExecutor

设计理念

相较于前面的各种多线程编程组件,ThreadPoolExecutor是一个更加高层次的线程池实现,用于管理和重用线程资源,并且提供更加简洁的编程接口,简化多线程编程的实现。

使用场景及要解决的问题

当要执行大量的短时间任务时,频繁创建和销毁线程会造成额外的开销,通过ThreadPoolExecutor来复用线程资源,从而实现提升性能的效果。

通常适用于处理大量的独立任务,比如,并行计算、网络请求、数据处理等。

主要操作方法

1)submit(fn, *args, **kwargs):提交表示业务处理逻辑的可调用对象(函数等)到线程池。

2)shutdown(wait=True):关闭线程池,不再接受新任务并等待已提交的任务完成。

3)map(func, iterable):将函数应用于给定的可迭代对象,返回一个结果迭代器。

总结

Python中提供了丰富的多线程编程的组件,虽然有GIL的存在,但不妨碍我们在编码层面,选择更适合业务需求场景的编程组件:

1、Lock,用于实现共享资源保护的核心原语。

2、Condition,用于多线程间的通知机制。

3、Semaphore,强调有限数量资源的合理共享。

4、Event,用于线程间的信号通知,控制执行顺序。

5、Queue,用于线程间的数据的安全传递,作为生产者-消费者模型的基础。

6、ThreadPoolExecutor,可以简化大量短时任务的管理和执行,提高资源利用率。

在实际使用中,选择合适的同步原语和并发模式,可以显著提高代码的性能及可维护性。

以上,就是本文的全部内容,感谢您的拨冗阅读。

0 阅读:4

南宫理的日志录

简介:深耕IT科技,探索技术与人文的交集