社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

小猪的Python学习之旅 —— 6.捋一捋Python线程概念

coder_pig • 7 年前 • 755 次点击  

引言

从刚开始学习Python爬虫的时候,就一直惦记着多线程这个东西,
想想每次下载图片都是单线程,一个下完继续下一个,多呆啊

没占满的带宽(10M带宽),1%的CPU占用率(笔者的是i7 6700K),要不要
那么浪费,所以,不搞点多线程,多进程,协程这样的东西提高下资源利用
率,怎么也说不过去吧?然而关于线程这种话题,一般都是会让很多新手
玩家望而却步,而且听说Python里还有什么全局解释器锁(GIL),搞得Py无法
实现高效的多线程,一听就感觉很难:

虚个卵子哦,跟着小猪把Python里和多线程相关的东西都撸一遍吧!
本节主要是对一些概念进行了解~


1.程序,进程,线程,多线程,多进程

多线程与多进程的理解

操作系统原理相关的书,基本都会提到一句很经典的话:
"进程是资源分配的最小单位,线程则是CPU调度的最小单位"。

说到进程,如果你是windows的电脑的话,Ctrl+Alt+Del打开任务
管理器,可以看到当前电脑上正在运行的很多个进程,网易云啊,
QQ,微信啊,等等;这就是多进程,只是每个进程各司其职完成
对应的功能而已,播放、聊天,互不干扰。这是吃瓜群众的看法,
而对于我们开发来说,多进程的概念更倾向于:多个进程协同地去完成
同一项工作,为什么要在应用里使用多线程,个人的看法如下:
为了摆脱系统的一些限制和为自己的应用获取更多的资源,举个例子:
在Android中为每个应用(进程)限制类最大内存,单个进程超过这个
阀值是会OOM的,而使用多进程技术可以减少内存溢出的问题;
再举个例子:Python在实现Python解析器(CPython)时引入GIL锁
这种东西,使得任何时候仅有一个线程在执行,多线程的效率还
可能比不上单线程,使用多线程可以规避这个限制。

说完多进程,然后说下多线程,首先为何会引入线程呢?举个例子:
你有一个文本程序,接收用户的输入,显示到屏幕上,并保存到硬盘里,
由三个进程组成:输入接收进程A,显示内容进程B,写入硬盘进程C,
而他们之间共同需要要拥有的东西——文本内容,因为进程A,B,C
运行在不同的内存空间,这就涉及到进程通信问题了,而频繁的切换
势必导致性能上的损失。有没有一种机制使得做这三个任务时共享资源呢?
这个时候线程(轻量级的进程)就粉墨登场啦!感觉就像进程又开辟了
一个小世界一样:系统 -> 进程 -> 线程,系统里有很多进程,进程里
又有很多线程。(有点像斗破小说那种套路...)

相信到这里你对多进程和多线程的概念就应一清二楚了,简单比较下
两者的区别与使用场景吧:(摘自:浅谈多进程多线程的选择)

对比维度 多进程 多线程
数据共享、同步 数据共享复杂,需要用IPC;
数据是分开的,同步简单
共享进程数据,数据共享简单,
但也是因为这个原因导致同步复杂
内存、CPU 占用内存多,切换复杂,CPU利用率低 占用内存少,切换简单,CPU利用率高
创建销毁、切换 创建销毁、切换复杂,速度慢 创建销毁、切换简单,速度很快
编程、调试 编程简单,调试简单 编程复杂,调试复杂
可靠性 进程间不会互相影响 一个线程挂掉将导致整个进程挂掉
分布式 适应于多核、多机分布式;如果一台
机器不够,扩展到多台机器比较简单
适应于多核分布式

2.线程的生命周期

各个状态说明:

  • 1.New(新建),新创建的线程进过初始化,进入Runnable(就绪)状态;
  • 2.Runnable(就绪),等待线程调度,调度后进入Running(运行)状态;
  • 3.Running(运行),线程正常运行,期间可能会因为某些情况进入Blocked(堵塞)
    状态(同步锁;调用了sleep()和join()方法进入Sleeping状态;执行wait()
    方法进入Waiting状态,等待其他线程notify通知唤醒);
  • 4.Blocked(堵塞),线程暂停运行,解除堵塞后进入Runnable(就绪)状态
    重新等待调度;
  • 5.Dead(死亡):线程完成了它的任务正常结束或因异常导致终止;

