python编程入门后学习笔记四——线程和进程
本节笔记
1.什么是进程?什么是线程?
2.threading 多线程控制和处理
1.什么是进程?什么是线程?
看过一篇介绍进程与线程的文章写得特别通俗易懂,这里分享下http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html,如有侵权,请联系。
进程:例如 QQ,以一个整体的形式暴露给操作系统管理,里面包含对各种资源的调用,内存的管理,网络接口的调用等等;这种对各种资源管理的集合,就称为进程。
线程:是操作系统最小的调度单位,是一串指令的集合。
进程与线程的关系:
1)进程 要操作cpu,必须要先创建一个线程。一个进程想要执行,至少需要有一个线程执行。
2)同一个进程里的所有线程是共享同一块内存空间的。
进程与线程的区别:
1)线程共享内存空间,进程的内存是独立的。
2)同一个进程里的线程之间可以直接通信(交流),而进程是独立的,两个进程想通信,必须通过一个中间代理实现。
3)创建新线程是很简单的,但创建新进程却需要对其父进程进行一次克隆。
4)一个线程可以控制和操作同一进程里的其他线程,而进程只能控制和操作子进程,不能控制和操作其他进程。
2.threading 多线程控制和处理
在CPython中,无论你启动多少个线程,你有多少个cpu,Python在执行时,都遵循“同一时刻只允许一个线程运行”的原则。
那还怎么能叫做“多线程”?
所以我的理解,更贴切的叫法应该是“多并发”
先直接看代码,然后再回头理解上面文字的意思。
1)threading.Thread
threading.Thread是threading模块中最重要的类之一,用来创建线程。
有两种方式来创建线程:
第一种是通过继承Thread类,重写它的run方法;
第二种是创建一个threading.Thread对象,在它的初始化函数(__init__)中将可调用对象作为参数传入。
为了便于理解threading.Thread的用法及用途,我们先按照第一种方法思路,看看下面这两段代码:
import time def run(n): print("task",n) time.sleep(2) run("t1") run("t2")
这段代码执行后的结果是:
先打印task t1,隔了2秒后打印task t2,又了2秒后,程序结束。
import threading import time def run(n): print("task",n) time.sleep(2) t1=threading.Thread(target=run,args=("t1",)) t2=threading.Thread(target=run,args=("t2",)) t1.start() t2.start()
这段代码执行后的结果是:
看起来同时打印了task t1和task t2,接着等待了2秒,程序结束。
小结一下:第二段代码就是引入了多线程多并发的理念。
好了,不绕了,按照第二种(面向对象编程)的思路,我们再重写下代码:
#-*- coding:utf-8 -*- #Author:'Yang' import threading import time class MyThread(threading.Thread): def __init__(self,n): super(MyThread,self).__init__() #继承父类的初始化 self.n=n def run(self): print("runing task",self.n) time.sleep(2) if __name__=="__main__": t1=MyThread("t1") t2=MyThread("t2") t1.start() t2.start()
无论是第一种还是第二种效果都是一样的,看个人偏好选择。
.
.
那么现在问题来了:如果我想起更多的线程,我只能一个个复制粘贴去执行吗?答案是No。
比如,我想启动20个线程,我可以写个for循环就可以了。
#-*- coding:utf-8 -*- #Author:'Yang' import threading import time '''run函数:打印任务后,等待2秒,任务结束''' def run(n): print("task",n) time.sleep(2) print("task %s done" %n) start_time=time.time() #子线程开始时间 for i in range(20): t=threading.Thread(target=run,args=("t-%s" %i,)) #子线程 t.start() #启动子线程 end_time=time.time() #子线程结束时间 print("all threads finished".center(50,"*")) print("cost:",end_time-start_time) #计算所有子线程执行时间
运行threading_Thread_v1.0.py,我的本意是希望打印出所有线程花费的总时间,
task t-0 task t-1 task t-2 task t-3 task t-4 task t-5 task t-6 task t-7 task t-8 task t-9 task t-10 task t-11 task t-12 task t-13 task t-14 task t-15 task t-16 task t-17 task t-18 task t-19 *************all threads has finished************* cost: 0.006999969482421875 task t-1 done task t-2 done task t-5 done task t-6 done task t-9 done task t-10 done task t-0 done task t-4 done task t-3 done task t-7 done task t-8 done task t-12 done task t-11 done task t-16 done task t-19 done task t-15 done task t-18 done task t-14 done task t-13 done task t-17 done
可是,实际打印的时间并不是我所预期的时间值范围(>2秒),why?
分析原因,发现:
程序本身就是一个线程(也就是主线程),主线程和它启动的子线程是并行的(而不是串行的),每 t.start() 一次,就启动了一个新的分支,这个新的分支(子线程)是独立运行的,它走的速度跟主线程没有直接关系(当然,排除主线程意外中断的情况)。
这里主线程没等子线程结束就并行着在往下走,所打印的时间仅仅是启动所有线程并打印第一个语句花费的时间,并没等待所有线程完成,所以就出现了上面的执行结果。
但是,现在我就是想获取所有线程花费的总时间,怎么做呢?
思路是:只要我等所有线程结束时,再获取当前时间就可以了,但是问题是我怎么知道所有线程结束了?
这里就需要引入 t.join(),意思是 t 这个进程没结束,我就一直等他结束,再往后执行。
先看一段代码:
#-*- coding:utf-8 -*- import threading import time '''run函数:打印任务后,等待一段时间(单位为秒),任务结束''' def run(n,sleep_time): print("task",n) time.sleep(sleep_time) print("task %s done" %n) t1=threading.Thread(target=run,args=("t1",2,)) #子线程1 t2=threading.Thread(target=run,args=("t2",4,)) #子线程2 t1.start() #启动子线程1 t2.start() #启动子线程2 print("main thread....") #打印主线程,提示主线程正在运行
执行结果:
task t1
task t2
main thread....
task t1 done
task t2 done
加入join等待t1进程结束,看一下改进的代码:
#-*- coding:utf-8 -*- import threading import time '''run函数:打印任务后,等待一段时间(单位为秒),任务结束''' def run(n,sleep_time): print("task",n) time.sleep(sleep_time) print("task %s done" %n) t1=threading.Thread(target=run,args=("t1",2,)) #子线程1 t2=threading.Thread(target=run,args=("t2",4,)) #子线程2 t1.start() #启动子线程1 t2.start() #启动子线程2 t1.join() #等待子线程1,直到子线程1结束后,继续往下走 print("main thread....") #打印主线程,提示主线程正在运行
执行结果:
task t1
task t2
task t1 done
main thread....
task t2 done
解释一下:上下两端代码的差异,第二段代码仅仅是在子线程1和子线程2启动后加入了一句 t1.join(),使得程序在执行时,必须等待子线程1执行结束,然后继续走主线程,子线程2也继续走;
而第一段代码,在没有加join语句等待时,子线程1和子线程2启动后,继续走主线程,然后由于子线程1 sleep 2秒,所以先结束,子线程1 sleep 4秒,后结束,最后程序完成(主线程结束)。
那再来看第三段代码:
#-*- coding:utf-8 -*- import threading import time '''run函数:打印任务后,等待一段时间(单位为秒),任务结束''' def run(n,sleep_time): print("task",n) time.sleep(sleep_time) print("task %s done" %n) t1=threading.Thread(target=run,args=("t1",2,)) #子线程1 t2=threading.Thread(target=run,args=("t2",4,)) #子线程2 t1.start() #启动子线程1 t2.start() #启动子线程2 t1.join() #等待子线程1 t2.join() #等待子线程2 print("main thread....") #打印主线程,提示主线程正在运行
执行结果:
task t1
task t2
task t1 done
task t2 done
main thread....
第三段代码,task t1 done 后又过了2秒打印 task t2 done,最后打印 main thread...
注意!!!这里千万不要写成:
t1.start()
t1.join()
t2.start()
t2.join()
因为这样写就完全是串行的了,就失去了并发的意义了!
.
.
铺垫了这么多,回到 threading_Thread_v1.0.py 怎么改进才能获取所有线程执行时间的问题上来,有人说加循环等待,怎么加?加哪里?
#-*- coding:utf-8 -*- #Author:'Yang' import threading import time '''run函数:打印任务后,等待2秒,任务结束''' def run(n): print("task",n) time.sleep(2) print("task %s done" %n) start_time=time.time() #子线程开始时间 t_objs=[] #定义一个空列表,存储线程实例 for i in range(20): t=threading.Thread(target=run,args=("t-%s" %i,)) #子线程 t.start() #启动子线程 t_objs.append(t) #为了不阻塞后面线程的启动,不在这里join,先将子线程装进列表中 '''循环线程实例列表,等待所有线程执行完毕''' for t in t_objs: t.join() end_time=time.time() #子线程结束时间 print("all threads finished".center(50,"*")) print("cost:",end_time-start_time) #计算所有子线程执行时间
是不是和你想得一样?只要了解了原理,理解起来就不那么困难了!
现在又有人问了,既然有主线程的存在,那么我启动的第一个线程是主线程吗?
这里需要强调一下,主线程其实就是程序本身,默认情况下,我们是看不见的,但是我可以证明给你看:
#-*- coding:utf-8 -*- import threading import time '''run函数:打印任务后,等待一段时间(单位为秒),任务结束''' def run(n,sleep_time): print("task",n,threading.current_thread()) time.sleep(sleep_time) print("task %s done" %n) t1=threading.Thread(target=run,args=("t1",2,)) #子线程1 t2=threading.Thread(target=run,args=("t2",4,)) #子线程2 t1.start() #启动子线程1 t2.start() #启动子线程2 t1.join() #等待子线程1 t2.join() #等待子线程2 print("main thread....",threading.current_thread()) #打印主线程,提示主线程正在运行
执行结果:
task t1 <Thread(Thread-1, started 10944)>
task t2 <Thread(Thread-2, started 13436)>
task t1 done
task t2 done
main thread.... <_MainThread(MainThread, started 11504)>
解释一下:<_MainThread(MainThread, started 11504)>中MainThread就是主线程的意思,<Thread(Thread-1, started 10944)>中Thread-1就是子线程1。
也就是说这段代码执行过程中实际上有3个线程在活动,
threading.active_count()
可以获取当前的活动线程数。
===================================================================================
上面说的情况基本上都是,子线程与主线程并发进行,如果子线程执行耗费时间更久,即使主线程结束了,子线程仍然会继续往下走。
但是,实际应用中,会有这样的需要:一旦主线程执行结束,子线程也要立刻中断。
这种情况,我们就需要将子线程设置为守护线程。
直接上代码,演示下效果:
#-*- coding:utf-8 -*- #Author:'Yang' import threading import time '''run函数:打印任务后,等待2秒,任务结束''' def run(n): print("task",n) time.sleep(2) print("task %s done" %n) start_time=time.time() #子线程开始时间 for i in range(20): t=threading.Thread(target=run,args=("t-%s" %i,)) #子线程 t.setDaemon(True) t.start() #启动子线程 end_time=time.time() #子线程结束时间 print("all threads finished".center(50,"*")) print("cost:",end_time-start_time) #计算所有子线程执行时间
执行threading_Thread_v2.0.py,主线程一结束,不等子线程的task done,所有子线程立刻中断。
这里需要特别注意:
t.setDaemon(True) #把当前线程设置为守护线程
一定要放在
t.start() #启动线程
之前,否则会报错。
小结:
守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。
用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。
将线程转换为守护线程可以通过调用threading.Thread对象的setDaemon(True)方法来实现。
浙公网安备 33010602011771号