前面通过几篇文章的篇幅,把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,可以简化大量短时任务的管理和执行,提高资源利用率。
在实际使用中,选择合适的同步原语和并发模式,可以显著提高代码的性能及可维护性。
以上,就是本文的全部内容,感谢您的拨冗阅读。