并发编程
必备知识:
#一 操作系统的作用: 1:隐藏丑陋复杂的硬件接口,提供良好的抽象接口 2:管理、调度进程,并且将多个进程对硬件的竞争变得有序 #二 多道技术: 目的:单核实现并发的效果 串行:一个任务完完整整运行完毕,才执行下一个程序 并发:多个任务看起来是同时运行的效果,单核就可以实现并发 并行:多个任务真正意义上同时运行的效果,多核才可以实现并行 多道技术: 1、空间上的复用—— >多个任务共用一个内存条,但占用内存是彼此隔离的,而且是物理层面隔离 2、时间上的复用——>多个任务共用同一个CPU 切换+保存状态: 1、遇到I/O操作进行切换,切换之前将进程的状态保存下来,这样才能保证下次切换回来时,
能基于上次切走的位置继续运行 作用:提高CPU的的利用率,并且不影响执行效率 2、一个任务占用CPU时间过长,或者有另外一个优先级更高的任务抢走cpu执行权限 作用:降低程序的执行效率
进程
什么是进程?
进程是正在进行的一个过程,或者说是一个任务,负责执行任务的是CPU。
为什么用进程?
使用进程实现并发
怎么用进程?
开启进程的两种方式
进程与程序的区别:
进程:是正在进行的一个过程,或者说是一个任务,负责执行任务的是CPU。
程序:硬件上的代码
注意:同一个程序执行两次,是两个进程,如打开两个QQ,一个大号,一个小号,只是进程名一样罢了。
并发与并行
并发:指两者同时执行
并行:指有限资源下执行
区别:
1.并发:是伪并行,看起来是同时运行。单个cpu+多道技术就可以实现并发(并行也属于并发)
2.并行:同时运行,具有多个cpu才能实现并行效果
单核的计算机肯定不能实现并行,但是可以实现并发!!!

进程调度
-
先来先服务调度算法:对长作业有利,对短作业无益
- 短作业优先调度算法:对短作业有利,多长作业无益
- 时间片轮转法:时间片将固定时间分成多份,每份表示一个时间片
- 多级反馈队列:优先级
进程的终止
1.正常退出(自愿,如用户点击程序上的关闭按钮或者程序执行完毕调用发起系统调用正常退出)
2.出错退出(自愿,python a.py中a.py不存在)
3.严重错误(非自愿,执行非法指令,如引用不存在的内存,1/0等,可以捕捉异常,try.....except...)
4.被其他进程杀死(非自愿)
进程的状态
两种情况下会导致一个进程在逻辑上的不能运行:
1、进程挂起是自身原因,遇到I/O阻塞,便要让出CPU为其它进程执行任务,这样就保证CPU随时保持工作状态
2、与进程无关,与操作系统有关。由于一个进程占用时间过长,或者优先级更高任务切换进入等原因,而调用其他的进程去使用CPU

