并发编程
1.操作系统的发展史
2.进程
2.1多道技术
时间和空间上的复用
CPU时间
内存空间(硬件设备)
实现:切换+保存状态
CPU切换的2种情况(操作系统会取消该程序的CPU使用权限):
1.程序在执行IO操作时,
作用:提高CPU利用率
2.程序长时间占用CPU
作用:降低了CPU效率,但让其他程序得到运行
2.2进程的调度
3种机制:
1.先来先服务调度算法:对长作业有利
2.短作业优先调度算法:对短作业有利
3.时间片轮转法+多级反馈队列:对整体有利,用户感官好
时间片轮转法+多级反馈队列:
程序先运行指定时间,然后降低优先级,放入下一层,去运行另一个程序;当这一层的程序都执行完或放入下一层,去下一层继续执行一开始没执行完的程序;当上一层又有程序进来的时候,会放弃当前程序,去执行优先级最高的那一层的程序。
注:
长作业,短作业是指运行时间的长短
2.3进程的3个状态
1.就绪态
2.运行态
3.阻塞态
2.4 两个重要的概念
同步和异步
"""描述的是任务提交的方式(提交后的状态)"""
同步:任务提交后,原地等待返回结果,就相当于卡住
异步:任务提交后,直接去做别的事;任务返回结果有一个异步回调机制自动处理
阻塞和非阻塞
"""描述的是程序的运行状态"""
阻塞:阻塞态
非阻塞:就绪态、运行态
这2个概念通常组合使用,其中异步非阻塞是最高效的
2.5创建进程的2种方式
"""创建进程方案1"""
from multiprocessing import Process
name ='egon'
time.sleep(3)
def my_print(name):
print(f"{name}是傻逼")
print('dddddddddd')
if __name__ == '__main__':
p=Process(target=my_print,name='自定义',args=('tank',)) #开辟另一个进程,指定要运行的函数并传参和设置进程名,不指定则新进程只运行模块代码
p.daemon = True #将子进程设置为守护进程,一定要写在p.start前面
p.start() #运行该进程
#p.join() #将2个进程改为同步执行,后面讲
p.terminate() #杀死当前进程,操作系统要一定的时间才会执行
p.is_alive #判断当前进程是否存活,返回True or False
print('ffffffffff')
"""
1.windoes系统中,开辟进程会先开辟内存空间,再将原文件先当作模块执行,再去执行传入的函数体代码,为了避免循环开辟进程,要将开辟进程的代码写到__mian__下面.
2.Linux系统中,开辟进程会先开辟内存空间,将函数体代码直接放进去运行,就不用这样写.
3.上面2个进程就是异步执行的.
上面运行结果为:
dddddddddd #原进程的打印
fffffffffff
dddddddddd #新进程的打印
tank是傻逼 #函数体代码最后执行
"""
"""创建进程方案2"""
from multiprocessing import Process
class Fun(Process): #自定义一个类,继承Process
def run(self) -> None: #一定要有run
print('skkkkkk')
if __name__ == '__main__':
p = Fun() #开辟另一个进程
p.start() #运行该进程
2.6join
在p.start()后面加上p.join(),可以让原进程等待新进程运行完再继续执行;如果有多个新进程,新进程之间仍然是独立的,但原进程和每个新进程之间都是同步的。
2.7进程间数据的相互隔离
不同进程间的数据默认是相互隔离的
2.8进程对象及其操作方法
1.查看进程PID:
print(p.pid) #返回当前进程的PID
from multiprocessing import Process,current_process
print(current_process().pid) #返回当前进程的PID
import os
print(os.getpid()) #返回当前进程的PID
print(os.getppid()) #返回当前进程的父进程的PID,谁启动了我这个进程谁就是我的父进程
在windows系统中:
tasklist #查看当前所有进程
tasklist | findstr PID #可以用来查找指定的进程
TASKKILL /PID 数值 #用来杀死指定进程
2.杀死进程
p.terminate() #杀死当前进程,操作系统要一定的时间才会执行
3.判断当前进程是否存活
p.is_alive #返回True or False
4.查看进程的名字
p.name #在实例化的时候可以修改进程的名字,使用关键字传参
2.9僵尸进程和孤儿进程(了解)
"""僵尸进程"""
子进程死了,并不会立刻释放占用的进程号(为了让父进程能查看其基本信息),此时称其为僵尸进程。所有的进程都会步入僵尸进程。
该子进程的PID最终尤其父进程处理
"""孤儿进程"""
父进程意外死亡,但子进程还在,此时,称子进程为孤儿进程。
孤儿进程会由操作系统统一回收资源。
2.10守护进程
定义:随被守护进程的死亡而立刻死亡的进程,称守护进程。
p.daemon = True #将子进程设置为守护进程,一定要写在p.start前面s
2.11互斥锁
"""概念"""
多个进程同时操作一份数据的时候,会出现数据错乱。
针对上述问题,解决方案就是加锁:将并发变成串行,牺牲效率换来数据安全。
互斥锁只用于数据处理,能不用就不用。改串行会大幅降低程序效率。
行锁,表锁也都是相同的本质。
from multiprocessing import Lock ,Process
import time
import random
def read(i,mutex):
mutex.acquire() #加锁
time.sleep(random.randint(1,3)) #锁中间的代码会会从并发改成串行
print(f'{i}')
mutex.release() #解锁
'''这4行代码也可以写成:
with mutex:
time.sleep(random.randint(1,3)) #锁中间的代码会会从并发改成串行
print(f'{i}')
'''
if __name__ == '__main__':
mutex =Lock() #实例化一个锁
for i in range(1,5):
p=Process(target=read,args=(i,mutex))
p.start()
"""
不加锁,进程并发,输出顺序是乱的。
加锁后,一次只有一个进程能执行加锁的代码,故输出是有顺序的1,2,3,4
注意:这只是用法的展示,例子本身没有任何意义。
"""
2.12队列
"""队列和堆栈"""
队列:先进先出
堆栈:先进后出
from multiprocessing import Process ,Queue
q=Queue(5) #生成一个队列,设置队列大小,不设置有默认值
q.put(111) #放入数据,没有空间了就会等,程序阻塞
q.put('111',block=False) #相当于q.put_nowait('111')
q.put(222,timeout=3)
q.put_nowait()
print(q.full()) #判断队列是否满了,返回True or False
print(q.empty()) #判断队列是否空了,返回True or False
print(q.qsize()) #判断队列中有多少数据
res =q.get() #取出数据,没有数据就会一直等,程序阻塞
q.get(timeout=3) #取出数据,没有数据就等3秒,再报错,时间可调
q.get_nowait() #取出数据,没有数据就报错
另一种方法:(这种方法产生的队列不能实现进程间的通信)
import queue
q=queue.Queue(5) #生成一个队列,设置队列大小,不设置有默认值
2.13IPC机制
功能:实现进程间的通信
本质:利用不同进程之间使用同一个队列的原理,一个进程存,另一个进程取,以此来实现进程间通信。
2.14生产者消费者模型
"""什么是生产者消费者模式"""
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
"""基于队列实现生产者消费者模型"""
from multiprocessing import Process,JoinableQueue #用JoinableQueue代替Queue
q.task_done() #向q.join()发送一次信号,证明一个数据已经被取走了
q.join() #只有当队列中没有数据的时候,才会继续向下执行,否则会阻塞在这
#再将消费者设置成守护模式,即可以让消费者随主进程一起结束
3.线程
3.1简介
进程是资源单位
线程是执行单位
"""为什么要有线程?"""
开设线程的开销较小,更快
同一个进程下的线程资源共享,即公用同一个内存空间
3.2开启线程的2种方法
#模板1
from threading import Thread
def my_print(name):
print(f'{name}出生了')
if __name__ == '__main__':#开启线程不需要放在main下面也能执行,但一般还是写在main下面。
p=Thread(target=my_print,args=('egon',))
p.start()
#模板2
from threading import Thread
class Mythead(Thread):
def run(self) -> None: #一定要写成run
print('sb')
t=Mythead()
t.start()
3.3用多进程实现 TCP并发效果
将通信循环写入进程或线程
3.4线程的join方法
在p.start()后面加上p.join(),可以让原进程等待新线程运行完再继续执行;和进程间的join方法一样,异步改同步。
3.5同一个程下的数据是共享的
同一进程下不同线程可以公用进程数据的
3.6线程对象属性及其方法
import time
from threading import Thread,current_thread,active_count
def fun():
time.sleep(1)
print('线程名:',current_thread().name) #线程的名字,相当于t.name
t = Thread(target=fun,)
t.start()
print('主线程名:',current_thread().name)
print(active_count()) #统计当前正在活跃的线程数
"""结果"""
主线程名: MainThread
2
线程名: Thread-1(子线程的名字会一直Thread-1,Thread-2,Thread-3这样排序)
3.7守护线程
和守护进程一样
t.setDaemon(True)
3.8线程互斥锁
和进程一样的用法
3.9GIL全局解释器锁
1.GIL不是python的特点,而是Cpyhton的特点
2.GIL保证的是解释器级别的数据安全
Cpython中内存管理不是线程安全的,如果不加互斥锁,在给变量赋值的时候,有可能垃圾回收机制也在工作,导致错误
3.同一进程下多线程无法同时运行,无法利用多核优势,但可以快速切换,伪多线程
4.解释型语言的通病
5.针对不同的数据还是要加不同的锁处理(自己加锁是为了避免由于阻塞而自动切换线程)
注释:GIL本质就是给python解释器加了锁,使得线程间只能串行,保证了数据内存的安全
3.10多进程和多线程的比较
多核:
计算密集型:多进程更快
IO密集型:多线程避免了开辟进程,更省资源
通常我们会结合使用
3.11死锁和递归锁(了解)
死锁:2把锁,一人抢了一把,还都要强另一把,就卡死了
递归锁:RLock模块
1.可以连续的acquire和realse
2.只能被第一次抢的人使用
3.内置计数器,acquire+1,realse-1,当计数为0时,释放锁
3.12信号量(了解)
#相当于是多把锁,用法和锁一样
import time
from threading import Thread,Semaphore #Semaphore模块
def fun():
s.acquire()
print('kkk')
time.sleep(1)
s.release()
s =Semaphore(5) #指定锁的个数,即同时运行的线程数
for i in range(1,9):
t = Thread(target=fun,)
t.start()
3.13Event事件(了解)
#让一个线程等待另一个线程发送信号后再执行
import time
from threading import Thread, Event #Event模块
def fun():
print('kkk')
time.sleep(2)
event.set() #给另一个线程发信号
def fun2():
event.wait() #等待另一个线程的信号,接收到之后再继续执行
print("等啊等")
event = Event() #实例化
t = Thread(target=fun, )
t.start()
t2 = Thread(target=fun2, )
t2.start()
3.14各种不同的Q模块(了解)
queue.Queue #队列,先进先出
queue.LifoQueue #堆栈,先进后出
queue.PriorityQueue #优先级Q,可以给数据设置优先级,放入的是(序号,'数据'),取出的也是元组,序号越小越优先,支持负数。
#上述用法是一样的
3.15进程池和线程池(******)
"""什么是池"""
池是用来在保证计算机硬件安全的前提下最大程度的利用计算机
它降低了程序运行的效率,但保证了计算机硬件的安全,从而让程序能正常运行
模板:
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import time
"""无论是进程池还是线程池,当其中的进程或线程开辟后,就一直是那几个,不会再注销重新开辟"""
"""线程池"""
pool = ThreadPoolExecutor(5) # 设置开设的线程数,默认是CPU核数的5倍
def task(n):
time.sleep(2)
print("这是低%d个线程" % n)
return n # 线程的返回值
res_list = []
for i in range(20):
res = pool.submit(task, i) # 提交任务并传参,任务间是异步的,返回值res是一个对象
# print(res.result()) #获得任务返回值
res_list.append(res)
pool.shutdown() # 等待当前所有线程执行完毕,关闭线程池
for res in res_list:
print(res.result())
"""进程池"""
pool = ProcessPoolExecutor(5) # 设置开设的进程数,默认是CPU核数
def task(n):
time.sleep(2)
print("这是低%d个线程" % n)
return n # 进程的返回值
def My_print(obj): # obj是自动传入的结果对象
print(obj.result())
if __name__ == '__main__':
for i in range(20):
# 提交任务并传参,任务间是异步的,后面是异步回调机制,当有返回值的时候自动执行括号内函数
pool.submit(task, i).add_done_callback(My_print) # 返回值res是一个对象
3.16协程(了解)
协程:单线程实现并发,又称微线程
1.协程是由用户程序自己控制调度的,进程和线程都是操作系统控制的。
2.本质:单线程内开启协程,一旦遇到io,就会从应用程序级别(即代码级别而非操作系统)控制切换,以此来提升效率
2种实现方法:
greenlet模块:
from greenlet import greenlet
def fun1():
print('fffff')
g2.switch() # 跳转到g2
print('gggggg')
def fun2():
print('hhhhhh')
g1.switch() # 跳转到g1
g1 = greenlet(fun1) # 声明g1,g2
g2 = greenlet(fun2)
g1.switch() # 跳转到g1,括号内可以给函数传参
gevent模块:
import gevent
from gevent import monkey
monkey.patch_all()
import time
def fun1():
print('fffff')
time.sleep(1)
print('gggggg')
def fun2():
print('hhhhhh')
time.sleep(1) # 跳转到g1
g1 = gevent.spawn(fun1) # 声明g1,g2
g2 = gevent.spawn(fun2)
g1.join()
g2.join()
4 IO模型
4.1阻塞非阻塞模型
阻塞:accept,recv,recvfrom会等待链接建立或返回数据
非阻塞:不再等待,直接返回结果,没有结果时,就抛出异常,可以使程序一直执行,不进入阻塞态。强占CPU,但通常都是在循环(空转),没有实际操作。实际情况下并不会使用。
通过调用socket对象的属性来实现:server.setblocking(False)
4.2 IO多路复用
程序发送请求后,操作系统会有一个监管机制,当有结果的时候,操作系统会发送提示,程序再发送请求,即可获得结果。
4.3异步IO
效率最高,使用最广
补充
1.字典取值用dict.get("key"),不要用中括号。因为如果值不存在,get返回None,[]则会报错