同步、异步、阻塞、非阻塞概念
前言
在实际的开发中,经常会听到同步,异步,阻塞,非阻塞这些编程概念,每次遇到的时候都会蒙圈,尤其是在一些场景下同步与阻塞,异步与非阻塞感觉没啥区别,但其实这四个术语描述的事物还真不是一回事。
同步和异步
- 同步/异步描述的是消息通信机制
同步方法表明:
就是在发出一个调用时 在没有得到结果之前 该调用就不返回
异步方法表明:
特点:针对每次都是一次同步的调用
调用后 没有得到想要的返回(或者不care返回值) 而是通过后面的回调、状态通知的方式获得结果
特点:通过回调callback、状态或者通知的方式告知调用方结果
阻塞和非阻塞
- 阻塞/非阻塞描述的是程序在等待调用结果(消息、返回值)时的状态
阻塞方法表明:
调用方线程在等待结果返回过程中 线程被挂起(调用方不能处理其它事情)等结果返回后 唤醒线程
阻塞特点:调用方被阻塞
非阻塞方法表明:
调用指在不能立刻得到结果之前 该调用不会阻塞当前线程
特点:调用方没有被阻塞
组合术语
- 医院举例
- 同步+阻塞
去医院挂号 医院的提示铃声坏了 你怕错过什么事也不做(阻塞)一直紧紧盯着当前的就诊号消息(同步) 这叫同步阻塞 效率最低
- 同步+非阻塞
也是去医院的提示铃声坏了 你看着人多便开始刷剧(非阻塞)但是也时不时看一下当前的就诊号消息(同步)这叫同步非阻塞 时间利用率较高 实际上效率低下
- 异步+阻塞
医院的提示铃声修好了 到一定时间它自己会广播提示(异步)但是你也什么也不做也不看当前就诊信息 就发呆(阻塞)这叫异步阻塞 效率和同步阻塞没什么两样
- 异步+非阻塞
医院的提示铃声修好了 到一定时间它自己会广播提示(异步)但是你也什么也不做也不看当前就诊信息 就发呆(阻塞)这叫异步阻塞 效率最高 时间利用率低
创建进程的多种方式
1.双击桌面程序图标
2.代码创建进程
函数版
同步状态:
import time
def task(name):
print('task is running', name)
# 睡个3秒
time.sleep(3)
print('task is over', name)
if __name__ == '__main__':
task('www') # 同步
异步状态:
from multiprocessing import Process
import time
def task(name):
print('task is running',name)
# 睡三秒
time.sleep(3)
print('task is over',name)
if __name__ == '__main__':
# p1 = Process(target=task, args=('jason',)) # 第一种传参方式:位置参数(注意这里需要是元组)
p1 = Process(target=task, kwargs={'name':'jason123'}) # 第二种传参方式:关键字参数
p1.start() # 异步 告诉操作系统创建一个新的进程 并在该进程中执行task函数
print('主')
打印结果:
不同的操作系统中创建进程底层原理不一样
- Windows
以导模块的形式创建进程 - Linux/mac
以拷贝代码的形式创建进程
面向对象版
from multiprocessing import Process
import time
class MyProcess(Process):
def __init__(self, name, age):
# 这里主要是为了传参才定义的双下init,但是super需要在上面,因为调用process中的init的时候会给对象的属性绑定一个默认值
super().__init__()
self.name = name
self.age = age
def run(self):
print('run is running', self.name, self.age)
time.sleep(3)
print('run is over', self.name, self.age)
if __name__ == '__main__':
obj = MyProcess('tony', 123)
obj.start() # 异步操作,告诉操作系统创建一个新的进程 并在该进程中执行task函数
print('主')
进程间数据隔离
同一台计算机上的多个进程双击是严格意义上的物理隔离(默认情况下)
from multiprocessing import Process
import time
money = 1000
def task():
global money
money = 666
print('子进程的task函数查看money', money)
if __name__ == '__main__':
p1 = Process(target=task)
p1.start() # 创建子进程
time.sleep(3) # 主进程代码等待3秒
print(money) # 主进程代码打印money
结果如下:
上面我们提到,在windows中创建进程是相当于导模块的操作,因此我们可以看成子进程的代码相当于在另外一个py文件中执行,虽然用上了global改变全局变量,因为跟主进程不在一个文件,可以看成产生了数据隔离。
进程的join方法
用上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=('jason1', 1))
p2 = Process(target=task, args=('jason2', 2))
p3 = Process(target=task, args=('jason3', 3))
# p.start() # 异步
'''主进程代码等待子进程代码运行结束再执行'''
# p.join()
# print('主')
start_time = time.time()
p1.start()
p1.join()
p2.start()
p2.join()
p3.start()
p3.join()
# p1.join()
# p2.join()
# p3.join()
print(time.time() - start_time) # 3秒多
IPC机制
- IPC(Inter-Process Communication):进程间通信
- 进程间通信——队列(multiprocess.Queue)
- 创建共享的进程队列,Queue是多进程安全的队列,可以使用Queue实现多进程之间的双击传递
- 说直观一点就是可以创建一个可以用于多进程间双击传输的队列
from multiprocessing import Queue
q = Queue(3) # 括号内可以指定存储数据的个数
# 往消息队列中存放数据
q.put(111)
# print(q.full()) # 判断队列是否已满
q.put(222)
q.put(333)
# print(q.full()) # 判断队列是否已满
# 从消息队列中取出数据
print(q.get())
print(q.get())
# print(q.empty()) # 判断队列是否为空
print(q.get())
# print(q.empty()) # 判断队列是否为空
# print(q.get())
print(q.get_nowait())
"""
full() empty() 在多进程中都不能使用!!!
"""
from multiprocessing import Process, Queue
def product(q):
q.put('子进程p添加的数据')
def consumer(q):
print('子进程获取队列中的数据', q.get())
if __name__ == '__main__':
q = Queue()
# 主进程往队列中添加数据
# q.put('我是主进程添加的数据')
p1 = Process(target=consumer, args=(q,))
p2 = Process(target=product, args=(q,))
p1.start()
p2.start()
print('主')
生产消费者模型
简介
在并发编程中使用生产者和消费者模式能够解决大多数并发问题。该模式通过平衡生产先程和消费线程的工作能力来提高程序的整体处理数据的速度
"""
生产者
负责输出数据的人
消费者
负责处理数据的人
"""
为什么要使用生产者和消费者模式
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式
什么是输出消费者模式
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力
扩展
当一个进程运行起来后,我们可以在cmd中,使用tasklist命令查看当前运行的进程信息。
PID(Process Identification)操作系统里指进程识别号,也就是进程标识符。操作系统里每打开一个程序都会创建一个进程ID,即PID
守护进程
简介
是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。Linux系统的大多数服务器就是通过守护进程实现的。常见的守护进程包括系统日志进程syslogd、 web服务器httpd、邮件服务器sendmail和数据库服务器mysqld等。
主进程创建守护进程
其一:守护进程会在主进程代码执行结束后就终止
其二:守护进程内无法再开启子进程,否则抛出异常:
from multiprocessing import Process
import time
def task(name):
print('德邦总管:%s' % name)
time.sleep(3)
print('德邦总管:%s' % name)
if __name__ == '__main__':
p1 = Process(target=task, args=('dzh',))
p1.daemon = True
p1.start()
time.sleep(1)
print('恕瑞玛皇帝:xwy嗝屁了')
僵尸进程与孤儿进程
僵尸进程
进程执行完毕后并不会立刻销毁所有的数据,会有一些信息短暂保留下来
比如进程号、进程执行时间、进程消耗功率等给父进程查看
ps:所有的进程都会变成僵尸进程(不过吧就是存在的时间不长,可能就几秒钟)
孤儿进程
子进程正常运行,父进程意外死亡(比如我们跑到cmd中用taskkill关掉父进程),操作系统针对孤儿进程会派遣福利院管理那么这些进程将会成为孤儿进程。孤儿进程将会被init进程(进程号为1)所收养,并由init进程对他们完成状态收集工作。
多进程数据错乱问题
模拟抢票软件
from multiprocessing import Process
import time
import json
import random
# 查票
def search(name):
with open(r'data.json', 'r', encoding='utf8') as f:
data = json.load(f)
print('%s在查票 当前余票为:%s' % (name, data.get('ticket_num')))
# 买票
def buy(name):
# 再次确认票
with open(r'data.json', 'r', encoding='utf8') as f:
data = json.load(f)
# 模拟网络延迟
time.sleep(random.randint(1, 3))
# 判断是否有票 有就买
if data.get('ticket_num') > 0:
data['ticket_num'] -= 1
with open(r'data.json', 'w', encoding='utf8') as f:
json.dump(data, f)
print('%s买票成功' % name)
else:
print('%s很倒霉 没有抢到票' % name)
def run(name):
search(name)
buy(name)
if __name__ == '__main__':
for i in range(10):
p = Process(target=run, args=('用户%s'%i, ))
p.start()
通过上面的代码,运行之后我们发现虽然设置成只有1张票,但是每个人都会显示买到票了。这个时候就出现了数据错乱。
但是有的时候又会发现,有时候又是正常的逻辑顺序进行抢票。
多进程操作数据很可能会造成数据错乱,解决方案>>>:互斥锁
互斥锁
将并发变成串行,牺牲了效率但是保障了数据的安全