并发编程

CPU ----- 工厂

进程-------车间

线程 ------ 工人

CPU:主要承担了计算的任务

进程:单核CPU总是运行一个进程,其他进程处于非运行状态

线程:一个进程可以包括多个线程,一个进程的内存空间是被线程共享的,每个线程都可以使用这些共享内存。“互斥锁”:作用是防止多个线程同时读写某一块内存区域

 

进程:一个正在运行的应用程序在操作系统中被视为一个进程

在电脑上一边聊微信一边听歌的场景是因为CPU在交替运行着多个进程

在程序运行的过程中,由于被操作系统的调度算法控制,程序会进入几个状态:就绪、与逆行、阻塞和终止

 

并发与并行

并行:同时运行,只是具备多个cpu才能实现并行

并发:是伪并行,即看起来是同时运行

总结:并发的关键是你有处理多个任务的能力,不一定要同时

并行的关键是你有同时处理多个任务的能力

关键点:是否是同时

并行和并发指是否具有同时处理多个任务的能力

 

同步和异步

同步:调用一旦开始,调用者必须等到方法调用返回后,才能继续该方法后续的行为代码

异步:调用更像一个消息传递,一旦调用开始,该方法调用就会立即返回,调用者就可以继续后续的操作

同步和异步针对的是cpu遇到阻塞操作时,所产生的不同行为

python进程的实现

multiprocessing模块

是python中管理进程的包,multi是取自multiple的多功能的意思,在这个包中几乎包含了和进程有关的所有子模块。

Process模块是一个创建进程的模块,运行一个py文件就差相当于启动了一个进程,这个进程我们称为主进程,而在主进程对应的py文件中可以通过Process模块创建另一个进程,这个进程是基于主进程创建的,称之为子进程。

 

实现异步机制的步骤:

1、导入模块:

 from multiprocessing import Process

2、基于Process创建一个子进程对象(当前运行的整个py文件表示主进程),然后可以基于target参数将外部的一个函数(get_request)注册到该子进程中,args表示一个元组,是给外部函数get_request传参数

  p = Process(target=get_request,args=(i,))

3、基于start()方法启动创建好的子进程

 p.start()

完整代码:

 from multiprocessing import Process
 from time import sleep
 
 # 自定义一组任务
 def get_request(i):
     print('子进程开始%s'%i)
     sleep(0.5)
     print('子进程结束%s'%i)
 
 if __name__ == '__main__':
     # 创建一个进程,并且让进程表示了一组即将被执行的任务
     p = Process(target=get_request,args=('ss',))
     p.start()
     print('这里是主进程')
 
 # 运行结果:
 这里是主进程
 子进程开始
 子进程结束
 
 # 注意:当前的py原文件一定执行,则表示操作系统创建了一个进程,当前os创建的进程叫做主进程
 # 在主进程表示的应用内部,我们手动创建了另一个进程,这个进程叫做子进程

查看子进程与主进程是否是父子关系:

 # os.getpid()获取自己进程的ID号
 # os.getppid()获取自己进程的父进程的ID号
 
 from multiprocessing import Process
 from time import sleep
 import os
 # 自定义一组任务
 def get_request():
     print('子进程开始')
     sleep(0.5)
     print('子进程结束')
     print('当前子进程的ID:',os.getpid())
     print('当前子进程的父ID:',os.getppid())
 
 if __name__ == '__main__':
     # 创建一个进程
     p = Process(target=get_request)
     p.start()
     print('这里是主进程')
     print('当前主进程的ID:',os.getpid())
     
 # 运行结果:
 这里是主进程
 当前主进程的ID: 9548
 子进程开始
 子进程结束
 当前子进程的ID: 9016
 当前子进程的父ID: 9548
 
 主进程的ID = 子进程的父ID

给注册在子进程的函数传参

 from multiprocessing import Process
 
 # 自定义一组任务
 def get_request(url):
     print('这里是子进程')
     print('子进程参数:',url)
 
 if __name__ == '__main__':
     # 创建一个进程,并且让进程表示了一组即将被执行的任务
     p = Process(target=get_request,args=('www.baidu.com',))
     p.start()
     print('这里是主进程')
     
 # 运行结果:
 这里是主进程
 这里是子进程
 子进程参数: www.baidu.com

