并发:是指系统具有处理多个任务(动作)的能力
并行:是指系统具有同时处理多个任务(动作)的能力
根据定义,可以得出并行是并发的子集。
同步:当进程执行到一个IO(等待外部数据)的时候 ------等:同步
异步 -------不等:异步
无论你启动多少个线程,你有多少个cpu,Python在执行的时候在同同一时刻只允许一个线程运行
假设有三个线程add,mul和main,因为GIL造成他们执行时是必须相互竞争来回切换运行线程,无法并发,当涉及到IO操作就会切换,你是不是有个疑惑,那当初为什么要搞这个GIL,这是因为是要在解释器层面保证线程安全,JAVA虽然没有GIL,但是在用户层面需要加锁来保证线程安全,这也需要消耗大量资源。
那该怎么办呢?解锁GIL吗,在实践证明,解锁后效率更低,因为前人开发的模块都是在GIL基础上开发的,当去掉锁后效率自然会降低。这里给大家介绍一种好办法:
多进程+协程
后面会具体讲解该方法
线程任务可分为:
任务:IO密集型
计算密集型
对于IO密集型的任务:Python的多线程有意义,可以采用多进程+协程。
对计算密集型的任务:python多线程不推荐,python就不适用了。
先看下面例子
def add():
global num
temp = num
time.sleep(0.1)
num = temp+1
num = 0
import threading
import time
l= []
for i in range(100):
t1 = threading.Thread(target=add)
t1.start()
l.append(t1)
for t in l:
t.join()
print(num)
照正常情况下num最后应该是100,但是结果却不是100,这是为什么呢,因为当线程执行到sleep处,睡眠,由于sleep涉及到IO操作,所以此时线程会切换,切换到的线程运行到sleep处也睡眠切换,结果到时很多个线程堆积到temp=num那里,所以导致temp大多都是0,可想而知,最终结果也就不为100了。那有什么解决办法吗?当然有的,咋们可以添加线程锁把可能造成堆积的地方给锁住,让cpu在把锁住内容执行完毕后在切换。这里采用threading.Lock().acquire()加锁和threading.Lock().release()解锁
如下
def add():
global num
lock.acquire() #将下面的内容锁住,只能有一个线程执行
temp = num
time.sleep(0.1)
num = temp+1
lock.release() #解锁
num = 0
import threading
import time
l= []
lock = threading.Lock()
for i in range(100):
t1 = threading.Thread(target=add)
t1.start()
l.append(t1)
for t in l:
t.join()
print(num)
虽然解决了堆积问题,但是加锁后就相当于串行了,效率也随之下降。在开发过程中要尽量减少加锁的内容和加锁范围,避免造成线程死锁。
在线程件共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁,因为系统判断这部分资源都正在使用,所有这两个线程在外力里作用下将一直等待下去。下面就是一个死锁的例子
import threading, time
#死锁,第一个线程执行完actionA后执行actionB,第一个线程拿到B锁,而第二个线程在actionA中想要获得#B锁,
#但是他被第一个线程用了,此时死锁
#输出下面
#Thread-1gotA--- Wed Feb 20 15:31:37 2019
#Thread-1 gotB--- Wed Feb 20 15:31:39 2019
#Thread-2gotA--- Wed Feb 20 15:31:40 2019
#Thread-1 gotB--- Wed Feb 20 15:31:41 2019------在这里卡住了
class Mythread(threading.Thread):
def actionA(self):
A.acquire()
print(self.name + "gotA---",time.ctime())
time.sleep(2)
B.acquire()
print(self.name,"gotB---",time.ctime())
time.sleep(1)
B.release()
A.release()
def actionB(self):
B.acquire()
print(self.name,"gotB---",time.ctime())
time.sleep(2)
A.acquire()
print(self.name,"gotA----",time.ctime())
time.sleep(1)
A.release()
B.release()
def run(self):
self.actionA()
time.sleep(0.5)
self.actionB()
pass
if __name__ == "__main__":
A = threading.Lock()
B = threading.Lock()
l = []
for i in range(5):
t = Mythread()
t.start()
l.append(t)
for i in l:
i.join()
print("end --------")
为了支持在同一线程中多次请求同一资源,python提供了"可重入锁":threading.Rlock。RLock内部维护者一个Lock和一个counter变量,counter记录了acquire的次数,从而式的资源可以呗多谢acquire。知道一个线程所有acquire都被release,其它的线程才能获得资源。
使用递归锁RLock解决死锁,加锁时计数器加一,当计数器大于0,只有本线程可用该锁,其它线程就用不了,当解锁时计数器减一,只有计数器为0是其它线程才能竞争执行,从而避免了死锁。
import threading, time
class Mythread(threading.Thread):
def actionA(self):
r_lock.acquire() #计数器加一
print(self.name + "gotA---",time.ctime())
time.sleep(2)
r_lock.acquire()#计数器加一
print(self.name,"gotB---",time.ctime())
time.sleep(1)
r_lock.release()#计数器减一
r_lock.release()#计数器减一
def actionB(self):
r_lock.acquire()#计数器加一
print(self.name,"gotB---",time.ctime())
time.sleep(2)
r_lock.acquire()#计数器加一
print(self.name,"gotA----",time.ctime())
time.sleep(1)
r_lock.release()#计数器减一
r_lock.release()#计数器减一
def run(self):
self.actionA()
time.sleep(0.5)
self.actionB()
if __name__ == "__main__":
# 使用递归锁RLock解决死锁, 加锁时计数器加一,当计数器大于0,只有本线程可用该锁,其它线程就用
#不了,
# 当解锁时计数器减一,只有计数器为0是其它线程才能竞争执行,从而避免了死锁。
r_lock=threading.RLock() #递归锁
l = []
for i in range(5):
t = Mythread()
t.start()
l.append(t)
for i in l:
i.join()
print("end --------")
这次就正常执行了!