Python语言线程概念捋一捋
小标 2018-12-20 来源 : 阅读 871 评论 0

摘要:本文主要向大家介绍了Python语言线程概念捋一捋,通过具体的内容向大家展示,希望对大家学习Python语言有所帮助。

本文主要向大家介绍了Python语言线程概念捋一捋,通过具体的内容向大家展示,希望对大家学习Python语言有所帮助。

引言

从刚开始学习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;<br />数据是分开的,同步简单    共享进程数据,数据共享简单,<br />但也是因为这个原因导致同步复杂    

内存、CPU    占用内存多,切换复杂,CPU利用率低    占用内存少,切换简单,CPU利用率高    

创建销毁、切换    创建销毁、切换复杂,速度慢    创建销毁、切换简单,速度很快    

编程、调试    编程简单,调试简单    编程复杂,调试复杂    

可靠性    进程间不会互相影响    一个线程挂掉将导致整个进程挂掉    

分布式    适应于多核、多机分布式;如果一台<br />机器不够,扩展到多台机器比较简单    适应于多核分布式    

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.system与**os.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兼容;

本文由职坐标整理并发布,希望对同学们学习Python有所帮助,更多内容请关注职坐标编程语言Python频道!

本文由 @小标 发布于职坐标。未经许可,禁止转载。
喜欢 | 1 不喜欢 | 0
看完这篇文章有何感觉?已经有1人表态,100%的人喜欢 快给朋友分享吧~
评论(0)
后参与评论

您输入的评论内容中包含违禁敏感词

我知道了

助您圆梦职场 匹配合适岗位
验证码手机号,获得海同独家IT培训资料
选择就业方向:
人工智能物联网
大数据开发/分析
人工智能Python
Java全栈开发
WEB前端+H5

请输入正确的手机号码

请输入正确的验证码

获取验证码

您今天的短信下发次数太多了,明天再试试吧!

提交

我们会在第一时间安排职业规划师联系您!

您也可以联系我们的职业规划师咨询:

小职老师的微信号:z_zhizuobiao
小职老师的微信号:z_zhizuobiao

版权所有 职坐标-一站式IT培训就业服务领导者 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
 沪公网安备 31011502005948号    

©2015 www.zhizuobiao.com All Rights Reserved

208小时内训课程