一个进程的生命周期:

 如果子进程的运行时间长,那么等到子进程执行结束后整个程序才结束
 如果主进程的执行时间长,那么主进程执行结束后整个程序才结束

 

 我们没有配置,主进程结束,子进程是不会跟着结束,自己会执行完
 如果配置主进程结束,子进程也跟着结束,那么就不会出现单独的子进程(孤儿进程)

主进程在子进程结束后结束

join()方法

当主进程需要使用子进程运行后的结果时,主进程会在加入join()方法的地方等待,会等待子进程执行结束之后,再继续往后执行主进程join后续的部分

 # 程序设计:车站一共有10张票,子进程花费2s购买了三张票,然后主进程需要在子进程购票结束后查询剩余车票数量
 
 from multiprocessing import Process
 from time import sleep
 total = 10
 def get_tickets(num):
     print('准备购买车票%s张'%num)
     sleep(2)
     print('成功购买了%d张票'%num)
     global total
     total -= num
 
 if __name__ == '__main__':
     p = Process(target=get_tickets,args=(2,))
     p.start()
     print('这里是主进程')
     p.join()
     print('剩余车票:',total)
 
 # 运行结果:
 这里是主进程
 准备购买车票2张
 成功购买了2张票
 剩余车票: 10
 
  问题:剩余车票数total并没有修改?
 因为进程之间的数据是隔离的,也就是数据不共享,主进程与子进程的内存空间是两个地方
 解决方法:使用外部文件或者数据库进行数据的读写
 

join()两种不同位置的调用

需求:将所有的子进程全部执行完之后,再结束主进程

 错误示例:实现的是类似同步的效果
 from multiprocessing import Process
 from time import sleep
 def func(i):
    print('子进程%s开始'%(i))
    sleep(2)
    print('子进程%s结束!!!!!' % (i))
 
 if __name__ == '__main__':
    for i in range(3):
        p = Process(target=func,args=(i,))
        p.start()
        p.join()
    print('这里是主进程')
     
 运行结果:
 子进程0开始
 子进程0结束!!!!!
 子进程1开始
 子进程1结束!!!!!
 子进程2开始
 子进程2结束!!!!!
 这里是主进程
 
 该操作是结束一个进程才进行下一个进程
 # 将所有的子进程全部执行完之后,再结束主进程
 from multiprocessing import Process
 from time import sleep
 def func(i):
     print('子进程%s开始'%(i))
     sleep(1)
     print('子进程%s结束!!!!!' % (i))
 
 if __name__ == '__main__':
     p_list = []
     for i in range(3):
         p = Process(target=func,args=(i,))
         p.start()
         p_list.append(p)
     for p in p_list:
         p.join()      # 将三个进程都结束后在执行print
     print('这里是主进程')
     
 运行结果:
 子进程0开始
 子进程1开始
 子进程2开始
 子进程0结束!!!!!
 子进程2结束!!!!!
 子进程1结束!!!!!
 这里是主进程

进程创建的第二种方式(继承)

自定义类继承Process

重写父类的run方法,作为进程的执行体

在类外部调用start方法

 from multiprocessing import Process
 from time import sleep
 
 class Func(Process):
    def __init__(self, name):
        super().__init__()
        self.name = name
         
  # 重写父类方法
    def run(self):
        print('子进程%s开始执行'%self.name)
        sleep(2)
        print('子进程%s结束!!!'%self.name)
 
 if __name__ == '__main__':
  # 创建一个自定义的进程对象
    t = Func('lili')
     
    # 启动进程对象(调用run)
    t.start()
    print('主进程')
 
 运行结果:
 主进程
 子进程lili开始执行
 子进程lili结束!!!

守护进程

主进程结束,主进程创建的子进程也都跟着结束,避免出现孤儿进程