程序会进入几种状态:就绪态,运行态与阻塞态
就绪态:已准备好,等待被运行
运行态:程序正在运行中
阻塞态:正在执行进程,由于等待某个事情的发生而无法继续执行,就是卡在原地不动。(例如:等待io完成,等待信号)
两对重要概念
同步和异步
-
-
"""描述的是任务的提交方式"""
-
同步:任务提交之后,原地等待任务的返回结果,等待的过程中不做任何事(干等)程序层面上表现出来的感觉就是卡住了
-
异步:任务提交之后,不原地等待任务的返回结果,直接去做其他事情
-
-
- """描述的程序的运行状态"""
-
- 阻塞:阻塞态
- 非阻塞:就绪态、运行态
理想状态:我们应该让我们的写的代码永远处于就绪态和运行态之间切换
from multiprocessing import Process import time def task(name): print('%s is running'%name) time.sleep(3) print('%s is over'%name) if __name__ == '__main__': # 1 创建一个对象 p = Process(target=task, args=('jason',)) # 容器类型哪怕里面只有1个元素 建议要用逗号隔开 # 2 开启进程 p.start() # 告诉操作系统帮你创建一个进程 异步 print('主')
from multiprocessing import Process import time class MyProcess(Process): def run(self): print('hello bf girl') time.sleep(1) print('get out!') if __name__ == '__main__': p = MyProcess() p.start() print('主')
- 创建进程就是在内存中申请一块内存空间将需要运行的代码丢进去
- 一个进程对应在内存中就是一块独立的内存空间
- 多个进程对应在内存中就是多块独立的内存空间
- 进程与进程之间数据默认情况下是无法直接交互,如果想交互可以借助于第三方工具、模块
join方法
from multiprocessing import Process import time def task(name, n): print('%s is running'%name) time.sleep(n) print('%s is over'%name) if __name__ == '__main__': # p1 = Process(target=task, args=('jason', 1)) # p2 = Process(target=task, args=('egon', 2)) # p3 = Process(target=task, args=('tank', 3)) # start_time = time.time() # p1.start() # p2.start() # p3.start() # 仅仅是告诉操作系统要创建进程 # # time.sleep(50000000000000000000) # # p.join() # 主进程等待子进程p运行结束之后再继续往后执行 # p1.join() # p2.join() # p3.join() start_time = time.time() p_list = [] for i in range(1, 4): p = Process(target=task, args=('子进程%s'%i, i)) p.start() p_list.append(p) for p in p_list: p.join() print('主', time.time() - start_time)
from multiprocessing import Process money = 100 def task(): global money # 局部修改全局 money = 666 print('子',money) if __name__ == '__main__': p = Process(target=task) p.start() p.join() print(money)
""" 一台计算机上面运行着很多进程,那么计算机是如何区分并管理这些进程服务端的呢? 计算机会给每一个运行的进程分配一个PID号 如何查看 windows电脑 进入cmd输入tasklist即可查看 tasklist |findstr PID查看具体的进程 mac电脑 进入终端之后输入ps aux ps aux|grep PID查看具体的进程 """ from multiprocessing import Process, current_process current_process().pid # 查看当前进程的进程号 import os os.getpid() # 查看当前进程进程号 os.getppid() # 查看当前进程的父进程进程号 p.terminate() # 杀死当前进程 # 是告诉操作系统帮你去杀死当前进程 但是需要一定的时间 而代码的运行速度极快 time.sleep(0.1) print(p.is_alive()) # 判断当前进程是否存活
多进程
multiprocessing模块:用来开启子进程并在子进程中执行我们定制的任务(比如函数),该模块与多线程模块threading的编程接口类似.
multiprocessing模块的功能众多:支持子进程、通信和共享数据、执行不同形式的同步,提供了Process、Queue、Pipe、Lock等组件。
# 僵尸进程 """ 死了但是没有死透 当你开设了子进程之后 该进程死后不会立刻释放占用的进程号 因为我要让父进程能够查看到它开设的子进程的一些基本信息 占用的pid号 运行时间。。。 所有的进程都会步入僵尸进程 父进程不死并且在无限制的创建子进程并且子进程也不结束 回收子进程占用的pid号 父进程等待子进程运行结束 父进程调用join方法 """ # 孤儿进程 """ 子进程存活,父进程意外死亡 操作系统会开设一个“儿童福利院”专门管理孤儿进程回收相关资源 """
守护进程其实就是一个'子进程'
守护 ---> 伴随
守护进程会伴随主进程的代码运行完毕后终止结束
两个关键点:
进程:当父进程需要将一个任务并发出去执行,需要将该任务放到一个子进程里
守护: 当该子进程的代码在父进程代码运行完毕后就没有存在的意义了,就应该将该子进程设置为守护进程,会在父进程代码结束后死掉
from multiprocessing import Process import time def task(name): print('%s总管正在活着'% name) time.sleep(3) print('%s总管正在死亡' % name) if __name__ == '__main__': p = Process(target=task,args=('egon',)) # p = Process(target=task,kwargs={'name':'egon'}) p.daemon = True # 将进程p设置成守护进程 这一句一定要放在start方法上面才有效否则会直接报错 p.start() print('皇帝jason寿终正寝') #主进程代码运行完毕后,守护进程就会结束掉
针对上述问题,解决方式就是加锁处理:
from multiprocessing import Process, Lock import json import time import random # 查票 def search(i): # 文件操作读取票数 with open('data','r',encoding='utf8') as f: dic = json.load(f) print('用户%s查询余票:%s'%(i, dic.get('ticket_num'))) # 字典取值不要用[]的形式 推荐使用get 你写的代码打死都不能报错!!! # 买票 1.先查 2.再买 def buy(i): # 先查票 with open('data','r',encoding='utf8') as f: dic = json.load(f) # 模拟网络延迟 time.sleep(random.randint(1,3)) # 判断当前是否有票 if dic.get('ticket_num') > 0: # 修改数据库 买票 dic['ticket_num'] -= 1 # 写入数据库 with open('data','w',encoding='utf8') as f: json.dump(dic,f) print('用户%s买票成功'%i) else: print('用户%s买票失败'%i) # 整合上面两个函数 def run(i, mutex): search(i) # 给买票环节加锁处理 # 抢锁 mutex.acquire() buy(i) # 释放锁 mutex.release() if __name__ == '__main__': # 在主进程中生成一把锁 让所有的子进程抢 谁先抢到谁先买票 mutex = Lock() for i in range(1,11): p = Process(target=run, args=(i, mutex)) p.start()
""" 扩展 行锁 表锁 注意: 1.锁不要轻易的使用,容易造成死锁现象(我们写代码一般不会用到,都是内部封装好的) 2.锁只在处理数据的部分加来保证数据安全(只在争抢数据的环节加锁处理即可) """
进程间通信
进程之间数据是相互隔离的,想要实现进程间通信(IPC机制)就要借用模块multiprocessing:队列+管道(就可以实现进程间数据传输) 队列:管道+锁
队列
创建共享的进程队列 Queue是多进程安全的队列,Queue可以实现多进程数据之间的传递

