多线程
一 threading模块介绍
multiprocess模块的完全模仿了threading模块的接口,二者在使用层面,有很大的相似性,因而不再详细介绍
官网链接:https://docs.python.org/3/library/threading.html?highlight=threading#
二 开启线程的两种方式
#方式一
from threading import Thread
import time
def task(name):
print(f'{name}task is running')
time.sleep(2)
print(f'{name}task is done')
if __name__ == '__main__':
t = Thread(target=task,args=('egon',))
t.start() # 告诉操作系统开一个线程 开启线程无需申请内存空间会非常快。
print('主')
# 注意:1 同一个进程下没有父子线程之分大家地位都一样。
# 2 右键运行发生了个什么事情? 开启了一个进程,开辟了一个内存空间,把代码都丢进去,然后运行代码(自带的主线程运行)
# 然后又开启了一个子线程。
# ps:主线程结束和子线程结束没有任何必然联系,比如主线程运行结束,子线程还在运行当中。不是主线程在等待子线程结束,是进程在等待自己的所有线程结束。
from threading import Thread
import time
class MyTread(Thread):
def run(self):
print('thread is running')
time.sleep(2)
print('thread is end')
if __name__ == '__main__':
t = MyTread()
t.start() #
print('主线程')
# 输出:
# thread is running
# 主线程
# thread is end
三 进程vs线程
1 比起进程来说线程的开启速度快
from threading import Thread
from multiprocessing import Process
import time
def task(name):
print(f"{name} is running")
time.sleep(2)
print(f"{name} is done")
if __name__ == '__main__':
t = Thread(target=task,args=('子线程',))
p = Process(target=task,args=('子进程',))
t.start()
# p.start()
print('主')
'''
开启子进程打印的效果:
>主
>子进程 is running
>子进程 is done
开启子线程打印的效果:
>子线程 is running
>主
>子线程 is done
对比的结果:从打印效果就能看的出来,开启线程要快于进程。
开启进程需要申请空间,复制代码到该空间,然后执行自带线程,非常慢。
开启子线程 基本没有资源消耗所以非常快。
'''
2 同一进程下的线程共享内存空间
from threading import Thread
import time
x = 100
def task():
global x
x = 20
if __name__ == '__main__':
t = Thread(target=task)
t.start()
time.sleep(2)
print(x) # 20
# 首先子线程修改了全局变量为20,主线程等待子线程修改完毕后打印x为20。
# 说明同一个进程下所有的线程共享同一份内存空间。
3 瞅瞅pid
from threading import Thread
import os
def task():
print('子线程',os.getpid()) # 子线程 14576
if __name__ == '__main__':
t = Thread(target=task)
t.start()
print('主线程',os.getpid()) #主线程 14576
四 线程对象join用法
主线程等待子线程运行结束。
from threading import Thread
import time
def task(name,n):
print(f'{name} is running')
time.sleep(n)
print(f'{name} is done')
if __name__ == '__main__':
t1 = Thread(target=task,args=('线程1',1))
t2 = Thread(target=task,args=('线程2',2))
t3 = Thread(target=task,args=('线程3',3))
start_time = time.time()
t1.start() #
t2.start() #
t3.start() #
t1.join() # 等1s
t2.join() # 等1s
t3.join() # 等1s
end_time = time.time()
print(end_time-start_time) # 3.0023529529571533
print('主')
'''
线程1 is running
线程1 is done
主
'''
ps:了解 对比进程的join,进程的join是当前线程在等子进程运行结束并不影响其他线程。
from multiprocessing import Process
from threading import Thread
import time
def threadtask(name):
print(f'{name} start')
time.sleep(5)
print(f'{name} end')
def processtask(name):
print(f'{name} start')
time.sleep(20)
print(f'{name} end')
if __name__ == '__main__':
t = Thread(target=threadtask,args=('子线程',))
p = Process(target=processtask,args=('子进程',))
p.start()
t.start()
p.join() # 当前线程等待当前进程下的p子进程结束,然后往下运行。
from threading import Thread
from multiprocessing import Process
import os
def work():
print('hello',os.getpid())
if __name__ == '__main__':
#part1:在主进程下开启多个线程,每个线程都跟主进程的pid一样
t1=Thread(target=work)
t2=Thread(target=work)
t1.start()
t2.start()
print('主线程/主进程pid',os.getpid())
#part2:开多个进程,每个进程都有不同的pid
p1=Process(target=work)
p2=Process(target=work)
p1.start()
p2.start()
print('主线程/主进程pid',os.getpid())
from threading import Thread
from multiprocessing import Process
import os
def work():
global n
n=0
if __name__ == '__main__':
# n=100
# p=Process(target=work)
# p.start()
# p.join()
# print('主',n) #毫无疑问子进程p已经将自己的全局的n改成了0,但改的仅仅是它自己的,查看父进程的n仍然为100
n=1
t=Thread(target=work)
t.start()
t.join()
print('主',n) #查看结果为0,因为同一进程内的线程之间共享进程内的数据
五 思考主线程是否会等子线程运行结束
import time
def task(name):
print(f'线程 start {name}')
time.sleep(3)
print('线程 end')
if __name__ == '__main__':
t = Thread(target=task,args=('egon',))
t.start() #非常快
print('主')
分析
1 其实是进程在等
貌似是主线程在原地等着,主线程已经运行完。
原来没有子线程的情况下,其实就一个主线程这一条流水线工作完了,这个进程就结束了。
那现在的情况是当前进程有其他的子线程,是进程等待自己所有的子线程运行完。
# 主进程等子进程是因为主进程要给子进程收尸
# 现在看到的等是进程必须等待其内部所有线程都运行完毕才结束。
六 线程相关的其他方法(了解)
Thread实例对象的方法
# isAlive(): 返回线程是否活动的。
# getName(): 返回线程名。
# setName(): 设置线程名。
threading模块提供的一些方法:
# threading.currentThread(): 返回当前的线程变量。
# threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
# threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
from threading import Thread
import threading
'''以后只有周日!!!'''
def task():
import time
time.sleep(3)
print(threading.current_thread().getName())
if __name__ == '__main__':
t = Thread(target=task)
t1 = Thread(target=task)
print(threading.current_thread().setName('张三')) # None
print(threading.current_thread().getName()) # 张三
t.start()
t1.start()
# 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
print(threading.enumerate()) # [<_MainThread(MainThread, started 17944)>, <Thread(Thread-1, started 19320)>, <Thread(Thread-2, started 19308)>]
print(len(threading.enumerate())) # 3
print(threading.activeCount()) # 返回正在运行的线程数量。与len(threading.enumerate())有相同的结果。
七 守护线程(了解)
# 守护线程首先是一个线程。
# 守护线程守护到当前进程运行结束。
# ps:比如有未完成的子进程阶段会守护,比如有未完成的其他子线程也均会守护。
# 守护进程首先是一个进程。
# 守护进程守护到当前进程的最后一行代码结束。
代码演示:
# 守护线程守护到当前进程结束
from threading import Thread
import threading
import time
def threadtask(name):
print(f' {name} start')
print(time.sleep(20))
print(f' {name} end')
# print(time.sleep(6))
def threadtask2(name):
print(f'{name} start')
time.sleep(10)
print(threading.enumerate()) # [<_MainThread(MainThread, stopped 14544)>, <Thread(Thread-1, started daemon 13676)>, <Thread(Thread-2, started 13148)>]
print(f'{name} end')
if __name__ == '__main__':
t = Thread(target=threadtask,args=('守护线程',))
t2 = Thread(target=threadtask2,args=('子线程',))
t.daemon = True
t.start()
t2.start()
print('主')
'''
守护线程 start
子线程 start
主
[<_MainThread(MainThread, stopped 15448)>, <Thread(Thread-1, started daemon 17520)>, <Thread(Thread-2, started 10356)>]
子线程 end
'''
可以看到当主线程已经结束的时候,其他子线程没有结束的时候打印当前的活跃的线程发现有守护线程。
八 同步锁(线程的互斥锁)
0 前言:
x = 1
def func1():
global x
print(x) # 1
func2()
print(x) # 10000 每次拿x的时候都会去全局拿到那个最新的x
x = x -1
def func2():
global x
x = 10000
func1()
# func2()
print(x) # 9999
1 多线程修改数据会造成混乱
from threading import Thread,current_thread,Lock
import time
x = 0
def task():
global x
for i in range(100000): # 最少10万级别才能看出来
x = x+1 # 有可能右边的x刚拿到了0,
# 发生线程不安全的原因:
# t1 x+1 阶段 x = 0 保存了状态 cpu被切走 t2 x+1 x = 0 保存了状态 cpu被切走
# 下次t1 继续运行代码 x = 0+1 下次t2 再运行代码的时候也是 x = 0+1
# 也就说修改了两次 x 只加了一次1 。
# time.sleep()
# lock.release()
if __name__ == '__main__':
t_list = []
for i in range(3):
t = Thread(target=task)
t_list.append(t)
t.start()
for i in t_list:
i.join()
print(x) # 99
2 使用线程锁解决线程修改数据混乱问题。
from threading import Thread,current_thread,Lock
import time
x = 0
lock = Lock()
def task():
global x
lock.acquire()
for i in range(100000): # 最少10万级别才能看出来
x = x+1 # 有可能右边的x刚拿到了0,
lock.release()
if __name__ == '__main__':
t_list = []
for i in range(3):
t = Thread(target=task)
t_list.append(t)
t.start()
for i in t_list:
i.join()
print(x) # 99
3 死锁问题
from threading import Thread,Lock
import time
mutexA=Lock()
mutexB=Lock()
class MyThread(Thread):
def run(self):
self.func1()
self.func2()
def func1(self):
mutexA.acquire()
print('\033[41m%s 拿到A锁\033[0m' %self.name)
mutexB.acquire()
print('\033[42m%s 拿到B锁\033[0m' %self.name)
mutexB.release()
mutexA.release()
def func2(self):
mutexB.acquire()
print('\033[43m%s 拿到B锁\033[0m' %self.name)
time.sleep(2)
mutexA.acquire()
print('\033[44m%s 拿到A锁\033[0m' %self.name)
mutexA.release()
mutexB.release()
if __name__ == '__main__':
for i in range(10):
t=MyThread()
t.start()
'''
Thread-1 拿到A锁
Thread-1 拿到B锁
Thread-1 拿到B锁
Thread-2 拿到A锁
然后就卡住,死锁了
'''
**解决死锁问题 **
解决方法,递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。
这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:
mutexA=mutexB=threading.RLock() #一个线程拿到锁,counter加1,该线程内又碰到加锁的情况,则counter继续加1,这期间所有其他线程都只能等待,等待该线程释放所有锁,即counter递减到0为止
九 信号量Semaphore
同进程的一样
Semaphore管理一个内置的计数器,
每当调用acquire()时内置计数器-1;
调用release() 时内置计数器+1;
计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。
实例:(同时只有5个线程可以获得semaphore,即可以限制最大连接数为5):
from threading import Thread,Semaphore
import threading
import time
def task():
sm.acquire()
print(f"{threading.current_thread().name} get sm")
time.sleep(3)
sm.release()
if __name__ == '__main__':
sm = Semaphore(5) # 同一时间只有5个进程可以执行。
for i in range(20):
t = Thread(target=task)
t.start()
与进程池是完全不同的概念,进程池Pool(4),最大只能产生4个进程,而且从头到尾都只是这四个进程,不会产生新的,而信号量是产生一堆线程/进程。
二 GIl
一 介绍
'''
定义:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple
native threads from executing Python bytecodes at once. This lock is necessary mainly
because CPython’s memory management is not thread-safe. (However, since the GIL
exists, other features have grown to depend on the guarantees that it enforces.)
'''
# 1
在Cpython解释器中有一把GIL锁,GIl锁本质是一把互斥锁。
因为GIL锁:同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势
# 2
GIL本质就是一把互斥锁,那既然是互斥锁,原理都一样,都是让多个并发线程同一时间只能
有一个执行
即:有了GIL的存在,同一进程内的多个线程同一时刻只能有一个在运行,意味着在Cpython中
一个进程下的多个线程无法实现并行===》意味着无法利用多核优势
多个线程只能并发不能并行
# 3
GIL可以被比喻成执行权限,同一进程下的所以线程 要想执行都需要先抢执行权限。
首先需要明确的一点是GIL
并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。有名的编译器例如GCC,INTEL C++,Visual C++等。Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL
归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL
二 GIL介绍
GIL本质就是一把互斥锁,既然是互斥锁,所有互斥锁的本质都一样,都是将并发运行变成串行,以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安全。
# 为何要有GIL?
因为Cpython解释器自带垃圾回收机制不是线程安全的。
# 如果不对垃圾回收机制线程做任何处理,也没有GIL锁行不行?
提示:如果是问题的这种情况,多线程和垃圾回收线程就会并发了。
可以肯定的一点是:保护不同的数据的安全,就应该加不同的锁。
要想了解GIL,首先确定一点:每次执行python程序,都会产生一个独立的进程。例如python test.py,python aaa.py,python bbb.py会产生3个不同的python进程
'''
#验证python test.py只会产生一个进程
#test.py内容
import os,time
print(os.getpid())
time.sleep(1000)
'''
python3 test.py
#在windows下
tasklist |findstr python
#在linux下
ps aux |grep python
在一个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保护的是解释器级的数据,保护用户自己的数据则需要自己加锁处理,如下图
GIL是导致python慢的元凶吗?
单核cpu同一个时刻只能执行一个线程。
而python因为有GIL的存在无法利用多核优势,即线程1在cpu核心1运行的同时不允许cpu在核心2上运行,也就是单进程下(同一个解释器)只能有同一个线程在运行。
那么如果开多进程的话跟GIL有啥关系吗还?答案是完全没有。
比如你有四个核心,你开了四个进程,每个进程里只有一个线程在执行。完全没有问题,同样能实现多核优势的效果(这跟无法利用多核优势毫无关系)。
所以说多线程更多的是为了显示出来并发的效果,而不是提升执行效率。
所以如果开多进程的话,那GIL解释器锁一点都不会影响效率。
参考:https://python3-cookbook.readthedocs.io/zh_CN/latest/c12/p09_dealing_with_gil_stop_worring_about_it.html
那么影响效率的是什么?
“它是解释型语言而非编译语言”
参考:https://www.jiqizhixin.com/articles/2018-08-14-11
cpython执行python文件第一次也会编译成为pychach字节码文件,然后解释执行。
但是他的字节码执行的效率不如java的高,java编译的时候用到里JIT,JIT允许在运行时进行优化,即执行某些代码次数越多运行该代码越快。
“它是动态类型语言”
不必声明类型不是使Python变慢的原因。Python语言的设计使我们几乎可以创建任何动态变量。我们可以在运行时替换对象中的方法,也可以胡乱地把低级系统调用赋给一个值。几乎怎么修改都可以。
正是这种设计使得优化Python变得异常困难。