主进程代码运行结束,守护进程随机终止

 # 主进程创建守护进程后:
 守护进程会在主进程代码执行结束后终止
 守护进程内无法再开启子进程,否则抛出异常:AssertionError: daemonic processes are not allowed to have children

基本语法:

 子进程对象.daemon = True

实例:

 from multiprocessing import Process
 from time import sleep
 
 class Func(Process):
    def __init__(self, name):
        super().__init__()
        self.name = name
 
    def run(self):
        print('子进程%s开始执行'%self.name)
        sleep(2)
        print('子进程%s结束!!!'%self.name)
 
 if __name__ == '__main__':
    t = Func('lili')
    t.daemon = True
    t.start()
    print('主进程')
     
 运行结果:
 主进程

进程同步(锁)

加锁流程:

 导包:from multiprocessing import Lock
 创建一个锁对象
 加锁:lock.acquire()
 解锁:lock.release()

加锁后由并发变成了串行,牺牲了运行效率,但避免了竞争导致的数据错乱

 from multiprocessing import Process
 from multiprocessing import Lock
 from time import sleep
 
 lock = Lock()
 def func(i,lock):
    lock.acquire()
    print('子进程%s开始'%(i))
    sleep(1)
    print('子进程%s结束!!!!!' % (i))
    lock.release()
 
 if __name__ == '__main__':
    p_list = []
    for i in range(3):
        p = Process(target=func,args=(i,lock))
        p.start()
        p_list.append(p)
    for p in p_list:
        p.join()     # 将三个进程都结束后在执行print
    print('这里是主进程')
     
  运行结果:
 子进程0开始
 子进程0结束!!!!!
 子进程1开始
 子进程1结束!!!!!
 子进程2开始
 子进程2结束!!!!!
 这里是主进程
 
 加锁前的运行结果:
 子进程0开始
 子进程1开始
 子进程2开始
 子进程0结束!!!!!
 子进程2结束!!!!!
 子进程1结束!!!!!
 这里是主进程

虽然使用加锁的形式实现了顺序的执行,但是程序有重新变成串行了,这样确实会浪费时间,却保证了数据的安全

 #注意:首先在当前文件目录下创建一个名为db的文件
 #文件db的内容为:{"count":1}表示先有余票,只有这一行数据,并且注意,每次运行完了之后,文件中的1变成了0,你需要手动将0改为1,然后在去运行代码。
 
 # 并发运行,效率高,但竞争写同一文件,数据写入错乱
 from multiprocessing import Process
 from time import sleep
 import json
 
 def get_tacket():
    with open('./db',mode='r',encoding='utf-8') as f:
        dic = json.load(f)
    count = dic['count']
    if count <=0:
        sleep(0.05)
        print('抢票失败,已没有余票')
    else:
        count-=1
        dic = {"count":count}
        with open('./db',mode='w',encoding='utf-8') as f:
            json.dump(dic,f)
        sleep(0.05)
        print('抢票成功')
 def search():
    with open('./db',mode='rt',encoding='utf-8') as fp:
        dic = json.load(fp)
        print('目前余票%s张'%(dic['count']))
 
 def task():
    search()
    get_tacket()
 
 if __name__ == '__main__':
    for i in range(5):
        p = Process(target=task)
        p.start()
         
 运行结果:
 目前余票3张
 目前余票2张
 目前余票1张
 抢票成功
 抢票成功
 目前余票0张
 抢票成功
 目前余票0张
 抢票失败,已没有余票
 抢票失败,已没有余票
 
 

加锁后

 from multiprocessing import Process
 from multiprocessing import Lock
 from time import sleep
 import json
 
 lock = Lock()
 def get_tacket():
    with open('./db',mode='r',encoding='utf-8') as f:
        dic = json.load(f)
    count = dic['count']
    if count <=0:
        sleep(0.05)
        print('抢票失败,已没有余票')
    else:
        count-=1
        dic = {"count":count}
        with open('./db',mode='w',encoding='utf-8') as f:
            json.dump(dic,f)
        sleep(0.05)
        print('抢票成功')
 def search():
    with open('./db',mode='rt',encoding='utf-8') as fp:
        dic = json.load(fp)
        print('目前余票%s张'%(dic['count']))
 
 def task(lock):
    lock.acquire()
    search()
    get_tacket()
    lock.release()
 
 if __name__ == '__main__':
    for i in range(5):
        p = Process(target=task,args=(lock,))
        p.start()
         
 运行结果:
 目前余票3张
 抢票成功
 目前余票2张
 抢票成功
 目前余票1张
 抢票成功
 目前余票0张
 抢票失败,已没有余票
 目前余票0张
 抢票失败,已没有余票

