唠叨话
进程和线程应该是编程语言老生常谈的话题了,但是python在这俩个方面,只能说能用,不怎么关心,起码在早期的python原生的库中,相关的支持很简陋,比如thread库,基本的同步原语都没,感觉早期python目标场景,比如运维领域,不需要多进程、多线程,后来,在需要高并发的场景,比如web,python又在不断迭代协程来实现
threading
threading模块封装了很多的同步原语,
- Lock
- 原始锁,实际上就是使用匿名信号量实现的,一般语言里面的原始锁可以由互斥量、临界区、信号量实现的,python这里选择了匿名信号量实现,也就是说,这个锁,谁(所有线程)都可以减去获取锁acquire、就是信号量减一,释放锁release、信号量加一
- Rlock
- 重入锁RLock,相当于java的重入锁,基于Lock实现的,就是在acquire的时候记录当前线程id,release的时候判定线程是不是acquire的线程
- condition,条件,维护一个锁Lock/RLock,和一个waiting池,线程通过acquire获得condition对象,当wait的时候,线程会释放condition内部的锁进入block状态,同时waiting池中记录这个线程,当notify,condition对象从waiting池中挑选一个线程,通知其acquire
- Semaphore,信号量,基于Condition实现
- BoundedSemaphore,防止Semaphore被无限释放
- event 基于Condition,控制线程间运行顺序
- Barrier 基于Condition,让特定线程先运行
- Timer 基于Thread和Event实现,定时任务
从锁上看,python原生库提供的支持很基础,而且没有特定的优化,相对于java在锁方面,大量运用CAS原语,提供各种字节码指令,优化各种并发情况下的同步操作,以达到高性能,python在这方面就很随意,为什么呢?因为python本身有着GIL,在多线程效率方面就天生有着限制,我觉得GIL就是对单核操作系统执行多进程的模拟,另外,如果说高并发的话,python可以用协程实现,比如tornado、asyncio,底层使用是IO多路复用、select/poll/epoll/kqueue等,不过老实说,在协程方面,python又不如go、scala、erlang等完善,python自己的协程框架asyncio的三方生态太少,单说高并发,也不及Go等
创建线程调用链: start -> _thread._start_new_thread -> thread_Pythread_start_new_thread -> PyThread_start_new_thread -> pthread_create
通过这个调用链看出,在linux,python的线程实现,就是通过pthread库函数创建的,是一种用户态的线程,和linux内核进程一对一
线程池
一个基本模型
不管是线程池还是进程池,都要维护俩个核心东西:进(线)程队列和任务队列,进(线)程队列中的进程从任务队列中取任务去执行,为了取任务的时候,各个进(线)程不冲突,需要一个锁,并将返回给一个对象,一般都叫future对象,后续通过这个future获取任务完成后的返回值。
关于以上这个模型,有一个很好的现实场景对应,假设一个银行有三个功能一模一样的窗口(进/线程队列),一群人(任务队列)去这个银行办理业务,这些都是领号,然后当某个窗口空了,大厅的屏幕上,就分配一个号对应的人,去那个窗口办理业务,这个分配号到窗口的系统,保证一次就只会把一个号分配到一个空闲的窗口,这就是锁的作用。
1 | |
这里有个分配号到窗口的具体逻辑,如果是按照号的顺序去分配,就是典型的FIFO队列,java的ArrayBlockingQueue/LinkedBlockingQueue就是这样的,但是如果某些银行的vip客户,可以优先分配到空闲窗口,那么就是优先级队列,java的PriorityBlockingQueue就是这样实现的。当然,现实场景往往是俩者结合的,想想,如果优先级队列的优先级,按照任务入队列的时间顺序递减,那么不就是FIFO队列吗?还有一种队列,就是随机选客户,众生平等,这会让编程变得不可预期,所以实际应用应该很少,起码java好像没有这种队列
ThreadPoolExecutor
java中也有一个ThreadPoolExecutor实现线程池,感觉python像是直接参考过来的。
结合上述模型,下面来分析源码,源码来自于github,我这里都是看的3.10-master上的代码,这里做了些省略
1 | |
通过分析代码,发现python的线程池十分基础,既没有java的ThreadPoolExecutor的核心线程与非核心线程,也不支持除了FIFO的其他任务队列,不过都可以自己参考来实现,也实现了基本的线程池模型
flag: 找找有没有功能更全的threadpoolexecutor或者自己实现进程
什么是进程?一般回答是进程是操作系统分配资源的基本单位,相对应的,线程是cpu调度的最小单位,在不同的操作系统上,线程的实现也有所差异。
multiprocessing.Process
源码对应cpython/Lib/multiprocessing/context.py,可以看到Process的实现基本是继承BaseProcess,所以来分析BaseProcess如何创建进程的吧,下面的中文注释就是分析过程同样,做了省略
1 | |
进程间通信
线程共享堆上的数据,本身就可以通信。
回顾一下进程间通信的方法:共享内存、管道、信号量、套接字、文件、消息队列、信号等,下面就看看python这块的具体实现吧
- 首先,在分析进程创建的时候,就使用了匿名管道,这个时os模块实现的
- multiprocessing.Pipe,管道,在linux,使用域间套接字实现的
- multiprocessing.Queue,队列,实际上基于上面的Pipe实现的
- sharedMemory,共享内存,基于mmap实现的,将进程自己的虚拟内存映射到同一片物理内存
- shareableList,基于sharedMemory实现的
有了这些,基本的进程间通信时没什么问题的
这里没有说锁的实现,因为就是用的线程threading模块的锁
进程池mumltiproessing.Pool
结合在说明线程池的时候,描述的模型,和去银行窗口办业务,这里不详细分析源码了,直接解释关键的实现
_taskqueue存放任务的队列,对这个队列取任务时,会使用锁锁定_poolworker进程队列_inqueue实际执行任务时,各个进程都是从这个队列取任务_outqueue发送结果的队列_work_handler线程,保证进程池在worker进程有退出的时候创建新的进程,添加到pool_task_handler线程,从任务队列中,取出任务,放入_inqueue_handle_results线程,将处理完的任务的结果,从_outqueue取出