一、同步异步概念
1、什么是同步
同步就是协同步调,按照预定的先后顺序进行运行。
二、互斥锁
1、什么是互斥锁
当多个线程几乎同时修改一个共享数据的时候,需要进行同步控制,线程同步能够保证多个线程安全的访问竞争资源,这时便需要使用互斥锁。
from threading import Thread,Lock
g_num = 0
def func1():
global g_num
for i in range(5):
lock.acquire()
print("func1得到锁")
g_num += 1
lock.release()
print("func1释放锁")
def func2():
global g_num
for i in range(5):
lock.acquire()
print("func2得到锁")
g_num += 1
lock.release()
print("func2释放锁")
if __name__ == '__main__':
lock = Lock()
t1 = Thread(target=func1)
t2 = Thread(target=func2)
t1.start()
t2.start()
2、上锁过程
当一个线程调用锁的acquire()方法获得锁时,就进入"locked"状态。
每次只有一个线程可以获得锁。如果此时另外一个线程试图获得这个锁,该线程就会变为"blocked"阻塞状态,直到拥有锁的线程,调用release()方法释放锁之后,锁进入"unlock"状态。
线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行状态。
3、互斥锁的优点
①、确保了某段关键代码只能由一个线程从头到尾完整执行。
4、互斥锁的缺点
①、阻止了多线程并发执行,包含锁的某段代码只能以单线程模式执行,效率低下。
②、由于可以存在多个锁,当不同的线程持有不同的锁,并试图获取对方持有的锁时,可能或造成死锁
三、死锁
1、什么是死锁
在多个线程共享资源的时候,如果连个线程分别占有一定的资源,并且同时等待对方的资源,就会造成死锁现象。
如果锁之间相互嵌套,就有可能出现死锁。因此尽量不要出现锁之间的嵌套。
from threading import Thread,Lock
import time
def func1():
lockA.acquire()
print("func1得到A锁")
time.sleep(1)
lockB.acquire()
print("func1得到B锁")
lockA.release()
print("func1释放A锁")
lockB.release()
print("func1释放B锁")
def func2():
lockB.acquire()
print("func2得到B锁")
time.sleep(1)
lockA.acquire()
print("func2得到A锁")
lockB.release()
print("func2释放B锁")
lockA.release()
print("func2释放A锁")
if __name__ == '__main__':
lockA = Lock()
lockB = Lock()
t1 = Thread(target=func1)
t2 = Thread(target=func2)
t1.start()
t2.start()
四、非共享数据
1、非共享数据是否需要上锁
在多线程开发中,全局变量是多个线程都共享的数据,而局部变量等是各自线程的,是非共享的
from threading import Thread,Lock
import time
class MyThread(Thread):
def __init__(self,num,sleeptime):
super().__init__()
self.num = num
self.sleeptime = sleeptime
def run(self):
self.num += 1
time.sleep(self.sleeptime)
print("线程:%s,num = %d"%(self.name,self.num))
if __name__ == '__main__':
lock = Lock()
t1 = MyThread(100,2)
t2 = MyThread(200,1)
t1.start()
t2.start()
from threading import Thread
import threading
import time
def func(sleeptime):
num = 1
time.sleep(sleeptime)
num += 1
print("num = %d"%num,threading.current_thread())
if __name__ == '__main__':
t1 = Thread(target = func,args = (1,))
t2 = Thread(target = func,args = (2,))
t1.start()
t2.start()
五、生产者和消费者
1、队列
from queue import Queue
q = Queue(3)
q.put("消息1")
q.put("消息2")
q.put("消息3")
# q.put("消息4",block=False) #queue.Full
print(q.qsize(),q.full()) #3 True
# print(q.get()) #消息1
# print(q.get()) #消息2
# print(q.get()) #消息3
while q.qsize() > 0:
print(q.get()) #消息1 消息2 消息3
# print(q.empty()) #True
try:
print(q.get(block=False))
except:
print("队列空了") #队列空了
# print(q.get_nowait()) #_queue.Empty
2、队列优先级
from queue import Queue,PriorityQueue
q = PriorityQueue()
q.put((1,"消息1"))
q.put((-1,"消息2"))
q.put((-5,"消息3"))
q.put((10,"消息4"))
q.put((7,"消息5"))
while q.qsize() > 0:
print(q.get())
"""
(-5, '消息3')
(-1, '消息2')
(1, '消息1')
(7, '消息5')
(10, '消息4')
"""
3、什么是生产者与消费者模式
通过一个容器来解决生产者与消费者的强耦合问题。生产者和消费者之间不能直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后,不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里获取,阻塞队列就相当于一个缓冲区,平衡和生产者和消费者的处理能力。
4、为什么要用生产者与消费者模式
生产者就是生产数据的线程,消费者就是消费数据的线程。
在多线程编程中,如果生产者的生产能力大于消费者,那么生产者就必须等待消费者处理完,才能继续生产数据。相反,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题,于是引进生产者与消费者模式。
该模式通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。
from threading import Thread
from queue import Queue
import time
q = Queue(10)
def producer(name):
count = 1
while True:
q.put("[包子%s]"%count)
print("[%s]生产了[包子%d]"%(name,count))
count += 1
time.sleep(1)
def consumer(name):
while True:
print("[%s]取到%s并吃了它..."%(name,q.get()))
time.sleep(1)
if __name__ == '__main__':
p1 = Thread(target = producer,args = ("张三",))
p2 = Thread(target = producer,args = ("李四",))
c1 = Thread(target = consumer,args = ("王五",))
c2 = Thread(target = consumer,args = ("赵六",))
p1.start()
p2.start()
c1.start()
c2.start()
六、threadLocal
在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他的线程,而全局变量的修改必须加锁。
这个时候就需要使用threadLocal方法
1、使用threadLocal的方法
from threading import Thread
import threading
import time
loc = threading.local()
def process():
student = loc.name
print("hello,%s 线程:%s"%(student,threading.current_thread().name))
def process_thread(name):
loc.name = name
time.sleep(2)
process()
if __name__ == '__main__':
t1 = Thread(target = process_thread,args = ("张三",),name = "线程1")
t2 = Thread(target = process_thread,args = ("李四",),name = "线程2")
t1.start()
t2.start()
七、全局解释器锁
1、什么是全局解释器锁
对python解释器访问是由全局解释器锁(GIL)控制的。
这个锁用来保证同时只能有一个线程运行。
在多线程环境中,python解释器将按照下面的方式执行。
①、设置GIL
②、切换进一个线程去执行
③、执行下面操作之一
A、指定数量的字节码指令
B、线程主动让出控制权(可以调用time,sleep(0)来完成)。
④、把线程设置回休眠状态(切换出线程)
⑤、解锁GIL
⑥、重复上述步骤
import time
from threading import Thread
from multiprocessing import Process
def func():
start = time.time()
sum = 0
for i in range(100000000):
sum += 1
end = time.time()
print(end - start)
if __name__ == '__main__':
p1 = Process(target = func)
p2 = Process(target=func)
p1.start()
p2.start()
八、协程
1、什么是协程
协程,又称微线程,纤程。英文名Coroutine。
线程是系统级别的它们由操作系统调度,而协程则是程序级别的由程序根据需要自己调度。
在一个线程中会有很多函数,我们把这些函数称为子程序,在子程序执行过程中可以中断去执行别的子程序,而别的子程序也可以中断回来继续执行之前的子程序,这个过程就称为协程。也就是说在同一线程内一段代码在执行过程中会中断然后跳转执行别的代码,接着在之前中断的地方继续开始执行,类似与yield操作。
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。
2、协程与进程的区别:
①、相同点:
相同点存在于,当我们挂起一个执行流的时,我们要保存的东西:
A、栈, 其实在你切换前你的局部变量,以及要函数的调用都需要保存,否则都无法恢复
B、寄存器状态,这个其实用于当你的执行流恢复后要做什么
而寄存器和栈的结合就可以理解为上下文,上下文切换的理解:
CPU看上去像是在并发的执行多个进程,这是通过处理器在进程之间切换来实现的,操作系统实现这种交错执行的机制称为上下文切换
操作系统保持跟踪进程运行所需的所有状态信息。这种状态,就是上下文。
在任何一个时刻,操作系统都只能执行一个进程代码,当操作系统决定把控制权从当前进程转移到某个新进程时,就会进行上下文切换,即保存当前进程的上下文,恢复新进程的上下文,然后将控制权传递到新进程,新进程就会从它上次停止的地方开始。
②不同点:
A、执行流的调度者不同,进程是内核调度,而协程是在用户态调度,也就是说进程的上下文是在内核态保存恢复的,而协程是在用户态保存恢复的,很显然用户态的代价更低
B、进程会被强占,而协程不会,也就是说协程如果不主动让出CPU,那么其他的协程,就没有执行的机会。
C、对内存的占用不同,实际上协程可以只需要4K的栈就足够了,而进程占用的内存要大的多
D、从操作系统的角度讲,多协程的程序是单进程,单协程
3、协程与线程的区别:
不同点:
A、线程之间需要上下文切换成本相对协程来说是比较高的,尤其在开启线程较多时,但协程的切换成本非常低。
B、同样的线程的切换更多的是靠操作系统来控制,而协程的执行由我们自己控制。
协程只是在单一的线程里不同的协程之间切换,其实和线程很像,线程是在一个进程下,不同的线程之间做切换,这也可能是协程称为微线程的原因吧。
4、协程的优点:
①、无需线程上下文切换的开销,协程避免了无意义的调度,由此可以提高性能(但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力)
②、无需原子操作锁定及同步的开销
③、方便切换控制流,简化编程模型
④、高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。
5、协程的缺点:
①、无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
②、进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序
def func():
for i in range(10):
yield i
a = func()
print(next(a))
print(next(a))
print(next(a))
def A():
for i in range(2):
print("---A---")
yield
def B(a):
for i in range(2):
print("---B---")
next(a)
if __name__ == '__main__':
a = A()
B(a)
import time
def func1():
start = time.time()
sum = 0
for i in range(1,100000001):
sum += 1
yield
end = time.time()
print(end - start)
def func2(a):
start = time.time()
sum = 0
for i in range(1, 100000001):
sum += 1
next(a)
end = time.time()
print(end - start)
if __name__ == '__main__':
a = func1()
func2(a)
import gevent,time
def A():
for i in range(3):
print("this is A",i)
gevent.sleep(1)
def B():
for i in range(3):
print("this is B",i)
gevent.sleep(1)
if __name__ == '__main__':
g1 = gevent.spawn(A)
g2 = gevent.spawn(B)
gevent.joinall([g1,g2])