同步异步例题

知识点:

 import requests
 # 从网页复制图片的地址
 img_url = 'https://img2.baidu.com/it/u=1726860177,1101781943&fm=26&fmt=auto&gp=0.jpg'
 
 # 对指定的资源发起网络请求
 # content返回的是服务器返回来的二进制数据
 # text返回的是字符串类型的数据
 img_data = requests.get(url = img_url).content
 with open('./123.jpg',mode='wb') as f:
     f.write(img_data)

如何通过网络请求下载网络数据:

server端:

 from flask import Flask
 import time
 
 # 创建一个flash实例对象
 app = Flask(__name__)
 
 # 添加一个路由地址
 @app.route('/img1')
 def op_img1():
     time.sleep(2)
     return 'i am img1 data'
 
 # 添加一个路由地址
 @app.route('/img2')
 def op_img2():
     time.sleep(2)
     return 'i am img2 data'
 
 # 添加一个路由地址
 @app.route('/img3')
 def op_img3():
     time.sleep(2)
     return 'i am img3 data'
 
 if __name__ == '__main__':
     app.run(host = '192.168.21.144',port = 8898)

客户端:同步操作(比较耗时)

 from multiprocrssing import Process
 import time
 import requests
 
 start = time.time()
  urls = [
      'http://192.168.21.144:8898/img1',
      'http://192.168.21.144:8898/img2',
      'http://192.168.21.144:8898/img3'
  ]
 
 def install_img(url):
  data = requests.get(url).text
  print(data)
 
 for url in urls:
  install_img(url)
 print(time.time() - start)
 
 运行结果:
 i am img1 data
 i am img2 data
 i am img3 data
 6.043097257614136
 

客户端:异步操作

 from multiprocessing import Process
 import time
 import requests
 
 def install_img(url):
  data = requests.get(url).text
  print(data)
 
 if __name__ == '__main__':
  start = time.time()
  urls = [
      'http://192.168.21.144:8898/img1',
      'http://192.168.21.144:8898/img2',
      'http://192.168.21.144:8898/img3'
      ]
  p_list = []
  for url in urls:
  p = Process(target = install_img,args = (url,))
  p.start()
  p_list.append(p)
  [p.join() for p in p_list]
  print(time.time() - start)
 
 运行结果:
 i am img1 data
 i am img2 data
 i am img3 data
 2.3538880348205566

python线程的实现

线程是操作系统能够进行运算调度的最小单位(相当于车间里的工人),被包含在进程中,线程是进程中的实际运作单位,一个进程可以并发多个线程,每条线程并行执行不同的任务。

注:

 同一个进程内的多个线程是共享该进程的资源的,不同进程内的线程资源肯定是隔离的
 创建线程的开销比创建进程的开销要小得多
 每一个进程中至少会包含有一个线程,该线程叫做‘主线程’

多线程可不可以实现并行?

 在CPU比较繁忙,资源不足(开启很多进程)时,操作系统只为一个含有多线程的进程分、
 配仅有的CPU资源,这些线程就会为自己尽量多抢时间片,这就是通过多线程实现并发,线程之间会竞争CPU资源争取执行机会
 在CPU资源比较充足的时候,一个进程内的多线程,可以被分配到不同的CPU资源,这就是通过多线程实现并行

用于多线程的模块,包括:thread、threading和Queue

 thread和threading模块允许程序员创建和管理线程
 thread模块提供了基本的线程和锁的支持
 threading提供了更高级别、功能更强的线程管理的功能
 Queue模块允许用户创建一个可以用于多个线程之间共享数据的队列数据结构
 
 与更高级别的threading模块更为先进,对线程的支持更为完善

