阻塞,非阻塞,异步,同步与协程
1.阻塞,非阻塞
1.1进程或线程在运行中表现的状态:
①阻塞
②运行
③就绪
1.2阻塞:
进程或线程遇到IO阻塞. 程序遇到IO立马会停止(挂起), cpu马上切换,等到IO
结束之后,在执行.
1.3非阻塞:
进程或线程没有IO或者 遇到IO通过某种手段让cpu去执行其他的任务,尽可
能的占用cpu.
2.异步,同步
站在任务发布的角度.
2.1同步
可以从两个方面去看:
①进程或线程间存在间接地相互制约关系
例如有一台打印机,进程A获取这台打印机后,进程B就会被阻塞,必须等待进程
A释放打印机后,进程B才能进入就绪状态,等待CPU执行.
②进程或线程间存在直接相互制约关系
这种情况源于进程或线程之间存在合作关系.
例如进程A通过单缓冲向进程B发送消息,当缓冲区为空时,进程B因获取不到
所需的数据而被阻塞,只有当进程A想缓冲区发送数据使缓冲区不为空时,进程
B才被唤醒;反过来,当缓冲区满时,进程A因无法向缓冲区放置数据而被阻塞,
只有当进程B从缓冲区接收数据,使缓冲区不满时,进程A才被唤醒.
结论:同步与阻塞在某些方面可以等价,如果两个进程构成同步关系,如果其中
一个进程不能正常工作或者被某些原因阻塞住,那么另一个进程也会迈向阻塞
的道路
2.2异步:
异步方式不用阻塞当前进程或线程来等待结果返回,而是允许后续操作,直
至其它进程或线程处理完毕后返回结果,然后通知此进程或线程去接收结果,
从这点来看异步与非阻塞在某些方面可以等价.
例如网络爬虫爬取图片时,线程A为主线程用于执行数据分析和其他的功能,线
程B为爬取线程,主要用于爬取网页信息,线程A执行过程中不会因为未接收到
线程B发送的网页信息而被阻塞,当线程B爬取完网页信息后会发送一个消息
通知线程A去进行数据分析.
2.1.1异步+ 调用机制
爬虫:
浏览器做的事情很简单:
浏览器 封装头部 发一个请求 ---> www.taobao.com(127.42.34.56) ---> 服
务器获取到请求信息,分析正确 ----> 给你返回一个文件.---> 浏览器将这个文
件的代码渲染,就成了你看的样子:
返回的文件:
爬虫: 利用requests模块功能模拟浏览器封装头,给服务器发送一个请求,骗过
服务器之后,服务器也给你返回一个文件. 爬虫拿到文件,进行数据清洗获取到
你想要的信息.
爬虫: 分两步,
第一步: 爬取服务端的文件(IO阻塞).
第二步: 拿到文件,进行数据分析,(非IO,IO极少)
import requests
from concurrent.futures import ProcessPoolExecutor
import time
import random
import os
def get(url):
response = requests.get(url)
print(f'{os.getpid()} 正在爬取:{url}')
time.sleep(random.randint(1,3))
if response.status_code == 200:
return response.text
def parse(text):
'''
对爬取回来的字符串的分析
简单用len模拟一下.
:param text:
:return:
'''
print(f'{os.getpid()} 分析结果:{len(text)}')
if __name__ == '__main__':
url_list = [
'http://www.taobao.com',
'http://www.JD.com',
'http://www.JD.com',
'http://www.JD.com',
'http://www.baidu.com',
'https://www.cnblogs.com/jinxin/articles/11232151.html',
'https://www.cnblogs.com/jinxin/articles/10078845.html',
'http://www.sina.com.cn',
'https://www.sohu.com',
'https://www.youku.com',
]
pool = ProcessPoolExecutor(4)
obj_list = []
for url in url_list:
obj = pool.submit(get, url)
obj_list.append(obj)
pool.shutdown(wait=True)
for obj in obj_list:
parse(obj.result())
'''
串行
obj_list[0].result()
obj_list[1].result()
obj_list[2].result()
obj_list[3].result()
obj_list[4].result()
'''
- 分析结果的过程是串行,效率低.
- 你将所有的结果全部都爬取成功之后,放在一个列表中,分析.
在开进程池,再开进程,耗费资源.
‘‘‘
爬取一个网页需要2s,并发爬取10个网页:2点多s.
分析任务: 一个任务1s,需10s. 总共12.多秒.
现在这个版本的过程:
异步发出10个爬取网页的任务,然后4个进程并发(并行)的先去完成4个爬取
网页的任务,然后谁先结束,谁进行下一个
爬取任务,直至10个任务全部爬取成功.
将10个爬取结果放在一个列表中,串行的分析.
爬取一个网页需要2s,分析任务: 1s,总共3s,总共3.多秒(开启进程损耗).
3.线程队列
3.1队列(FIFO先进先出)
3.2栈(LIFO后进先出)
下一个版本的过程:
异步发出10个 爬取网页+分析 的任务,然后4个进程并发(并行)的先去完
成4个爬取网页+分析 的任务,
然后谁先结束,谁进行下一个 爬取+分析 任务,直至10个爬取+分析 任务
全部完成成功.‘‘‘
import queue
q = queue.Queue(3)
q.put(1)
q.put(2)
q.put('太白')
print(q.get()) # 1
print(q.get()) # 2
print(q.get()) # 太白
3.3优先级队列
4.事件Event
并发的执行某个任务 .多线程多进程,几乎同时执行.
一个线程执行到中间时通知另一个线程开始执行.
import queue
q = queue.LifoQueue()
q.put(1)
q.put(3)
q.put('barry')
print(q.get()) # barry
print(q.get()) # 3
print(q.get()) # 1
需要元组的形式,(int,数据) int 代表优先级,数字越低,优先级越高.
import queue
q = queue.PriorityQueue(3)
q.put((10, '垃圾消息'))
q.put((-9, '紧急消息'))
q.put((3, '一般消息'))
print(q.get()) # (-9, '紧急消息')
print(q.get()) # (3, '一般消息')
print(q.get()) # (10, '垃圾消息')
import time
from threading import Thread
from threading import current_thread
from threading import Event
event = Event() # 实例化对象,默认是False
def task():
print(f'{current_thread().name} 检测服务器是否正常开
启....')
time.sleep(3)
event.set() # 改成了True
def task1():
print(f'{current_thread().name} 正在尝试连接服务器')
event.wait() # 轮询检测event是否为True,当其为True,继续
下一行代码. 阻塞.
event.wait(1)
设置超时时间,如果1s中以内,event改成True,代码继续执行.
设置超时时间,如果超过1s中,event没做改变,代码继续执行.
print(f'{current_thread().name} 连接成功')
if __name__ == '__main__':
t1 = Thread(target=task1,)
t2 = Thread(target=task1,)
t3 = Thread(target=task1,)
t = Thread(target=task)
t.start()
t1.start()
t2.start()
t3.start()
5.协程的初识
一个线程实现并发.
并发,并行,串行:
串行: 多个任务执行时,第一个任务从开始执行,遇到了IO等待,等待IO阻塞结
束之后,继续执行下一个任务.
并行: 多核,多个线程或者进程同时执行. 4个cpu,同时执行4个任务.
并发: 多个任务看起来是同时执行, cpu在多个任务之间来回切换(遇到IO阻
塞,计算密集型执行时间过长).
并发的本质:
- 遇到IO阻塞,计算密集型执行时间过长 切换.
- 保持原来的状态.
一个线程实现并发.
多进程: 操作系统控制 多个进程的多个任务切换 + 保持状态.
多线程: 操作系统控制 多个线程的多个任务切换 + 保持状态.
协程: 程序控制 一个线程的多个任务的切换以及保持状态.
协程: 微并发, 处理任务不宜过多.
协程它会调度cpu,如果协程管控的任务中,遇到阻塞,它会快速的(比操作系统
快)切换到另一个任务,并且能将上一个任务挂起(保持状态,),让操作系统以为
cpu一直在工作.
之前我们学过协程?yield 就是一个协程,
yield 虽然可以实现两个任务来回切换,并且能够保存原来的状态,而且还是一
个线程,但是 他只能遇到yield切换,遇到Io还是阻塞.
计算密集型:串行与协程的效率对比
1. 串行:
import time
def func1():
for i in range(11):
yield
print('这是我第%s次打印啦' % i)
time.sleep(1)
def func2():
g = func1()
#next(g)
for k in range(10):
print('哈哈,我第%s次打印了' % k)
time.sleep(1)
next(g)
#不写yield,下面两个任务是执行完func1里面所有的程序才会执行func2里面
的程序,
有了yield,我们实现了两个任务的切换+保存状态
func1()
func2()
协程:
串行
import time
def task1():
res = 1
for i in range(1,100000):
res += i
def task2():
res = 1
for i in range(1,100000):
res -= i
start_time = time.time()
task1()
task2()
print(f'串行消耗时间:{time.time()-start_time}') # 串行消耗时
间:0.012000560760498047
import time
def task1():
res = 1
for i in range(1, 100000):
res += i
yield res
def task2():
g = task1()
协程的优点 :
多线程并发: 一个进程如果要是开4个线程,最多可以处理30个任务.
多协程并发: 一个进程开启4个线程,然后我将4个线程设置4个协程,每个协程
可以执行30个任务.120个任务.(了解)
6.除yield之外两种协程的写法
6.1greenlet与switch
res = 1
for i in range(1, 100000):
res -= i
next(g)
start_time = time.time()
task2()
print(f'协程消耗时间:{time.time() - start_time}') # 协程消耗时
间:0.0260012149810791
#1. 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而
更加轻量级
#2. 单线程内就可以实现并发的效果,最大限度地利用cpu
#3. 修改共享数据不需加锁
除yield之外两种协程的写法
greenlet与switch
from greenlet import greenlet
import time
不能自动切换,
遇到IO不切换
# 可以保持原来的状态.
def eat(name):
print('%s eat 1' %name) #2
g2.switch('alex') #3 (任务的第一次切换一定要传参)
time.sleep(3)
print('%s eat 2' %name) #6
g2.switch() #7
def play(name):
print('%s play 3' %name) #4
g1.switch() #5
print('%s play 4' %name) #8
g1 = greenlet(eat)
g2 = greenlet(play)
g1.switch('太白') # 1 (任务的第一次切换一定要传参)
6.2gevent与monkey
可以保持原来的状态.
def eat(name):
print('%s eat 1' %name) #2
g2.switch('alex') #3 (任务的第一次切换一定要传参)
time.sleep(3)
print('%s eat 2' %name) #6
g2.switch() #7
def play(name):
print('%s play 3' %name) #4
g1.switch() #5
print('%s play 4' %name) #8
g1 = greenlet(eat)
g2 = greenlet(play)
g1.switch('太白') # 1 (任务的第一次切换一定要传参)
import threading
from gevent import monkey
monkey.patch_all() # 将你代码中的所有的IO都标识.
import gevent # 直接导入即可
import time
def eat():
print(f'线程1:{threading.current_thread().getName()}')
print('eat food 1')
time.sleep(3) # 加上mokey就能够识别到time模块的sleep了
print('eat food 2')
def play():
print(f'线程2:{threading.current_thread().getName()}')
print('play 1')
time.sleep(1) # 来回切换,直到一个I/O的时间结束,这里都是我们
个gevent做得,不再是控制不了的操作系统了。
print('play 2')
g1=gevent.spawn(eat) # 使用spawn执行任务
g2=gevent.spawn(play)
gevent.joinall([g1,g2]) # 执行完全部的线程
print(f'主:{threading.current_thread().getName()}')
7.1线程与进程与协程的对比总结
7.1.1进程
特点: 开销大,数据隔离,数据不安全,可以利用多核 操作系统级别,资源分配的
最小单位
7.1.2线程
特点: 开销小,数据共享,数据不安全,可以利用多核 操作系统级别,能被CPU调
度的最小单位
7.1.3协程
特点: 开销小,数据共享,数据安全,不能利用多核,用户级别
原文地址:https://www.cnblogs.com/lyoko1996/p/11328878.html