队列Queue模块
""" 管道:subprocess stdin stdout stderr 队列:管道+锁 队列:先进先出 堆栈:先进后出 """ from multiprocessing import Queue # 创建一个队列 q = Queue(5) # 括号内可以传数字 标示生成的队列最大可以同时存放的数据量 # 往队列中存数据 q.put(111) q.put(222) q.put(333) # print(q.full()) # 判断当前队列是否满了 # print(q.empty()) # 判断当前队列是否空了 q.put(444) q.put(555) # print(q.full()) # 判断当前队列是否满了 # q.put(666) # 当队列数据放满了之后 如果还有数据要放程序会阻塞 直到有位置让出来 不会报错 """ 存取数据 存是为了更好的取 千方百计的存、简单快捷的取 同在一个屋檐下 差距为何那么大 """ # 去队列中取数据 v1 = q.get() v2 = q.get() v3 = q.get() v4 = q.get() v5 = q.get() # print(q.empty()) # V6 = q.get_nowait() # 没有数据直接报错queue.Empty # v6 = q.get(timeout=3) # 没有数据之后原地等待三秒之后再报错 queue.Empty try: v6 = q.get(timeout=3) print(v6) except Exception as e: print('一滴都没有了!') # # v6 = q.get() # 队列中如果已经没有数据的话 get方法会原地阻塞 # print(v1, v2, v3, v4, v5, v6)
1.什么是生产者消费者模型
生产者:生产/制造东西的 消费者:消费/处理东西的 生产者--> 共享的介质(队列)<--消费者
2.为何用
实现了生产者与消费者的解耦合,生产者可以不停地生产,消费者也可以不停地消费
从而平衡了生产者的生产能力与消费者消费能力,提升了程序整体运行的效率
3.怎么用?
当我们的程序中存在明显的两类任务,一类负责产生数据,另外一类负责处理数据
此时就应该考虑使用生产者消费者模型来提升程序的效率
from multiprocessing import Process, Queue, JoinableQueue import time import random def producer(name,food,q): for i in range(5): data = '%s生产了%s%s'%(name,food,i) # 模拟延迟 time.sleep(random.randint(1,3)) print(data) # 将数据放入 队列中 q.put(data) def consumer(name,q): # 消费者胃口很大 光盘行动 while True: food = q.get() # 没有数据就会卡住 # 判断当前是否有结束的标识 # if food is None:break time.sleep(random.randint(1,3)) print('%s吃了%s'%(name,food)) q.task_done() # 告诉队列你已经从里面取出了一个数据并且处理完毕了 if __name__ == '__main__': # q = Queue() q = JoinableQueue() p1 = Process(target=producer,args=('大厨egon','包子',q)) p2 = Process(target=producer,args=('小厨tank','糖水',q)) c1 = Process(target=consumer,args=('kk',q)) c2 = Process(target=consumer,args=('cc',q)) p1.start() p2.start() # 将消费者设置成守护进程 c1.daemon = True c2.daemon = True c1.start() c2.start() p1.join() p2.join() # 等待生产者生产完毕之后 往队列中添加特定的结束符号 # q.put(None) # 肯定在所有生产者生产的数据的末尾 # q.put(None) # 肯定在所有生产者生产的数据的末尾 q.join() # 等待队列中所有的数据被取完再执行往下执行代码 """ JoinableQueue 每当你往该队列中存入数据的时候 内部会有一个计数器+1 没当你调用task_done的时候 计数器-1 q.join() 当计数器为0的时候 才往后运行 """ # 只要q.join执行完毕 说明消费者已经处理完数据了 消费者就没有存在的必要了
什么是线程
进程:资源单位
线程:执行单位
多线程(即多个控制线程)的概念,在一个进程中存在多个控制线程,多个控制线程共享该
进程的地址空间,相当于一个车间内有多条流水线,都共用一个车间的资源。
将操作系统比喻成一个大的工厂
那么进程就相当于工厂里面的车间
而线程就是车间里面的流水线
每一个进程肯定自带一个线程
再次总结:
进程:资源单位(起一个进程仅仅只是在内存空间中开辟一块独立的空间)
线程:执行单位(真正被cpu执行的其实是进程里面的线程,线程指的就是代码的执行
过程,执行代码中所需要使用到的资源都找所在的进程索要)
进程和线程都是虚拟单位,只是为了我们更加方便的描述问题
1、 隔离:多个进程内空间彼此隔离 内存共享:同一进程下的多个线程共享该进程内的数据 2、创建速度 创建线程的速度远快于创建进程的速度
为何要有线程
""" 开设进程 1.申请内存空间 耗资源 2.“拷贝代码” 耗资源 开线程 1.一个进程内可以开设多个线程 2. 一个进程内开设多个线程无需再次申请内存空间操作 总结: 开设线程的开销要远远的小于进程的开销 同一个进程下的多个线程数据是共享的!!! """ 我们要开发一款文本编辑器 获取用户输入的功能 实时展示到屏幕的功能 自动保存到硬盘的功能 针对上面这三个功能,开设进程还是线程合适??? 开三个线程处理上面的三个功能更加的合理
为什么开启线程的两种方式?
开启线程的两种方式
threading模块介绍:
multiprocess模块完全模仿了threading模块的接口,在使用层面以及相似性上差不多
from multiprocessing import Process from threading import Thread import time def task(name): print('%s is running'%name) time.sleep(1) print('%s is over'%name) # 开启线程不需要在main下面执行代码 直接书写就可以 # 但是我们还是习惯性的将启动命令写在main下面 t = Thread(target=task,args=('egon',)) # p = Process(target=task,args=('jason',)) # p.start() t.start() # 创建线程的开销非常小 几乎是代码一执行线程就已经创建了 print('主')
from threading import Thread import time class MyThead(Thread): def __init__(self, name): """针对刷个下划线开头双下滑线结尾(__init__)的方法 统一读成 双下init""" # 重写了别人的方法 又不知道别人的方法里有啥 你就调用父类的方法 super().__init__() self.name = name def run(self): print('%s is running'%self.name) time.sleep(1) print('egon DSB') if __name__ == '__main__': t = MyThead('egon') t.start() print('主')
import socket from threading import Thread from multiprocessing import Process """ 服务端 1.要有固定的IP和PORT 2.24小时不间断提供服务 3.能够支持并发 """ server =socket.socket() # 括号内不加参数默认就是TCP协议 server.bind(('127.0.0.1',8080)) server.listen(5) # 将服务的代码单独封装成一个函数 def talk(conn): # 通信循环 while True: try: data = conn.recv(1024) # 针对mac linux 客户端断开链接后 if len(data) == 0: break print(data.decode('utf-8')) conn.send(data.upper()) except ConnectionResetError as e: print(e) break conn.close() # 链接循环 while True: conn, addr = server.accept() # 接客 # 叫其他人来服务客户 # t = Thread(target=talk,args=(conn,)) t = Process(target=talk,args=(conn,)) t.start()
import socket client = socket.socket() client.connect(('127.0.0.1',8080)) while True: client.send(b'hello world') data = client.recv(1024) print(data.decode('utf-8'))
join是让主进程等待子进程代码运行结束之后,再继续运行。不影响其他子进程的执行
from threading import Thread import time def task(name): print('%s is running'%name) time.sleep(3) print('%s is over'%name) if __name__ == '__main__': t = Thread(target=task,args=('egon',)) t.start() t.join() # 主线程等待子线程运行结束再执行 print('主')
from threading import Thread import time money = 100 def task(): global money money = 666 print(money) if __name__ == '__main__': t = Thread(target=task) t.start() t.join() print(money)
线程对象的其他方法和属性
主进程等子进程是因为主进程要给子进程收尸
进程必须等待其内部所有线程都运行完毕才结束
守护线程:
守护线程会在本进程内所有非守护的线程都结束后了才跟着结束
即:
守护线程其实守护的是整个进程的运行周期(进程内所有的非守护线程都运行完毕)
from threading import Thread, current_thread import time def task(): print('%s is running' % current_thread().name) time.sleep(3) print('%s is done' % current_thread().name) if __name__ == '__main__': t = Thread(target=task, name='守护线程') t.daemon = True t.start() print('主') """ 创建线程的速度非常之快(比进程快百倍),在start时几乎同时创建的出来,
所以是优先打印了%s is running """
from threading import Thread import time def task(name): print('%s is running'%name) time.sleep(1) print('%s is over'%name) if __name__ == '__main__': t = Thread(target=task,args=('egon',)) t.daemon = True t.start() print('主') """ 主线程运行结束之后不会立刻结束 会等待所有其他非守护线程结束才会结束 因为主线程的结束意味着所在的进程的结束 """
# 稍微有一点迷惑性的例子 from threading import Thread import time def foo(): print(123) time.sleep(1) print('end123') def func(): print(456) time.sleep(3) print('end456') if __name__ == '__main__': t1 = Thread(target=foo) t2 = Thread(target=func) t1.daemon = True t1.start() t2.start() print('主.......')
互斥锁
from threading import Thread,Lock import time money = 100 mutex = Lock() def task(): global money mutex.acquire() tmp = money time.sleep(0.1) money = tmp - 1 mutex.release() if __name__ == '__main__': t_list = [] for i in range(100): t = Thread(target=task) t.start() t_list.append(t) for t in t_list: t.join() print(money)
GIL
将多个任务对共享数据修改的那一部分代码由并发变成“串行”,牺牲了效率但保证数据安全
GIL(全局解释器锁)
1.什么是GIL(这是Cpython解释器)
GIL本质就是一把互斥锁,那既然是互斥锁,原理都一样,都是让多个并发线程同一时间只能有一个执行
即:有了GIL的存在,同一进程内的多个线程同一时刻只能有一个在运行,意味着在Cpyhon中,一个进程下的多个线程无法实现运行===》意味着无法利用多核优势
但不影响并发的实现
GIL可以被比喻成执行权限,同一进程下的所有线程要想执行都需要先抢执行权限
GIL vs 自定义互斥锁
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.) """ """ python解释器其实有多个版本 Cpython Jpython Pypypython 但是普遍使用的都是CPython解释器 在CPython解释器中GIL是一把互斥锁,用来阻止同一个进程下的多个线程的同时执行 同一个进程下的多个线程无法利用多核优势!!! 疑问:python的多线程是不是一点用都没有???无法利用多核优势 因为cpython中的内存管理不是线程安全的 内存管理(垃圾回收机制) 1.应用计数 2.标记清楚 3.分代回收 """ """ 重点: 1.GIL不是python的特点而是CPython解释器的特点 2.GIL是保证解释器级别的数据的安全 3.GIL会导致同一个进程下的多个线程的无法同时执行即无法利用多核优势(******) 4.针对不同的数据还是需要加不同的锁处理 5.解释型语言的通病:同一个进程下多个线程无法利用多核优势 """
from threading import Thread,Lock import time mutex = Lock() money = 100 def task(): global money # with mutex: # tmp = money # time.sleep(0.1) # money = tmp -1 mutex.acquire() tmp = money time.sleep(0.1) # 只要你进入IO了 GIL会自动释放 money = tmp - 1 mutex.release() if __name__ == '__main__': t_list = [] for i in range(100): t = Thread(target=task) t.start() t_list.append(t) for t in t_list: t.join() print(money) """ 100个线程起起来之后 要先去抢GIL 我进入io GIL自动释放 但是我手上还有一个自己的互斥锁 其他线程虽然抢到了GIL但是抢不到互斥锁 最终GIL还是回到你的手上 你去操作数据 """
GIL与Lock
GIL保护的是解释器级的数据,保护自己的数据则需要自己加锁处理
""" 多线程是否有用要看具体情况 单核:四个任务(IO密集型\计算密集型) 多核:四个任务(IO密集型\计算密集型) """ # 计算密集型 每个任务都需要10s 单核(不用考虑了) 多进程:额外的消耗资源 多线程:介绍开销 多核 多进程:总耗时 10+ 多线程:总耗时 40+ # IO密集型 多核 多进程:相对浪费资源 多线程:更加节省资源
from multiprocessing import Process from threading import Thread import os,time def work(): res = 0 for i in range(10000000): res *= i if __name__ == '__main__': l = [] print(os.cpu_count()) # 获取当前计算机CPU个数 start_time = time.time() for i in range(12): p = Process(target=work) # 1.4679949283599854 t = Thread(target=work) # 5.698534250259399 t.start() # p.start() # l.append(p) l.append(t) for p in l: p.join() print(time.time()-start_time) # 4 # 8.41348123550415
from multiprocessing import Process from threading import Thread import os,time def work(): time.sleep(2) if __name__ == '__main__': l = [] print(os.cpu_count()) # 获取当前计算机CPU个数 start_time = time.time() for i in range(4000): # p = Process(target=work) # 21.149890184402466 t = Thread(target=work) # 3.007986068725586 t.start() # p.start() # l.append(p) l.append(t) for p in l: p.join() print(time.time()-start_time)
""" 多进程和多线程都有各自的优势 并且我们后面在写项目的时候通常可以 多进程下面再开设多线程 这样的话既可以利用多核也可以介绍资源消耗 """

浙公网安备 33010602011771号