百万年薪python之路 -- 并发编程之 多线程 三

老子叫甜甜 提交于 2019-11-28 17:19:22

1. 阻塞,非阻塞,同步,异步

进程运行的三个状态: 运行,就绪,阻塞.

从执行的角度:

阻塞: 进程运行时,遇到IO了,进程挂起,CPU被切走.

非阻塞: 进程没有遇到IO 当进程遇到IO,但我通过某种手段,让CPU强行运行我的进程

提交任务的角度:

同步: 提交一个任务,自任务开始运行直到此任务结束(可能有IO),返回一个返回值之后,我在提交下一个任务.

异步: 一次提交多个任务,然后我就直接执行下一行代码.

返回的结果,应该如何回收?

eg: 给三个老师发布任务:

​ 同步: 先告知第一个老师完成写书的任务,然后我在原地等待,等他两天之后完成了,告知我完事了,我才发布下一个任务........

​ 异步: 直接将三个任务告知三个老师,我就忙我的,知道三个老师完成之后,告知我.

2. 同步调用,异步调用

  1. 同步调用:
# 同步调用
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
import time
import random
import os

def task(i):
    print(f"{os.getpid()} 开始任务")
    time.sleep(random.randint(1,3))
    print(f"{os.getpid()} 任务结束")
    return i


if __name__ == '__main__':

    # 同步调用
    pool = ProcessPoolExecutor()
    for i in range(10):
        obj = pool.submit(task,i)
        # obj是一个动态对象,返回的是当前的对象的状态,有可能运行中,可能(就绪阻塞),还可能是结束了.
        # obj.result() 必须等到这个任务完成后,返回了结果之后,再能执行下一个任务.
        print(f"任务结果:{obj.result()}")

    pool.shutdown(wait=True)

    # shutdown : 让我的主进程等待进程池中的所有子进程都结束任务之后,再执行,有点类似于join
    # shutdown : 在上一个进程池没有完成所有的任务之前,不允许添加新的任务.
    # 一个任务是通过一个函数实现的,任务完成了,他的返回值就是函数的返回值.
    print('===main===')
  1. 异步调用:
# 异步调用
# 异步调用返回值如何接收? 未解决?

from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
import time
import random
import os

def task(i):
    print(f"{os.getpid()} 开始任务")
    time.sleep(random.randint(1,3))
    print(f"{os.getpid()} 任务结束")
    return i


if __name__ == '__main__':

    # 异步调用
    pool = ProcessPoolExecutor()
    for i in range(10):
        pool.submit(task,i)

    pool.shutdown(wait=True)

    # shutdown : 让我的主进程等待进程池中的所有子进程都结束任务之后,再执行,有点类似于join
    # shutdown : 在上一个进程池没有完成所有的任务之前,不允许添加新的任务.
    # 一个任务是通过一个函数实现的,任务完成了,他的返回值就是函数的返回值.
    print('===main===')
  1. 异步如何取结果:

    • 方式一:

      统一回收

      # 异步调用
      # 方式一: 异步调用,统一回收结果.
      
      from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
      import time
      import random
      import os
      
      def task(i):
          print(f"{os.getpid()} 开始任务")
          time.sleep(random.randint(1,3))
          print(f"{os.getpid()} 任务结束")
          return i
      
      
      if __name__ == '__main__':
      
          # 异步调用
          pool = ProcessPoolExecutor()
          lst = []
          for i in range(10):
              obj = pool.submit(task,i)
              lst.append(obj)
      
          pool.shutdown(wait=True)
      
          # shutdown : 让我的主进程等待进程池中的所有子进程都结束任务之后,再执行,有点类似于join
          # shutdown : 在上一个进程池没有完成所有的任务之前,不允许添加新的任务.
          # 一个任务是通过一个函数实现的,任务完成了,他的返回值就是函数的返回值.
          print(lst)
          for i in lst:
              print(i.result())
          print('===main===')
          # 统一回收结果: 不能马上收到任何一个已经完成的任务的返回值,只能等待所有的任务全部结束统一返回回收.

3. 异步调用+回调函数

首先以爬虫引入:

浏览器工作原理: 向服务器发送一个请求,服务端验证你的请求,如果正确,给你浏览器返回一个文件,浏览器接收文件,将文件里面的代码渲染成你看到的美丽漂亮的磨样.

什么是爬虫?

  1. 利用代码模拟一个浏览器,进行浏览器的工作流程得到一堆源代码.
  2. 对源代码进行数据清洗得到我想要的数据.
import requests
response = requests.get("http://www.baidu.com")
if response.status_code == 200:
    print(response.text)

版本一:

# 版本一:
from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor
import time
import random
import os
import requests

def task(url):
    """模拟的就是爬起多个源代码,一定会有IO操作(网络延迟)"""
    response = requests.get(url)
    if response.status_code == 200:
        return response.text

    
def parse(content):
    """模拟对数据的分析, 一般没有IO"""
    return len(content)