线程创建的两种方式:

方式一:

 from threading import Thread
 import time
 
 def func(i):
    print('子进程%s开始'%i)
    time.sleep(2)
    print('子进程%s结束'%i)
 
 if __name__ == '__main__':
    th = Thread(target=func, args=('lili',))
    th.start()
    th.join()
    print('这里是主进程')
     
  运行结果:
 子进程lili开始
 子进程lili结束
 这里是主进程

方式二(继承)

 from threading import Thread
 import time
 
 class Func(Thread):
    def __init__(self, name):
        super().__init__()
        self.name = name
 
    def run(self):
        print('子进程%s开始' % self.name)
        time.sleep(2)
        print('子进程%s结束' % self.name)
 
 
 if __name__ == '__main__':
    th = Func('lili')
    th.start()
    th.join()
    print('这里是主进程')
     
 运行结果:
 子进程lili开始
 子进程lili结束
 这里是主进程

join()方法的使用与进程一样,等待子进程运行结束再运行join()方法后续的代码

线程内存数据共享:

 from threading import Thread
 import time
 
 def func(num):
    global total
    print('正在购买车票%s张' % num)
    time.sleep(2)
    total -= num
 
 if __name__ == '__main__':
    total = 10
    th = Thread(target=func, args=(2,))
    th.start()
    th.join()
    print('剩余车票%s张' % total)
     
 运行结果:
 正在购买车票2张
 剩余车票8张

守护线程:

无论是进程还是线程,都遵循:守护xx会在主xx运行完毕后被销毁,不管守护xx什么时候被执行结束

基本语法:

 子线程对象.daemon = True

多线程使用

 from threading import Thread
 import time
 
 def func(num):
    print('正在购买车票第%s张' % num)
    time.sleep(0.5)
    print('购票成功!!!!!')
 
 if __name__ == '__main__':
    th_list = []
    for i in range(1,4):
        th = Thread(target=func, args=(i,))
        th.start()
        th_list.append(th)
    [th.join() for th in th_list]
    print('这里是主进程')
 from threading import Thread
 import time
 
 def get_request(url):
    print('正在请求:',url)
    time.sleep(2)
    print('请求结束:',url)
 if __name__ == '__main__':
    start = time.time()
    urls = [
        'wwww.1.com',
        'wwww.2.com',
        'wwww.3.com'
    ]
    ths = []
    for url in urls:
        t = Thread(target=get_request,args=(url,))
        t.start()
        ths.append(t)
    [th.join() for th in ths]
    print('总耗时:',time.time()-start)

线程的GIL锁

python加了一个GIL全局解释锁,锁的是整个线程,而不是线程里面的某些数据,也就是说每次只能有一个线程使用CPU,就是多线程用不了多核实现并行

在同一进程中只有一个线程可以获取CPU的使用权限,那么其他的线程就必须等待该线程的CPU使用权消失后才能使用CPU,多个线程之间不会相互影响,在同一个进程下也只有一个线程使用CPU,这样的机制称为全局解释器锁(GIL)

每一个python线程,在CPython解释器中执行时,都会先索住自己的线程,阻止别的线程执行

GIL的优点

 避免了大量的加锁解锁的繁琐操作
 使数据更加安全,解决多线程间的数据完整性和状态同步

GIL的缺点

 多核处理器的效果退化成单核处理器,只能并发不能并行

总结:

 多CPU,意味着可以有多个核并行完成计算,所以多核提升的是计算性能
 每个CPU一旦遇到I/O阻塞,仍然需要等待,所以多核对I/O操作没什么用处
 对于计算来说,CPU越多越好,但对于I/O来说,再多的CPU也没用
 
 对运行一个程序来说,随着CPU的增多执行效率肯定会有所提高(不管提高幅度多大,总会有所提高),这是因为一个程序基本上不会是纯计算或者纯I/O,所以我们只能相对的去看一个程序到底是计算密集型还是I/O密集型,从而进一步分析python的多线程到底有无用武之地

