py知识(每日更新) 7.26

阻塞,非阻塞,异步,同步与协程

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()
'''
  1. 分析结果的过程是串行,效率低.
  2. 你将所有的结果全部都爬取成功之后,放在一个列表中,分析.

在开进程池,再开进程,耗费资源.

'''
爬取一个网页需要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阻
塞,计算密集型执行时间过长).
并发的本质:

  1. 遇到IO阻塞,计算密集型执行时间过长 切换.
  2. 保持原来的状态.
    一个线程实现并发.
    多进程: 操作系统控制 多个进程的多个任务切换 + 保持状态.
    多线程: 操作系统控制 多个线程的多个任务切换 + 保持状态.
    协程: 程序控制 一个线程的多个任务的切换以及保持状态.
    协程: 微并发, 处理任务不宜过多.
    协程它会调度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协程
特点: 开销小,数据共享,数据安全,不能利用多核,用户级别

猜你喜欢

转载自www.cnblogs.com/lyoko1996/p/11328878.html