线程的概念
能被操作系统调度(给CPU执行)的最小单位
同一个进程中的多个线程可以同时被CPU执行
数据共享,操作系统调度的最小单位,可以利用多核,由操作系统调度,数据不安全
开启关闭时间消耗小(比进程小很多)
数据不安全
def a():
for i in range(1000000):
global count
count+=1
def b():
for i in range(1000000):
global count
count-=1
count=0
t1=Thread(target=a)
t2=Thread(target=b)
t1.start()
t2.start()
t1.join()
t2.join()
print(count)#此时的count不等于0
数据不安全的本质是因为+=操作在CPU指令层面是两个操作(计算和赋值)
在执行加法操作后GIL将CPU的权限轮转给下一个线程,当前线程会等待下次执行时间片到来然后再将计算后的值 1 赋值回a
下一个线程在取数据时就会出现问题
因为上一个线程未将实现加法后的值赋值回去所以这个线程中a还是=0
计算赋值结束后a=1就与上个线程重复了
线程的切换跟执行非常非常快,所以开启巨量线程时会导致数据不安全的问题出现
解决方法就是下文中的互斥锁Lock
Python当中的多线程
from threading import Thread
全局解释器锁 GIL (global interpreter lock)
全局解释器锁的出现是为了完成gc的回收机制,对不同线程的引用计数的变化记录更加精确
全局解释器锁导致了同一个进程中的多个线程只能有一个线程真正被执行
主要节省的是I/O操作的时间,而不是CPU计算的时间,因为CPU的计算速度非常快,部分情况下,我们没有办法把一条进程中所有的I/O操作都规避掉
Cpython解释器 无法利用多核
在Cpython解释器中的py多线程本质上还是串行
gc 垃圾回收机制 线程
引用计数 + 分代回收
pypy 解释器 gc 不能利用多核
jpython解释器 能利用多核
threading模块开启线程
def start_function(n):
print(n)
t=Thread(target=start_function,args=(n,))
#target=函数名,args=(arg1,arg2,arg3,arg4)传入的参数需为一个元组类型
t.start()#开启线程
所有线程不能杀死,只能等待执行完毕
current_thread()获取到当前线程的对象,通过 .ident可以获取到线程的id
enumerate 列表 存储了所有活着的线程对象 包括主线程
active_count() 数字 ,存储了所有活着的线程对象的个数
阻塞线程
def func():
print(1)
t=Thread(target=func).start()
t.join()#阻塞,等待线程执行完毕再接着进行主进程下面的代码
print('done')
阻塞多个或所有进程
每开启一个线程时将该线程实例化后产生的对象存入主进程的一个列表中
在所有线程开启完后,遍历线程列表,使用join方法阻塞
thread_list=[]
def func():
pass
for i in range(100):
t=Tread(target=func)
thread_list.append(t)
t.start()
for i in thread_list:
i.join()
在子线程中current_thread()可以获取到当前线程的对象,通过 .ident()可以获取到线程的id
在主进程中active_count() 返回数字 ,存储了所有活着的线程对象的个数
守护线程的概念
主进程会等待子线程结束之后才结束
守护线程随着主线程的结束而结束
守护线程会在主线程的代码结束之后继续守护其他子线程
守护线程与守护进程的区别
守护进程和守护线程的结束原理不同
守护进程需要主进程来回收资源
守护线程是随着进程的结束才结束的
其他子线程–>主线程结束–>主进程结束–>整个进程中所有的资源都被回收–>守护线程也会被回收
守护进程
守护进程 会随着主进程的代码结束而结束
如果主进程代码结束之后还有其他子进程在运行,守护进程不守护
守护线程
守护线程 随着主线程的结束而结束
如果主线程代码结束之后还有其他子线程在运行,守护线程也守护
进程是资源分配单位
子进程都需要它的父进程来回收资源
线程是进程中的资源
所有的线程都会随着进程的结束而被回收的
Python开启守护进程的方法
在实例化线程对象时添加一个daemon=True的参数即可
def a(n):
time.sleep(n)
t1= Thread(target=a,args=(10,)).start()
t2= Thread(target=a,args=(1,),daemon=True).start()
#此时t2会等待t1执行完毕才关闭
线程锁
互斥锁 Lock
from threading import Lock
当多个线程需要同时共用某一样东西时,因为线程之间数据共享且数据不安全所以需要加锁来确保数据安全
在操作数据前获取锁
在操作数据结束后释放锁
所有线程需要共用一个主进程的锁否则锁会失效
当下一个线程获取不到锁时会阻塞,等待上一个线程释放锁之后获取并运行
from threading import Lock
lock=Lock()#实例化一个锁
def a(lock):
for i in range(1000000):
global count
lock.acquire()#获取锁
count+=1
lock.release()#释放锁
def b(lock):
for i in range(1000000):
global count
lock.acquire()#获取锁
count-=1
lock.release()#释放锁
count=0
t1=Thread(target=a,args=(lock,))
t2=Thread(target=b,args=(lock,))
t1.start()
t2.start()
t1.join()
t2.join()
print(count)#此时count==0
Rlock 递归锁 (recursion lock)
当一个线程必须获得两个或多个文件的操作权限才能进行操作时,使用递归锁将所有文件锁起来防止其他线程来操作
rlock=RLock()
rlock.acquire()
rlock.release()
在同一个线程中可以被多次acquire
Lock效率比Rlock效率高
死锁现象
多把锁,并且在多个线程中交叉使用
如果是互斥锁,出现了死锁现象,最快速的解决方式是把所有锁都改成一把递归锁
程序的效率会降低
递归锁 效率低 但是解决死锁现象有奇效
互斥锁 效率高 但是多把锁容易出现死锁现象