3.并行与并发

并行是同时处理多个任务,而并发则是处理多个任务,而不一定要同时
并行可以说是并发的子集。


4.同步与异步

同步:线程执行某个请求,如果该请求需要一段时间才能返回信息,
那么这个线程会一直等待,直到收到返回信息才能继续执行下去;

异步:线程执行完某个请求,不需要一直等,直接继续执行后续操作,
当有消息返回时系统会通知线程进程处理,这样可以提高执行的效率;
异步在网络请求的应用非常常见~


5.线程同步安全问题

当有两个或以上线程在同一时刻访问同一资源,可能会带来一些问题,
比如:数据库表不允许插入重复数据,而线程1,2都得到了数据X,然后
线程1,2同时查询了数据库,发现没有数据X,接着两线程都往数据库中
插入了X,然后就GG啦,这就是线程的同步安全问题,而这里的数据库
资源我们又称为:临界资源(共享资源)


6.如何解决同步安全问题(同步锁)

当多个线程访问临界资源的时候,有可能会出现线程安全问题;
而基本所有并发模式在解决线程安全问题时都采用"系列化访问
临界资源"的方式,就是同一时刻,只能有一个线程访问临界资源,
也称"同步互斥访问"。通常的操作就是加锁(同步锁),当有线程访问
临界资源时需要获得这个锁,其他线程无法访问,只能等待(堵塞),
等这个线程使用完释放锁,供其他线程继续访问。


7.与锁有关的特殊情况:死锁,饥饿与活锁

有了同步锁不意味着就一了百了了,当多个进程/线程的操作涉及到了多个锁,
就可能出现下述三种情况:

  • 死锁(DeadLock)

