协程
协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。
一句话说明什么是线程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。
- python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行)
- 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(!!!非io操作的切换与效率无关)
对比操作系统控制线程的切换,用户在单线程内控制协程的切换
优点如下:
- 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
- 单线程内就可以实现并发的效果,最大限度地利用cpu
缺点如下:
- 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
- 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
总结协程特点:
必须在只有一个单线程里实现并发
修改共享数据不需加锁
用户程序里自己保存多个控制流的上下文栈
附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))
信号量
信号量代码演示(信号量指的是同一时间有多把锁并发运行)
Semaphore 导入信号量模块
from threading import Thread, Semaphore, current_thread
import time
def func():
sm.acquire() # 抢锁
print("%s 干饭 " % current_thread().getName())
time.sleep(3)
sm.release() # 释放锁
if __name__ == '__main__':
sm = Semaphore(5) # 一共有5把锁
for i in range(21):
t = Thread(target=func)
t.start()
死锁现象与递归锁
出现死锁现象不会有报错提示,锁套锁会出现死锁的现象
模拟死锁代码演示
from threading import Thread, Lock
mutexA = Lock()
mutexB = Lock()
class Mythread(Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
self.f1()
self.f2()
def f1(self):
mutexA.acquire()
print("%s 抢到了A锁" % self.name) # 线程1先抢到了a锁,继续抢了b锁。这时候其他三个线程都并发抢a锁,
# 线程1释放了b锁,再释放了a锁,其他三个并发抢a锁的同时,线程1又抢了b锁并且睡了0.1秒
mutexB.acquire() # 这个时候b锁被线程2抢到了,a锁在线程1的手里。线程1和线程2互相锁死。线程3和线程4还在等着抢a锁
print("%s 抢到了B锁" % self.name)
mutexB.release()
mutexA.release()
def f2(self):
mutexB.acquire()
print("%s 抢到了B锁" % self.name)
time.sleep(0.1)
mutexA.acquire()
print("%s 抢到了A锁" % self.name)
mutexA.release()
mutexB.release()
if __name__ == '__main__':
t1 = Mythread("线程1")
t2 = Mythread("线程2")
t3 = Mythread("线程3")
t4 = Mythread("线程4")
t1.start()
t2.start()
t3.start()
t4.start()
print("主线程")
死锁现象发生打印的结果:
线程1 抢到了A锁
线程1 抢到了B锁
线程1 抢到了B锁
线程2 抢到了A锁
主线程
递归锁
递归锁同一把锁可以连续acquire,以计数的方式来抢锁和释放锁
from threading import Thread, RLock
mutexA = mutexB = RLock() # 递归锁,一个进程可以连续抢锁,抢一次锁技计数1次,第一次计数1,第二次计数2。。。。
# 其他的线程如果发现递归锁的计数不为0就直接堵塞在原地,等这把锁计数为0的时候才能抢锁
class Mythread(Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
self.f1()
self.f2()
def f1(self):
mutexA.acquire()
print("%s 抢到了A锁" % self.name)
mutexB.acquire()
print("%s 抢到了B锁" % self.name)
mutexB.release()
mutexA.release()
def f2(self):
mutexB.acquire()
print("%s 抢到了B锁" % self.name)
time.sleep(0.1)
mutexA.acquire()
print("%s 抢到了A锁" % self.name)
mutexA.release()
mutexB.release()
if __name__ == '__main__':
t1 = Mythread("线程1")
t2 = Mythread("线程2")
t3 = Mythread("线程3")
t4 = Mythread("线程4")
t1.start()
t2.start()
t3.start()
t4.start()
print("主线程")
事件event
一个线程完毕,另外一个线程才开始运行,并不涉及到数据的交接,通过发送信号(数据)来提醒另外一个线程。
代码演示
from threading import Event, Thread, current_thread
e = Event() # 默认全局变量为False
def f1():
print("%s 运行" % current_thread().name) # current_thread().name获取线程名字
time.sleep(3)
e.set() # 把全局变量变成了True
# e.clear() # 把全局变量变成False
# e.is_set() # 判断有没有设置过,设置过就是True,没有设置过就是False
def f2():
e.wait() # 在原地等待,等到全局变量变为True。e.wait(n)可以设置等待时间,超时如果全局变量还没有变成True那么会自动执行代码
print("%s 运行" % current_thread().name)
if __name__ == '__main__':
t1 = Thread(target=f1)
t2 = Thread(target=f2)
t1.start()
t2.start()
案例
from threading import Event, Thread, current_thread
import time
import random
e = Event() # e = Ture
def task1():
while True:
e.clear() # e = False
print("红灯亮")
time.sleep(2)
e.set() # e = True
print("绿灯亮")
time.sleep(3)
# task1:2秒亮一次红灯,三秒亮一次绿灯。不停的在循环
def task2():
while True:
if e.is_set(): # 判断e有没有被设置过,如果设置过就是True,没有设置过就是False
print("%s 走你" % current_thread().name)
break # 线程只有进入“走你”才会结束循环
else:
print("%s 等灯" % current_thread().name)
e.wait() # 等待全局变量变为True
if __name__ == '__main__':
Thread(target=task1).start()
while True:
time.sleep(random.randint(1, 5)) # 每1-5秒之间产生循环
Thread(target=task2).start()
定时器与线程queue
定时器
from threading import Timer
def hello(x):
print("hello,world", x)
t = Timer(0.2, hello, args=(10,)) # Timer(n,"xx")可以在定时器第一个逗号前面写上时间,表示过多长时间运行代码
t.start()
线程queue(里面有队列,堆栈,优先级队列)
import queue
队列(先进先出)
q = queue.Queue(3)
q.put(111)
q.put("aaa")
q.put((1, 2, 3))
print(q.get())
print(q.get())
print(q.get())
堆栈(后进先出)
q = queue.LifoQueue(3)
q.put(111)
q.put("aaa")
q.put((1, 2, 3))
print(q.get())
print(q.get())
print(q.get())
优先级队列(元组,第一个整型数字是优先级排序)
q = queue.PriorityQueue(3)
q.put((2, (111)))
q.put((3, "aaa"))
q.put((1, (1, 2, 3)))
print(q.get())
print(q.get())
print(q.get())
GIL解释器锁(cpython解释器自带的锁)
在python解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势
python线程需要拿到解释器的gil锁才可以运行代码,相当于一个解释器执行权限,有了执行权限,操作系统才会给python代码分配cpu,一旦cpu被调走了,gil锁会直接释放
gil锁主要是为了保护python解释器内存管理的数据安全的,无法保护你自己写的python代码的数据安全
gil锁可以使线程并发,但是不能让线程并行(python运行一个进程只能用一个cpu,如果想要同时使用多个cpu来提升计算效率,需要开多个进程)
from threading import Thread, Lock
import time
mutex = Lock()
n = 100
def task():
global n
mutex.acquire() # 互斥锁
temp = n
time.sleep(0.1)
n = temp - 1
mutex.release()
if __name__ == '__main__':
l = []
for i in range(100):
t = Thread(target=task)
l.append(t)
t.start()
for obj in l:
obj.join()
print(n)
多线程性能测试
纯计算的行为可以用多进程,使用多核优势来提升运行效率
纯io行为的可以在一个进程里面开多个线程,因为io行为多核优势就体现不出来了,
开多进程,需要开辟新的内存空间,把父进程的程序复制到子进程里面,开启进程。一旦产生io行为,操作系统会自动把cpu调走,io结束操作系统会把cup再调过来。
开多个线程,一个cpu来回切换,因为开线程的开销是远远小于开进程的开销的,所以效率也会高于多进程。
代码演示
from multiprocessing import Process
from threading import Thread
import os, time
def work():
# res = 0
# for i in range(100000000):
# res *= i
time.sleep(5)
if __name__ == '__main__':
l = []
# print(os.cpu_count()) # 本机为8核
start = time.time()
for i in range(8):
p = Process(target=work) # 纯计算 多进程花费的时间12.8s |纯io行为多进程运行时间为5.335s
# p = Thread(target=work) # 纯计算 多线程运行的时间46.45s |纯io行为多线程运行时间为5.003s
l.append(p)
p.start()
for p in l:
p.join()
stop = time.time()
print("run time is %s" % (stop - start))