本文始发于我的公众号:TechFlow,原创不易,求个关注程序员
今天是Python专题第20篇文章,咱们来聊聊Python当中的多线程。web
其实关于元类还有不少种用法,好比说如何在元类当中设置参数啦,以及一些规约的用法等等。只不过这些用法比较小众,使用频率很是低,因此咱们不过多阐述了,能够在用到的时候再去详细了解。我想只要你们理解了元类的原理以及使用方法,再去学习那些具体的用法应该会很容易。因此咱们今天开始了一个新的话题——多线程和并发。后端
为了照顾小白,咱们来简单聊聊进程和线程这两个概念。这两个概念属于操做系统,咱们常常据说,可是可能不多有人会细究它们的含义。对于工程师而言,二者的定义和区别仍是颇有必要了解清楚的。多线程
首先说进程,进程能够当作是CPU执行的具体的任务。在操做系统当中,因为CPU的运行速度很是快,要比计算机当中的其余设备要快得多。好比内存、磁盘等等,因此若是CPU一次只执行一个任务,那么会致使CPU大量时间在等待这些设备,这样操做效率很低。为了提高计算机的运行效率,把机器的技能尽量压榨出来,CPU是轮询工做的。也就是说它一次只执行一个任务,执行一小段碎片时间以后当即切换,去执行其余任务。并发
因此在早期的单核机器的时候,看起来电脑也是并发工做的。咱们能够一边听歌一边上网,也不会以为卡顿。但实际上,这是CPU轮询的结果。在这个例子当中,听歌的软件和上网的软件对于CPU而言都是独立的进程。咱们能够把进程简单地理解成运行的应用,好比在安卓手机里面,一个app启动的时候就会对应系统中的一个进程。固然这种说法不彻底准确,一个应用也是能够启动多个进程的。app
进程是对应CPU而言的,线程则更多针对的是程序。即便是CPU在执行当前进程的时候,程序运行的任务其实也是有分工的。举个例子,好比听歌软件当中,咱们须要显示歌词的字幕,须要播放声音,须要监听用户的行为,好比是否发生了切歌、调节音量等等。因此,咱们须要进一步拆分CPU的工做,让它在执行当前进程的时候,继续经过轮询的方式来同时作多件事情。编辑器
进程中的任务就是线程,因此从这点上来讲,进程和线程是包含关系。一个进程当中能够包含多个线程,对于CPU而言,不能直接执行线程,一个线程必定属于一个进程。因此咱们知道,CPU进程切换切换的是执行的应用程序或者是软件,而进程内部的线程切换,切换的是软件当中具体的执行任务。函数
关于进程和线程有一个经典的模型能够说明它们之间的关系,假设CPU是一家工厂,工厂当中有多个车间。不一样的车间对应不一样的生产任务,有的车间生产汽车轮胎,有的车间生产汽车骨架。可是工厂的电力是有限的,同时只能知足一个厂房的使用。oop
为了让你们的进度协调,因此工厂个须要轮流提供各个车间的供电。这里的车间对应的就是进程。学习
一个车间虽然只生产一种产品,可是其中的工序却不止一个。一个车间可能会有好几条流水线,具体的生产任务实际上是流水线完成的,每一条流水线对应一个具体执行的任务。可是一样的,车间同一时刻也只能执行一条流水线,因此咱们须要车间在这些流水线之间切换供电,让各个流水线生产进度统一。
这里车间里的流水线天然对应的就是线程的概念,这个模型很好地诠释了CPU、进程和线程之间的关系。实际的原理也的确如此,不过CPU中的状况要比现实中的车间复杂得多。由于对于进程和CPU来讲,它们面临的局面都是实时变化的。车间当中的流水线是x个,下一刻可能就成了y个。
了解完了线程和进程的概念以后,对于理解电脑的配置也有帮助。好比咱们买电脑,常常会碰到一个术语,就是这个电脑的CPU是某某核某某线程的。好比我当年买的第一台笔记本是4核8线程的,这实际上是在说这台电脑的CPU有4个计算核心,可是使用了超线程技术,使得能够把一个物理核心模拟成两个逻辑核心。至关于咱们能够用4个核心同时执行8个线程,至关于8个核心同时执行,但其实有4个核心是模拟出来的虚拟核心。
有一个问题是为何是4核8线程而不是4核8进程呢?由于CPU并不会直接执行进程,而是执行的是进程当中的某一个线程。就好像车间并不能直接生产零件,只有流水线才能生产零件。车间负责的更可能是资源的调配,因此教科书里有一句很是经典的话来诠释:进程是资源分配的最小单元,线程是CPU调度的最小单元。
Python当中为咱们提供了完善的threading库,经过它,咱们能够很是方便地建立线程来执行多线程。
首先,咱们引入threading中的Thread,这是一个线程的类,咱们能够经过建立一个线程的实例来执行多线程。
from threading import Thread
t = Thread(target=func, name='therad', args=(x, y)) t.start() 复制代码
简单解释一下它的用法,咱们传入了三个参数,分别是target,name和args,从名字上咱们就能够猜想出它们的含义。首先是target,它传入的是一个方法,也就是咱们但愿多线程执行的方法。name是咱们为这个新建立的线程起的名字,这个参数能够省略,若是省略的话,系统会为它起一个系统名。当咱们执行Python的时候启动的线程名叫MainThread,经过线程的名字咱们能够作区分。args是会传递给target这个函数的参数。
咱们来举个经典的例子:
import time, threading
# 新线程执行的代码: def loop(n): print('thread %s is running...' % threading.current_thread().name) for i in range(n): print('thread %s >>> %s' % (threading.current_thread().name, i)) time.sleep(5) print('thread %s ended.' % threading.current_thread().name) print('thread %s is running...' % threading.current_thread().name) t = threading.Thread(target=loop, name='LoopThread', args=(10, )) t.start() print('thread %s ended.' % threading.current_thread().name) 复制代码
咱们建立了一个很是简单的loop函数,用来执行一个循环来打印数字,咱们每次打印一个数字以后这个线程会睡眠5秒钟,因此咱们看到的结果应该是每过5秒钟屏幕上多出一行数字。
咱们在Jupyter里执行一下:
表面上看这个结果没毛病,可是其实有一个问题,什么问题呢?输出的顺序不太对,为何咱们在打印了第一个数字0以后,主线程就结束了呢?另一个问题是,既然主线程已经结束了,为何Python进程没有结束, 还在向外打印结果呢?
由于线程之间是独立的,对于主线程而言,它在执行了t.start()以后,并不会停留,而是会一直往下执行一直到结束。若是咱们不但愿主线程在这个时候结束,而是阻塞等待子线程运行结束以后再继续运行,咱们能够在代码当中加上t.join()这一行来实现这点。
t.start()
t.join() print('thread %s ended.' % threading.current_thread().name) 复制代码
join操做可让主线程在join处挂起等待,直到子线程执行结束以后,再继续往下执行。咱们加上了join以后的运行结果是这样的:
这个就是咱们预期的样子了,等待子线程执行结束以后再继续。
咱们再来看第二个问题,为何主线程结束的时候,子线程还在继续运行,Python进程没有退出呢?这是由于默认状况下咱们建立的都是用户级线程,对于进程而言,会等待全部用户级线程执行结束以后才退出。这里就有了一个问题,那假如咱们建立了一个线程尝试从一个接口当中获取数据,因为接口一直没有返回,当前进程岂不是会永远等待下去?
这显然是不合理的,因此为了解决这个问题,咱们能够把建立出来的线程设置成守护线程。
守护线程即daemon线程,它的英文直译实际上是后台驻留程序,因此咱们也能够理解成后台线程,这样更方便理解。daemon线程和用户线程级别不一样,进程不会主动等待daemon线程的执行,当全部用户级线程执行结束以后即会退出。进程退出时会kill掉全部守护线程。
咱们传入daemon=True参数来将建立出来的线程设置成后台线程:
t = threading.Thread(target=loop, name='LoopThread', args=(10, ), daemon=True)
复制代码
这样咱们再执行看到的结果就是这样了:
这里有一点须要注意,若是你在jupyter当中运行是看不到这样的结果的。由于jupyter自身是一个进程,对于jupyter当中的cell而言,它一直是有用户级线程存活的,因此进程不会退出。因此想要看到这样的效果,只能经过命令行执行Python文件。
若是咱们想要等待这个子线程结束,就必须经过join方法。另外,为了预防子线程锁死一直没法退出的状况, 咱们还能够在joih当中设置timeout,即最长等待时间,当等待时间到达以后,将再也不等待。
好比我在join当中设置的timeout等于5时,屏幕上就只会输出5个数字。
另外,若是没有设置成后台线程的话,设置timeout虽然也有用,可是进程仍然会等待全部子线程结束。因此屏幕上的输出结果会是这样的:
虽然主线程继续往下执行而且结束了,可是子线程仍然一直运行,直到子线程也运行结束。
关于join设置timeout这里有一个坑,若是咱们只有一个线程要等待还好,若是有多个线程,咱们用一个循环将它们设置等待的话。那么主线程一共会等待N * timeout的时间,这里的N是线程的数量。由于每一个线程计算是否超时的开始时间是上一个线程超时结束的时间,它会等待全部线程都超时,才会一块儿终止它们。
好比我这样建立3个线程:
ths = []
for i in range(3): t = threading.Thread(target=loop, name='LoopThread' + str(i), args=(10, ), daemon=True) ths.append(t) for t in ths: t.start() for t in ths: t.join(2) 复制代码
最后屏幕上输出的结果是这样的:
全部线程都存活了6秒,不得不说,这个设计有点坑,和咱们预想的彻底不同。
在今天的文章当中,咱们一块儿简单了解了操做系统当中线程和进程的概念,以及Python当中如何建立一个线程,以及关于建立线程以后的相关使用。今天介绍的只是最基础的使用和概念,关于线程还有不少高端的用法,咱们将在后续的文章当中和你们分享。
多线程在许多语言当中都是相当重要的,许多场景下一定会使用到多线程。好比web后端,好比爬虫,再好比游戏开发以及其余全部须要涉及开发ui界面的领域。由于凡是涉及到ui,必然会须要一个线程单独渲染页面,另外的线程负责准备数据和执行逻辑。所以,多线程是专业程序员绕不开的一个话题,也是必定要掌握的内容之一。
今天的文章就到这里,若是喜欢本文,能够的话,请点个关注,给我一点鼓励,也方便获取更多文章。
本文使用 mdnice 排版