if __name__ == '__main__':
    # """串行 消耗时间长,不可取"""
    # ret = task("http://www.baidu.com")
    # print(parse(ret))
    # ret = task('http://www.JD.com')
    # print(parse(ret))
    #
    # ret = task('http://www.taobao.com')
    # print(parse(ret))
    #
    # ret = task('https://www.cnblogs.com/jin-xin/articles/7459977.html')
    # print(parse(ret))



    # 开启线程池,并发并行的执行
    url_list = [
                'http://www.baidu.com',
                'http://www.JD.com',
                'http://www.JD.com',
                'http://www.JD.com',
                'http://www.taobao.com',
                'https://www.cnblogs.com/jin-xin/articles/7459977.html',
                'https://www.luffycity.com/',
                'https://www.cnblogs.com/jin-xin/articles/9811379.html',
                'https://www.cnblogs.com/jin-xin/articles/11245654.html',
                'https://www.sina.com.cn/',
    ]
    pool = ThreadPoolExecutor(4)
    obj_list = []
    for url in url_list:
        obj = pool.submit(task,url)
        obj_list.append(obj)


    pool.shutdown(wait=True)
    for res in obj_list:
        print(parse(res.result()))


# 版本一的问题:
# 1.异步发出多个任务,并发的执行,但是统一的接收所有的任务的返回值.(效率低,不能实时的获取结果)
# 2. 分析结果流程是串行的,影响效率.

版本二:

# 版本二:
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
import time
import random
import os
import requests


def task(url):
    """模拟的就是爬起多个源代码,一定会有IO操作(网络延迟)"""
    response = requests.get(url)
    if response.status_code == 200:
        return parse(response.text)


def parse(content):
    """模拟对数据的分析, 一般没有IO"""
    return len(content)
    # print(len(content))


if __name__ == '__main__':

    # 开启线程池,并发并行的执行
    url_list = [
        'http://www.baidu.com',
        'http://www.JD.com',
        'http://www.JD.com',
        'http://www.JD.com',
        'http://www.taobao.com',
        'https://www.cnblogs.com/jin-xin/articles/7459977.html',
        'https://www.luffycity.com/',
        'https://www.cnblogs.com/jin-xin/articles/9811379.html',
        'https://www.cnblogs.com/jin-xin/articles/11245654.html',
        'https://www.sina.com.cn/',
    ]
    pool = ThreadPoolExecutor(4)
    obj_list = []
    for url in url_list:
        obj = pool.submit(task, url)
        obj_list.append(obj)

    #
    pool.shutdown(wait=True)
    for res in obj_list:
        print(res.result())

现在解决问题的两个方式:

  1. 再开一个线程进程池,并发并行的处理,但是再开一个进程线程池的开销

  2. 将原来的任务扩大

    版本一:

    • 线程池设置4个线程,异步发起10个任务,每个任务是通过网页获取源码,并发执行. 最后统一用到列表回收10个任务,串行着分析源码.

    版本二:

    • 线程池设置4个线程,异步发起10个任务,每个任务是通过网页获取源码 + 数据分析,并发并行,
    • 最后将所有的结果展示出来.
    • 耦合性增强了
    • 并发执行任务,此任务最好是IO阻塞,才能发挥最大的作用.

版本三:

可以为进程池或线程池内的每个进程或线程绑定一个函数,该函数在进程或线程的任务执行完毕后自动触发,并接收任务的返回值当作参数,该函数称为回调函数

# 版本三:
# 基于异步调用回收所有任务的结果,并且要做到实时回收结果,
# 并发执行任务,每个任务只有处理IO阻塞的,不能增加新功能
# 异步调用 + 回调函数


from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
import time
import random
import os
import requests


def task(url):
    """模拟的就是爬起多个源代码,一定会有IO操作(网络延迟)"""
    response = requests.get(url)
    if response.status_code == 200:
        return response.text


def parse(obj):
    """模拟对数据的分析, 一般没有IO"""
    print(len(obj.result()))


if __name__ == '__main__':

    # 开启线程池,并发并行的执行
    url_list = [
        'http://www.baidu.com',
        'http://www.JD.com',
        'http://www.JD.com',
        'http://www.JD.com',
        'http://www.taobao.com',
        'https://www.cnblogs.com/jin-xin/articles/7459977.html',
        'https://www.luffycity.com/',
        'https://www.cnblogs.com/jin-xin/articles/9811379.html',
        'https://www.cnblogs.com/jin-xin/articles/11245654.html',
        'https://www.sina.com.cn/',
    ]
    pool = ThreadPoolExecutor(4)
    for url in url_list:
        obj = pool.submit(task, url)
        obj.add_done_callback(parse)
    """
    线程池设置4个线程,异步发起10个任务,每个任务是通过网页获取源代码,并发执行,
    当一个任务完成之后,将parse这个分析代码的任务交给剩余的空闲的线程去执行,这个完成了获取源代码的线程继续去处理其他任务
    如果进程池 + 回调: 回调函数由主进程去执行.
    如果线程池 + 回调: 回调函数由空闲的线程去执行.
    """

Q:

异步和回调时一回事吗?

A:不是一回事,

异步是站在发布任务的角度,一次提交多个任务,然后就直接执行下一行代码.

而回调函数,站在接收结果的角度: 按照顺序接收每个任务的结果,进行下一步处理.

回调函数: 可以为进程池或线程池内的每个进程或线程绑定一个函数,该函数在进程或线程的任务执行完毕后自动触发,并接收任务的返回值当作参数,该函数称为回调函数

异步 + 回调:

异步处理的是IO类型,回调处理的是非IO类型

我们可以把耗时间(阻塞)的任务放到进程池中,然后指定回调函数(主进程负责执行),这样主进程在执行回调函数时就省去了I/O的过程,直接拿到的是任务的结果

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!