多进程 VS 多线程

(1)计算密集型:多进程效率高

 # 多进程
 from multiprocessing import Process
 import time
 
 def get_request():
    sum = 1
    for i in range(1,100000):
        sum *= i
 
 if __name__ == '__main__':
    start = time.time()
    ths = []
    for i in range(10):
        t = Process(target=get_request)  
        t.start()
        ths.append(t)
    [th.join() for th in ths]
    print('总耗时:',time.time()-start)
     
 # 总耗时: 13.434641599655151

 

 # 多线程
 from threading import Thread
 import time
 
 def get_request():
    sum = 1
    for i in range(1,100000):
        sum *= i
 
 if __name__ == '__main__':
    start = time.time()
    ths = []
    for i in range(10):
        t = Thread(target=get_request)  
        t.start()
        ths.append(t)
    [th.join() for th in ths]
    print('总耗时:',time.time()-start)
     
  # 总耗时: 26.398848056793213

密集I/O型:多线程效率高

 # 多进程
 from multiprocessing import Process
 import time
 
 def get_request():
    for i in range(100000):
        print('*************')
 
 if __name__ == '__main__':
    start = time.time()
    ths = []
    for i in range(10):
        t = Process(target=get_request)
        t.start()
        ths.append(t)
    [th.join() for th in ths]
    print('总耗时:',time.time()-start)
     
  总耗时: 7.7796854972839355

同步锁

一个进程中的一个线程只能使用一个CPU,要保证一个线程对应的操作在一段时间内被完整的直接完毕所加入的锁就是同步锁

为什么使用同步锁?

有可能当一个线程在使用CPU时,该线程下的程序可能会遇到I/O阻塞操作,那么CPU就会切换到别的线程上去,这样就有可能会影响到该程序结果的完整性

使用:

 # 加锁前
 from threading import Thread
 import time
 
 def get_tackets():
    global total_tackets
    if total_tackets < 1:
        time.sleep(0.5)
        print('已无余票,抢票失败!')
    else:
        time.sleep(0.5)
        print('抢票成功!')
        time.sleep(0.5)
        total_tackets -= 1
 
 def search():
    print('剩余%s张车票'%total_tackets)
 
 def task():
    search()
    get_tackets()
 
 if __name__ == '__main__':
    total_tackets = 3
    for i in range(5):
        t = Thread(target=task)
        t.start()
 
 运行结果:        
 剩余3张车票
 剩余3张车票
 剩余3张车票
 剩余3张车票
 剩余3张车票
 抢票成功!抢票成功!
 抢票成功!
 抢票成功!
 抢票成功!
 以上就是因为竞争带来的数据错乱

 

 # 加锁后
 from threading import Thread
 from multiprocessing import Lock
 import time
 
 def get_tackets():
    global total_tackets
    if total_tackets < 1:
        time.sleep(0.5)
        print('已无余票,抢票失败!')
    else:
        time.sleep(0.5)
        print('抢票成功!')
        time.sleep(0.5)
        total_tackets -= 1
 
 def search():
    print('剩余%s张车票'%total_tackets)
 
 def task(lock):
    lock.acquire()
    search()
    get_tackets()
    lock.release()
 
 if __name__ == '__main__':
    lock = Lock()
    total_tackets = 3
    for i in range(5):
        t = Thread(target=task,args=(lock,))
        t.start()
 
 运行结果:
 剩余3张车票
 抢票成功!
 剩余2张车票
 抢票成功!
 剩余1张车票
 抢票成功!
 剩余0张车票
 已无余票,抢票失败!
 剩余0张车票
 已无余票,抢票失败!
 

总结:保护不同的数据就应该加不同的锁

 GIL与Lock是两把锁,保护的数据不一样,前者是解释器级别的(当然保护的就是解释器级别的数据,比如垃圾回收的数据),后者是保护用户自己开发的应用程序的数据,很明显GIL不负责这件事,只能用户自定义加锁处理,即Lock

线程池

