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")
View Code

这段代码执行后的结果是:

  先打印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()
View Code

这段代码执行后的结果是:

  看起来同时打印了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()
View Code

无论是第一种还是第二种效果都是一样的,看个人偏好选择。

.

.

那么现在问题来了:如果我想起更多的线程,我只能一个个复制粘贴去执行吗?答案是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

运行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....")     #打印主线程,提示主线程正在运行
View Code

执行结果:

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....")     #打印主线程,提示主线程正在运行
View Code

执行结果:

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....")     #打印主线程,提示主线程正在运行
View Code

执行结果:

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)  #计算所有子线程执行时间
threading_Thread_v1.1.py

是不是和你想得一样?只要了解了原理,理解起来就不那么困难了!

现在又有人问了,既然有主线程的存在,那么我启动的第一个线程是主线程吗?

这里需要强调一下,主线程其实就是程序本身,默认情况下,我们是看不见的,但是我可以证明给你看:

#-*- 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())     #打印主线程,提示主线程正在运行
View Code

执行结果:

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

执行threading_Thread_v2.0.py,主线程一结束,不等子线程的task done,所有子线程立刻中断。

这里需要特别注意

t.setDaemon(True)  #把当前线程设置为守护线程

一定要放在

t.start()  #启动线程

之前,否则会报错。

 小结:

守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。

用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。

将线程转换为守护线程可以通过调用threading.Thread对象的setDaemon(True)方法来实现。

 

posted on 2017-07-25 09:18  奔跑的蜗牛~~  阅读(107)  评论(0)    收藏  举报

导航