你好,我是goldsunC
让我们一起进步吧!
基本知识
在Python中有一个全局解释器锁GIL(Global Interpreter Lock)。GIL源于Python设计之初的考虑,目的是使数据更加安全。现在我们见到的各种电脑基本上都是多核CPU的,多核CUP比单核CPU性能要更高,为了更好的利用多核处理器的性能就出现了多线程的编程方式,而随之带来的就是线程间数据一致性和状态同步的困难。解决多线程之间数据一致性、完整性和状态同步的最简单的方法就是加锁,于是就有了GIL这把超级大锁,每一个单独的进程拥有唯一的GIL锁。
对CPU而言,在同一时间只能执行一个线程,在单核CPU下的多线程执行方式其实都是并发(Concurrent),而不是并行(Parallel)。并发和并行宏观上表现都为同时处理多路请求的情况,但它们具有本质区别。并发是指一个时间段内有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个CPU内核上运行,且任一时间点有且仅有一个程序在内核上运行。
并行是指几个程序在同一时刻同时在不同内核上运行。
举个例子来讲,一个进程代表一个工人,线程指工人做的工作,多线程就是一个工人同时干几样工作,但是工作时同时只能干一个工作却能快速的切换,比如他要洗衣服、做饭、扫地,你可以理解为他是按顺序来做这三件事,但是当做每件事的时间用时非常少的时候,宏观上就表现为他同时把三件事完成了。这样说你可能还不太理解,如果我们假设他需要做的工作为网购、洗衣服、做饭,假设他先去洗衣服,当把洗衣服放进洗衣机打开之后,多线程讲使他在此之间可以去做饭和网购,而不需要等洗衣机把衣服洗好之后才能去做饭和网购,这也就是我们所说的IO型代码。
但多进程就不一样了,每个进程代表一个工人,假如你电脑为8核CPU,那么你可以开8个进程也就等于雇了8个工人,他们8个就可以真正同时干8件工作了。
在Python多线程下,每个线程执行方式如下:1. 先获取GIL
2. 执行代码直到休眠或者Python虚拟机将其挂起
3. 释放GIL
每个线程想要执行,必须拿到所在进程的GIL,我们可以把每个进程都看成有一把大锁将其锁着,只有拿到GIL这把唯一钥匙才能运行,因此每个进程同一时刻仅运行一个线程执行。在Python3中,GIL使用计数器进行控局,即若有一个线程执行到一定时间后还没执行完毕,那么它的GIL将被强制收回分配给别的线程。而实际上,每次“拿锁放锁”是会消耗系统资源的,因此有时候使用多线程并不会提高效率,反而会降低效率。分类说明:CPU密集型代码
这种代码是指如各种循环处理、计算等,需要CPU的工作量大,且执行时间长,程序执行时很快就会达到计时器阈值,然后GIL再被收回和重新释放,GIL反复收回释放会更加大系统资源消耗,这种情况下使用多线程是不太好的。
IO密集型代码
这种代码如文件处理、网络爬虫等,如你在爬取一些视频,在下载的时候会有IO等待,此时单线程的话会等待其下载完才能开始下载另一部视频,而在视频下载的时候,CPU是不工作的,这就造成了时间资源浪费,假如使用多线程就可以在这一部视频下载的时候CPU去处理开始下载别的视频,从而提高程序的运行效率。
一般来说多核的多线程比单核多线程效率更低,因为当单核多线程每次释放GIL的时候,下一个线程能直接获取到GIL,能够无缝执行,当多核环境中某个CPU释放GIL后,本该在其它CPU的线程也都会竞争获得此CPU的GIL,但很大可能GIL又被此CPU下的某个线程拿到,导致其它几个CPU上被唤醒的线程醒着等待到切换时间后又进入调度状态,这样会造成线程颠簸(Thrashing),导致效率更低。而多进程的每个进程有各自独立的GIL,这才保证程序真正的并行执行,因此多进程效率一般来说效率更高。
有一句话说的很好,进程是线程的容器。
多线程
详细方法
在Python3中有两个模块可以创建多线程,分别是:_thread
threading
_thread模块只提供了简单的线程和锁的支持,而threading提供了更高级的、更完整的线程管理,因此推荐使用threading模块。
下面给出关于threading模块的一些使用方法(后文将会给出一些重要方法的使用示例):对象作用
Thread类是threading模块中非常重要和常用的功能,下面给出其属性和方法:Thread类的属性名称作用
守护线程是一个很重要的概念,对于一个程序而言,至少拥有一个线程即主线程,如果你新建了一个线程,并且把它设置成了守护线程,那么在主线程执行结束的时候,不管新建的线程是否执行结束都会被强制结束,例如假如你打开了一个游戏,你操控着主线程,而背景音乐也是一个单独的被创建的线程,在后台背景音乐是个死循环不会执行结束,但是当你只要把游戏关了,就是说背景音乐这个线程会随着主线程而结束。如果不是守护线程的话,它会继续执行,整个程序也就不会结束。
#可以这样让一个线程变为守护线程
Thread1.daemon = TrueThread类的方法le data-draft-node="block" data-draft-type="table" data-size="normal" data-row-style="normal">
创建一个线程一般用两种方法:创建Thread的一个实例,传入函数
创建Thread的一个实例,传递一个可调用的类实例
派生的Thread的一个子类
在创建线程时一般根据实际需要选择创建方法,第一种和第三种方法使用较多ÿ