线程预先被创建并放入前程池中,同时处理完当前任务之后并不销毁而是被安排处理下一个任务,因此能够避免多次创建线程,从而节省线程创建和销毁的开销,能带来更好的性能和系统稳定性

 from multiprocessing.dummy import Pool # 线程池
 import time
 
 def func(url):
    print('正在请求:',url)
    time.sleep(1)
    print('请求结束:',url)
    return url
 
 urls=[
    'www.1.com',
    'www.2.com',
    'www.3.com',
    'www.4.com',
    'www.5.com',
    'www.6.com'
 ]
 start = time.time()
 
 # 创建线程池对象
 pool = Pool(2)
 
 # 一组任务对应的函数必须只能有一个参数(这里是url)
 # map函数的return返回值(每一组任务返回的结果result_list)是一个列表
 result_list = pool.map(func,urls)
 print(result_list)
 
 print('总耗时:',time.time() - start)

生产者消费者模式

生产者:负责造数据的任务

消费者:接收造出来的数据进行进一步操作

为什么要使用消费者模式呢?

 在并发编程中,如果生产者处理速度很快,而消费者处理速度比较慢,那么生产者就必须等待消费者处理完,才能继续生产数据
 如果消费者的处理能力大于生产者,那么消费者就必须等待生产者
 为了解决这个等待的问题,引入生产者和消费者模型,让他们之间可以不停的生产和消费

程序设计:动态生成10个网址,假设每个网址对应的页面中存在20张图片。则现在需要将每一个页面中的图片数据下载,然后再将图片数据进行持久化存储操作。

  • 下载图片就是在生产数据

  • 将图片保存就是在消费数据

 from threading import Thread
 from queue import Queue
 from time import sleep
 
 class Producer(Thread):
    def __init__(self,urlQueue,imgQueue):
        super().__init__()
        self.urlQueue = urlQueue
        self.imgQueue = imgQueue
 
    def run(self):
        while True:
            if self.urlQueue.empty():
                break
            else:
                print('正在生产%s这个网页中的20张图片数据',self.urlQueue.get())
                self.downloadImgData()
 
    def downloadImgData(self): # 生产每个网址中的20张图片
        for i in range(20):
            sleep(0.5)
            img_data = 'XXXX%d'%i
            self.imgQueue.put(img_data)
 
 class Customer(Thread):
    def __init__(self,urlQueue,imgQueue):
        super().__init__()
        self.urlQueue = urlQueue
        self.imgQueue = imgQueue
 
    def run(self):
        while True:
            if self.imgQueue.empty() and self.urlQueue.empty():
                break
            else:
                img_data = self.imgQueue.get()
                sleep(0.5)
                print(img_data)
 
 if __name__ == '__main__':
    urlQueue = Queue(10) # 用于存储网址
    imgQueue = Queue(50) # 每个网址中下载下来的图片数
 
    url = 'www.qq%d.com'
    for i in range(1,11):
        new_url = format(url%i)
        urlQueue.put(new_url) # 创建模拟网址加入队列
 
    for i in range(5): # 多个生产者异步生产数据
        p = Producer(urlQueue,imgQueue)
        p.start()
 
    for i in range(10): # 多个消费者异步消费数据
        c = Customer(urlQueue,imgQueue)
        c.start()
 
 

协程 asyncio

多任务的异步协程

如何创建一个协程:

1、特殊的函数:

如果一个函数的定义被async关键字修饰,就被视为是一个特殊的函数

 async def func():
  print('i am func')
  return 'hello world'

特殊之处:

1)特殊函数调用后,函数内部的程序语句不会被立即执行

2)特殊函数调用后,会返回一个对象(协程对象)

2、协程对象:

特殊的函数 == 一组指定形式的操作 == 协程对象

 import asyncio
 # 特殊的函数
 async def func():
     print('i am func')
     return 'hello world'
 
 # 返回一个协程对象
 c = func()
 print(c)
 
 运行结果:
 <coroutine object func at 0x0000017C52F0DBC0>
 sys:1: RuntimeWarning: coroutine 'func' was never awaited
 

3、任务对象:

就是一个高级的协程对象

