守护线程
守护进程(守护线程)会等待主进程(主线程)运行完毕后被销毁
运行完毕并非终止运行:
1.对主进程来说:运行完毕指的是主进程代码运行完毕
2.对主线程来说:运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
详细解释:
1.主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵 尸进程),才会结束。
2.主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程 必须保证非守护线程都运行完毕后才能结束。
from threading import Thread import time def sayhi(name): time.sleep(2) print('%s say hello' %name) if __name__ == '__main__': t=Thread(target=sayhi,args=('egon',)) # t.setDaemon(True) #必须在t.start()之前设置
t.daemon=True t.start() print('主线程') print(t.is_alive()) ''' 打印结果 主线程 True '''
# 思考下述代码的执行结果有可能是哪些情况?为什么?
from threading import Thread import time def foo(): print(123) time.sleep(1) print("end123") def bar(): print(456) time.sleep(3) print("end456") if __name__ == '__main__': t1=Thread(target=foo) t2=Thread(target=bar) t1.daemon=True t1.start() t2.start() print("main-------") #必须等到非守护线程执行完毕,才结束
''' 打印结果: 123 456 main------- end123 end456 '''
互斥锁
在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势。
GIL并不是Python的特性,Python完全可以不依赖于GIL。像其中的JPython就没有GIL。
互斥锁:
原理:将并行变成串行
精髓:局部串行,只针对共享数据修改 保护不同的数据就应该用不用的锁
''' 互斥锁可以保证程序的安全性提高,但是效率下降 '''
from threading import Thread,Lock import time n = 100
def task(): global n mutex.acquire() # 99个线程都停在这等待抢锁
temp = n time.sleep(0.1) # 没加锁之前 睡了0.1秒,100个线程都停到这了
n=temp-1 mutex.release() if __name__ == '__main__': mutex=Lock() t_l=[] for i in range(100): t = Thread(target=task) t_l.append(t) t.start() for t in t_l: t.join() print('主线程',n) ''' 打印结果: 主线程 0 '''
GIL(global interpreter lock)
GIL本身就是互斥锁,在一个python的进程内,不仅有test.py的主线程或者由该主线程开启的其他线程,还有解释器开启的垃圾回收等解释器级别的线程,总之,所有线程都运行在这一 个进程内,毫无疑问
1.所有数据都是共享的,这其中,代码作为一种数据也是被所有线程共享的(test.py的所有代码以及Cpython解释器的所有代码)
例如:test.py定义一个函数work(代码内容如下图),在进程内所有线程都能访问到work的代码,于是我们可以开启三个线程然后target都指向该代码,能访问 到意味着就是可以执行
2.所有线程的任务,都需要将任务的代码当做参数传给解释器的代码去执行,即所有的线程要想运行自己的任务,首先需要解决的是能够访问到解释器的代码
执行流程:
target=work
多个线程先访问到解释器的代码,即拿到执行权限,然后将target的代码交给解释器的代码去执行
解释器的代码是所有线程共享的,所以垃圾回收线程也可能访问到解释器的代码而去执行,这就导致了一个问题:对于同一个数据100,可能线程1执行x=100的同时,而垃圾回收执行的是回收100的操作,解决这种问题没有什么高明的方法,就是加锁处理,如下图的GIL,保证python解释器同一时间只能执行一个任务的代码
GIL与Lock
GIL: 是解释器级别的锁,用来保护解释器相关的数据,例如:回收垃圾
Lock: 是应用程序的锁,用来保护应用程序相关的数据
分析:
1.100个线程去抢GIL锁,即抢执行权限 2.肯定有一个线程先抢到GIL(暂且称为线程1),然后开始执行,一旦执行就会拿到lock.acquire() 3.极有可能线程1还未运行完毕,就有另外一个线程2抢到GIL,然后开始运行,但线程2发现互斥锁lock还未被线程1释放,于是阻塞,*交出执行权限,即释放GIL 4.直到线程1重新抢到GIL,开始从上次暂停的位置继续执行,直到正常释放互斥锁lock,然后其他的线程再重复2 3 4的过程
GIL与多线程:
1.python的进程可以利用多核,但是开销大;
2.python的多线程开销小,但却无法利用多核的优势;(在python中一个进程中同一时刻只有一个线程执行用不上多核,有GIL锁)
共识:
1、cpu到底是用来做计算的,还是用来做I/O的? 计算的
2、多cpu,意味着可以有多个核并行完成计算,所以多核提升的是计算性能。
3、每个cpu一旦遇到I/O阻塞,仍然需要等待,所以多核对I/O操作没什么用处。
结论:
1、对计算来说,cpu越多越好,但是对于I/O来说,再多的cpu也没用
2、当然对运行一个程序来说,随着cpu的增多执行效率肯定会有所提高(不管提高幅度多大,总会有所提高),这是因为一个程序基本上不会是纯计算或者纯I/O,
所以我们只能相对的去看一个程序到底是计算密集型还是I/O密集型,从而进一步分析python的多线程到底有无用武之地
假设:
有四个任务要处理:玩出并发的效果;
方案:
方案一:开启四个进程
方案二:一个进程下,开启四个线程
单核:
如果四个任务是计算密集型,没有多核来并行计算,方案一徒增了创建进程的开销,方案二胜
如果四个任务是I/O密集型,方案一创建进程的开销大,且进程的切换速度远不如线程,方案二胜
多核:
如果四个任务是计算密集型,多核意味着并行计算,在python中一个进程中同一时刻只有一个线程执行用不上多核,方案一胜
如果四个任务是I/O密集型,再多的核也解决不了I/O问题,方案二胜
结论:
现在的计算机基本上都是多核,python对于计算密集型的任务开多线程的效率并不能带来多大性能上的提升,甚至不如串行(没有大量切换),
但是,对于IO密集型的任务效率还是有显著提升的。
应用:
多线程用于IO密集型,如:socket,爬虫,web
多进程用于计算密集型,如金融分析
# 如果并发的多个任务是计算密集型:多进程效率高
# 如果并发的多个任务是I/O密集型:多线程效率高
#计算密集型:应该用多进程 # from multiprocessing import Process # from threading import Thread # import os,time # def work(): # res=0 # for i in range(10000000): # res*=i # # # if __name__ == '__main__': # l=[] # print(os.cpu_count()) #本机为4核 # start=time.time() # for i in range(4): # # p=Process(target=work) #耗时3s多 # p=Thread(target=work) #耗时9s多 # l.append(p) # p.start() # for p in l: # p.join() # stop=time.time() # print('run time is %s' %(stop-start)) # IO密集型:用多线程 from multiprocessing import Process from threading import Thread import threading import os,time def work(): time.sleep(2) if __name__ == '__main__': l=[] print(os.cpu_count()) #本机为4核 start=time.time() for i in range(40): # p=Process(target=work) #耗时5.4s多,大部分时间耗费在创建进程上 p=Thread(target=work) #耗时2.0s多 l.append(p) p.start() for p in l: p.join() stop=time.time() print('run time is %s' %(stop-start))