Python 多线程笔记
中国有句古话:好记性不如烂笔头。看会了和学会了是两码事,能教会别人说明是真掌握了。
比起一堆 job1 | job2 和 生产者-消费者 模型,单纯复制粘贴相似度 99% 的代码就算自己敲一遍也不会有印象。
反向思考
请写出一个死锁的案例
加锁即为互斥,防止多线程同时修改同一个资源导致数据错误。
A 拿酱油
-> 酱油炒饭耗时10分钟
-> 拿盐 # 等待B放回盐
-> 盐水菜心耗时5分钟
-> 放回盐
-> 放回酱油
B 拿盐
-> 盐水菜心耗时5分钟
-> 拿酱油 # 等待A放回酱油
-> 酱油炒饭耗时10分钟
-> 放回酱油
-> 放回盐
你在等酱油我在盐,两人的下一道菜就永远卡这了。
当然这种写法肯定是有问题的,资源执行和释放的顺序不一致。
解决方法
让步:
只要每个线程执行完任务立马释放相应资源,线程崩溃的话设置好超时释放。
排队:
同时竞争资源,用队列(推荐)解决。
规则:
占用资源前给予规则限制。
例子:5人吃饭同时拿起左边筷子并等待右边筷子的释放。
解决:给筷子编号,没拿到小号筷子前不能拿大号的。最大数字的筷子不可能单独拿到,问题解决。
如果用多进程处理多线程代码,会发生什么
问的其实是二者在使用方面的区别,首先自然是确定多线程代码执行的任务是什么。
如果要处理 `IO密集型`任务,用多进程反而会慢,主要耗时在创建和维护进程过程。
如果处理 `CPU密集型`任务,则可以充分利用多核优势并行计算。
多进程的主要问题:
- 进程不共享内存,计算的输入必须被传到每个工作进程里,比如列表中的元素;
- 能被传递的东西必须 picklable,而有相当多的东西是 unpicklable 的;
- 如果后续程序执行需要并行计算的输出,那么这些输出也得 picklable;
- Pickle -> unpickle 操作带来了额外的性能开销。
多线程涉及资源竞争&修改、死锁、上下文切换导致效率下降问题。
什么时候加 .join()
|
|
很多人会这么写多线程代码,执行后会发现上面写法需要 5 秒多点没问题,但是下面的写法串行输出一共需要 5+3 秒,然后:
这是 GIL 导致的多线程效率问题,所以我们用多进程吧。
呃呃。
.join() 会卡住主线程,并让当前已经 .start() 的子线程继续运行,直到调用 .join() 的这个线程运行完毕才会继续运行主线程。
|
|
就算给每个线程都加,耗时短的任务立刻返回结果让其他子线程继续执行,没有任何意义。
.join() 强调顺序执行,你只需要阻塞耗时最长的线程任务即可。
所以上述代码改成这样:
|
|
为什么爬虫多个 url 返回前,每个线程都要加 .join()呢?因为每个线程在执行网络请求的时候,返回时间有差异,必须保证每个线程返回结果主线程才能调度后续任务。
|
|
什么时候加 .setDeamon(True)
守护:与主线程同生共死,这么理解就很直观了。
如果主线程永远不退出,设置守护线程无意义。
如果把子线程的代码写在 while True 里面一直循环,要设置为守护线程。不然主线程结束了,子线程还一直运行,程序结束不了。
事件的使用场景
常规使用场景:
用户外部发送终止命令结束程序,彻底结束子线程。
等待数据库的连接信号就绪。
池与信号量是两个完全不同的概念 ,进程池Pool(4),最大只能产生4个进程,而且从头到尾都只是这四个进程,不会产生新的,而信号量是产生一堆线程/进程。