任务对象 == 协程对象 == 特殊的函数 == 一组指定形式的操作

 import asyncio
 # 特殊的函数
 async def func():
     print('i am func')
     return 'hello world'
 
 # 返回一个协程对象
 c = func()
 print(c)
 # 创建一个任务对象
 task = asyncio.ensure_future(c)
 print(task)
 
 运行结果:
 <coroutine object func at 0x00000208B37FDBC0>
 <Task pending name='Task-1' coro=<func() running at T:/python35期全栈资料/练习.py:4>>
 

4、事件循环

事件循环对象loop.就是一个数据结构(容器)

可以将创建好的任务对象注册到事件循环对象表示的容器中

一旦启动了事件循环对象,则其内部存储/注册的任务对象就会被异步的执行

 import asyncio
 # 特殊函数
 async def func():
     print('i am func')
     return 'hello world'
 
 # 创建协程对象
 c = func()
 print(c)
 
 # 创建任务对象
 task = asyncio.ensure_future(c)
 
 # 创建一个事件循环对象
 loop = asycnio.get_event_loop()
 # 将指定的任务对象注册到loop,且启动loop
 loop.run_until_complete(task)
 
 运行结果:
 <coroutine object func at 0x000002236C36DB40>
 <Task pending name='Task-1' coro=<func() running at T:/python35期全栈资料/练习.py:4>>
 i am func

任务对象是一个高级的协程对象,高级之处在于:

可以给任务对象绑定一个回调函数(只有当对应的任务对象调用结束后,回调函数才会被调用)

回调函数必须只有一个参数,参数表示的就是给其绑定的任务对象

 import asyncio
 # 特殊函数
 async def func():
     print('i am func')
     return 'hello world'
 
 # 自定义的普通函数
 def callback(t):  # t == task
     # result()返回的就是任务对象对应特殊函数内部实现的返回值
     result = t.result()
     print(result)
 
 # 创建一个协程对象
 c = func()
 # 创建一个任务对象
 task = asyncio.ensure_future(c)
 # 给当前的任务对象绑定一个回调函数
 task.add_done_callback(callback)
 # 创建一个事件循环对象
 loop = asyncio.get_event_loop()
 # 将指定的任务对象注册到loop,且启动loop
 loop.run_until_complete(task)
 
 运行结果:
 i am func
 hello world
 

任务对象回调函数的作用:

可以将任务对象执行后的结果捕获,进行后续的操作(?)

asyncio.wait(tasks):tasks列表,列表中存储的是一个又一个任务对象

.wait()可以给tasks列表中每一个任务对象赋予一个可以被挂起的权限

挂起:如果给一个任务对象赋予可被挂起的权限后,则该任务在发生阻塞后既可以被挂起。可以被挂起指让当前的任务对象自己主动交出CPU的使用权

实现多个任务对象的异步操作:

 import asyncio
 # 特殊函数
 async def get_request(url):
  print('正在请求:',url)
  await asyncio.sleep(2)
  print('请求结束:',url)
 
 urls = [
     'www.1.com',
     'www.2.com',
     'www.3.com'
 ]
 tasks = []
 
 for url in urls:
     # 创建协程对象
  c = get_request(url)
     # 创建任务对象
  task = asyncio.ensure_future(c)
  tasks.append(task)
 # 创建事件循环对象
 loop = asyncio.get_event_loop()
 # 将指定的任务对象注册到loop,且启动loop
 loop.run_until_complete(asyncio.wait(tasks))
 
 运行结果:
 正在请求: www.1.com
 正在请求: www.2.com
 正在请求: www.3.com
 请求结束: www.1.com
 请求结束: www.2.com
 请求结束: www.3.com

多任务异步模型下:

注意:特殊函数内部,不可以出现不支持异步的模块代码,否则就会中断整个的一部效果,如time.sleep(2)

如果特殊函数内部有阻塞操作,则阻塞操作前必须使用await修饰才可以

 

posted @ 2021-12-11 21:41  多啦a梦与时光机  阅读(55)  评论(0编辑  收藏  举报