两个或以上进程(线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,
如果无外力作用,他们将继续这样僵持下去;简单点说:两个人互相持有对方想要的资源,
然后每一方都不愿意放弃自己手上的资源,就一直那样僵持着。

死锁发生的条件

互斥条件(临界资源);
请求和保持条件(请求资源但不释放自己暂用的资源);
不剥夺条件(线程获得的资源只有线程使用完后自己释放,不能被其他线程剥夺);
环路等待条件:在死锁发生时,必然存在一个”进程-资源环形链”,t1等t2,t2等t1;

如何避免死锁

破坏四个条件中的一个或多个条件,常见的预防方法有如下两种:
有序资源分配法:资源按某种规则统一编号,申请时必须按照升序申请:
1.属于同一类的资源要一次申请完;2.申请不同类资源按照一定的顺序申请。
银行家算法:就是检查申请者对资源的最大需求量,如果当前各类资源都可以满足的
申请者的请求,就满足申请者的请求,这样申请者就可很快完成其计算,然后释放它占用
的资源,从而保证了系统中的所有进程都能完成,所以可避免死锁的发生。
理论上能够非常有效的避免死锁,但从某种意义上说,缺乏使用价值,因为很少有进程
能够知道所需资源的最大值,而且进程数目也不是固定的,往往是不断变化的,
况且原本可用的资源也可能突然间变得不可用(比如打印机损坏)。

  • 饥饿(starvation)与饿死(starve to death)

资源分配策略有可能是不公平的,即不能保证等待时间上界的存在,即使没有
发生死锁, 某些进程可能因长时间的等待,对进程推进与相应带来明显影响,
此时的进程就是 发生了进程饥饿(starvation),当饥饿达到一定程序即此时
进程即使完成了任务也 没有实际意义时,此时称该进程被饿死(starve to death),
典型的例子: 文件打印,采用短文件优先策略,如果短文件太多,长文件会一直
推迟,那还打印个毛。

  • 活锁(LiveLock)

特殊的饥饿,一系列进程轮询等待某个不可能为真的条件为真,此时进程不会
进入blocked状态, 但会占用CPU资源,活锁还有几率能自己解开,而死锁则
无法自己解开。(例子:都觉得对方优先级比自己高,相互谦让,导致无法
使用某资源),简单避免死锁的方法:先来先服务策略。


8.守护线程

也叫后台线程,是一种为其他线程提供服务的线程,比如一个简单的例子:
你有两个线程在协同的做一件事,如果有一个线程死掉,事情就无法继续
下去,此时可以引入守护线程,轮询地去判断两个线程是否或者(调isAlive()),
如果死掉就start开启线程,在Python中可以在线程初始化的时候调用
setDaemon(True)把线程设置为守护线程,如果程序中只剩下守护线程
的话会自动退出


9.线程并发的经典问题:生产中与消费者问题

说到线程并发,不得不说的一个经典问题就是:生产中与消费者问题:

两个共享固定缓冲区大小的线程,生产者线程负责生产一定量的数据
放入缓冲区, 而消费者线程则负责消耗缓冲区中的数据,关键问题是
需要保证两点:

  • 1.缓冲区满的时候,生产者不再往缓冲区中填充数据
  • 2.缓存区空的时候,消费者不在消耗缓冲区中的数据

听不懂也没什么,这个后面会写例子的~


10.Python中的GIL锁

概念

全局解释器锁,用于同步线程的一种机制,使得任何时候仅有一个线程在执行
GIL 并不是Python的特性,只是在实现Python解析器(CPython)时引入
一个概念。换句话说,Python完全可以不依赖于GIL。

Python解释器进程内的多线程是以协作多任务方式执行的,当一个线程遇到
I/O操作时会释放GIL。而依赖CPU计算的线程则是执行代码量到一定的阀值,
才会释放GIL。而在Python 3.2开始使用新的GIL,使用固定的超时时间来指示
当前线程放弃全局锁,就是:当前线程持有这个锁,且其他线程请求这个锁时,
当前线程就会再5毫秒后被强制释放掉该锁。

多线程在处理CPU密集型操作因为各种循环处理计数等,会很快达到阀值,
而多个线程来回切换是会消耗资源的,所以多线程的效率往往可能还比不上
单线程!而在多核CPU上效率会更低,因为多核环境下,持有锁的CPU释放锁后,
其他CPU上的线程都会进行竞争,但GIL可能马上又会被之前的CPU拿到拿到,
导致其他几个CPU上被唤醒后的线程会醒着等待到切换时间后又进入待调度
状态,从而造成线程颠簸(thrashing),导致效率更低。

问题

因为GIL锁的原因,对于CPU密集型操作,Python多线程就是鸡肋了?

答:是的!尽管多线程开销小,但却无法利用多核优势!
可以使用多进程来规避这个问题,Python提供了multiprocessing
这个跨平台的模块来帮助我们实现多进程代码的编写。
每个线程都有自己独立的GIL,因此不会出现进程间GIL
锁抢夺的问题,但是也增加程序实现线程间数据通讯和同步
是的成本,这个需要自行进行权衡。


11.Python中对多线程与多进程的支持

Python与线程,进程相关的官方文档: 17. Concurrent Execution
https://docs.python.org/3/library/concurrency.html

简单介绍下里面的一些模块,后面会一个个啃~

  • threading —— 提供线程相关的操作
  • multiprocessing —— 提供进程程相关的操作
  • concurrent.futures —— 异步并发模块,实现多线程和多进程的异步并发(3.2后引入)
  • subprocess —— 创建子进程,并提供链接到他们输入/输出/错误管道的方法,
    并获得他们的返回码,该模块旨在替换几个较旧的模块和功能:os.systemos.spawn*
  • sched —— 任务调度(延时处理机制)
  • queue —— 提供同步的、线程安全的队列类

还有几个是兼容模块,比如Python 2.x上用threading和Python 3.x上用thread:

  • dummy_threading:提供和threading模块相同的接口,2.x使用threading兼容;
  • _thread:threading模块的基础模块,应该尽量使用 threading 模块替代;
  • dummy_thread:提供和thread模块相同的接口,3.x使用threading兼容;

小结

本节我们围绕着线程以及进程相关的概念进行了解析,尽管有些
枯燥,但是如果坚持看完,相信你对于线程与进程的理解会更进
一步,概念什么都是虚的,纸上得来终觉浅绝知此事要躬行
下节开始我们来通过写代码的方式一一学习这些模块吧!


参考文献


来啊,Py交易啊

欢迎各种像我一样的Py初学者,或者Py大神加入,
一起愉快地交流学♂习:


今天看啥 - 高品质阅读平台
本文地址:http://www.jintiankansha.me/t/6EHZZdf465
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/5911
 
755 次点击