并发编程
概念理解
1. 无论是并行和并发,在用户看起来都是"同时"运行的,不管是进程还是线程,都只是一个任务而已,真正干活的是cpu,cpu来做这些任务,一个cpu同一时刻只能执行一个任务
2. 并发并不是真正的并行,而是利用多道技术+单个cpu做出并行的效果
3. 如果想要真正同时运行,只有具备多个cpu才能实现并行
4. 进程的调度,分配给哪个cpu运行,由操作系统说了算
5. 内存中同时存入多道(多个)程序,cpu从一个进程快速切换到另外一个,使每个进程各自运行几十或几百毫秒,这样,虽然在某一个瞬间,一个cpu只能执行一个任务,但在1秒内,cpu却可以运行多个进程,这就给人产生了并行的错觉,即伪并发,以此来区分多处理器操作系统的真正硬件并行(多个cpu共享同一个物理内存)
几组比较重要的概念
同步:在发出一个功能调用时,在没有得到结果之前,该调用就不会返回。
异步:当一个功能调用发出后,调用者不能立刻得到结果
阻塞:当socket工作在阻塞模式的时候,如果没有数据的情况下调用recv函数,则当前线程就会被挂起,直到有数据为止
非阻塞:指在不能立刻得到结果之前也会立刻返回,同时该函数不会阻塞当前线程
同步与异步针对的是函数/任务的调用方式:同步就是当一个进程发起一个函数(任务)调用的时候,一直等到函数(任务)完成,而进程继续处于激活状态。而异步情况下是当一个进程发起一个函数(任务)调用的时候,不会等函数返回,而是继续往下执行当,函数返回的时候通过状态、通知、事件等方式通知进程任务完成
阻塞与非阻塞针对的是进程或线程:阻塞是当请求不能满足的时候就将进程挂起,而非阻塞则不会阻塞当前进程
进程的三种状态
1.就绪态
2.运行态
3.阻塞态
multiprocessing模块
进程没有任何共享状态,进程修改的数据,改动仅限于该进程内
应用:
from multiprocessing import Process
p = Process(target=func,args=(),kwargs={})
p.start():启动进程,并调用该子进程中的p.run()
p.run():进程启动时运行的方法,正式它去调用target指定的函数,我们自定义的类中一定要实现该方法
p.terminate():强制终止进程p,不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。如果p还保存了一个锁,那么锁也不会被释放,进而导致死锁现象
p.is_alive():如果p仍然运行,返回True
p.join():主线程等待p终止(是主线程处于等的状态,而p是处于运行的状态),只能join住start开启的进程,不能join住run开启的进程
p.daemon:默认值False,如果设为True,代表p为后台运行的守护进程,当p的父进程终止时,p也随着终止
p.name:进程的名称
p.pid:进程的pid
p.exitcode:进程在运行时为None、如果-N,表示被信号N结束
p.authkey:进程的身份验证,默认是由os.urandom()随机生成的32字符的字符串
Process的使用要点:
1.在windows中Process()必须放到if__name__ == '__main__':下
2.创建进程的使用类的方式:
class P(Process): # 必须继承Process类
def __init__(self,name):
super.__init__() # 继承父类ini方法
self.name = name
def run(self):
print(self.name)
3.进程池:每来一个客户端,都在服务端开启一个进程,如果并发来一个万个客户端,要开启一万个进程吗,你自己尝试着在你自己的机器上开启一万个,10万个进程试一试。解决方法:进程池
4.p.daemon=True一定要在p.start()前设置
进程同步(锁)
加锁:由并发变成了串行,牺牲了运行效率,但避免了竞争
from multiprocessing import Process,Lock
import os,time
def work(lock):
lock.acquire()
print('%s is running' %os.getpid())
time.sleep(2)
print('%s is done' %os.getpid())
lock.release()
if __name__ == "__main__":
lock = Lock()
p = Process(target=work,args=(lock,))
p.start()
进程同步(队列,推荐使用)
进程彼此之间互相隔离,要实现进程间通信(IPC),multiprocessing模块支持两种形式:队列和管道,这两种方式都是使用消息传递的
from multiprocessing import Process,Queue
import time
q = Queue(3)
q.put(3)
q.put(3)
q.put(3)
q.get()
生产者消费者模型:生产者<------>队列<------>消费者,让生产者在生产完毕后,往队列中再发一个结束信号,这样消费者在接收到结束信号后就可以break出死循环
进程池
from multiprocessing import Pool
import os,time
def work(n):
print("%s run" % os.getpid())
time.sleep(3)
return n**2
if __name__ == "__main__":
p = Pool(3)
res_l = []
for i in range(10):
res = p.apply(work,args=(i,)) # apply_saync,异步提交
res_l.append(res)
print(res_l)
回调函数:
需要回调函数的场景:进程池中任何一个任务一旦处理完了,就立即告知主进程:我好了额,你可以处理我的结果了。主进程则调用一个函数去处理该结果,该函数即回调函数
p.apply_async(get_page,args=(url,),callback=pasrse_page)
如果在主进程中等待进程池中所有任务都执行完毕后,再统一处理结果,则无需回调函数
线程概念理解
1.进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是cpu上的执行单位
2.线程的创建开销小
3.多线程共享一个进程的地址空间
4.线程比进程更轻量级,线程比进程更容易创建可撤销,在许多操作系统中,创建一个线程比创建一个进程要快10-100倍,在有大量线程需要动态和快速修改时,这一特性很有用
5.若多个线程都是cpu密集型的,那么并不能获得性能上的增强,但是如果存在大量的计算和大量的I/O处理,拥有多个线程允许这些活动彼此重叠运行,从而会加快程序执行的速度
threading模块
1.开启线程的两种方式
Ⅰ. from threading import Thread
if __name__ == "__main__":
t = Thread(target=func,args=())
t.start()
Ⅱ. from threading import Thread
class T(Thread):
def __init__(self,):
super().__init__()
def run(self): # 函数名必须为run
2.其他常用方法
t.join()
t.daemon = True
3.GIL
100个线程去抢GIL锁,即抢执行权限
肯定有一个线程先抢到GIL(暂且称为线程1),然后开始执行,一旦执行就会拿到lock.acquire()
极有可能线程1还未运行完毕,就有另外一个线程2抢到GIL,然后开始运行,但线程2发现互斥锁lock还未被线程1释放,于是阻塞,被迫交出执行权限,即释放GIL
直到线程1重新抢到GIL,开始从上次暂停的位置继续执行,直到正常释放互斥锁lock,然后其他的线程再重复2 3 4的过程
死锁与递归锁
信号量
与进程池是完全不同的概念,进程池Pool(4),最大只能产生4个进程,而且从头到尾都只是这四个进程,不会产生新的,而信号量是产生一堆线程/进程
Event
条件Condition:使得线程等待,只有满足某条件时,才释放n个线程
定时器
from threading import Timer
Timer(5,func)
线程queue
用法和进程Queue一样
同步异步提交
逻辑和进程差不多
协程