并发编程:同步异步、队列、协程与实现方式

本文目录:

一、进程池

二、同步异步

三、利用回调完成生产者消费者

四、线程队列

五、协程实现

六、greenlet使用

七、gevent使用

 

一、进程池

前言

在利用python来进行系统管理的时候,特别是同事操作多个文件或目录时,或者远程控制主机时,并行可以节约大量时间,多进程是很好的实现并发的方法。

但也会出现以下问题:

1.如果遇到大量的IO操作(如从文件或硬盘读取/写入数据等)就不适合多进程

2.实现多任务的调度、并发的程序明显大于核数,操作系统不可能无限开启进程

3.进程开启过多也无法实现真正意义上的并行(进程会消耗系统资源)

结论:所以我们就需要一个容器来装一定数量的进程

什么是进程池

通过一个容器(理解为池子)来控制进程的数量,规定最小和最大的进程数,对于远程过程调用的高级应用程序而言,应该使用进程池,Pool可以提供指定数量的进程,供用户调用,当有新的请求提交到pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中有进程结束,就重用进程池中的进程

怎么用进程池

from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor
import os,time
# 默认按照cpu的核数
# pool = ProcessPoolExecutor()
#
# def task():
#     print("%s is running" % os.getpid())
#     time.sleep(0.2)
#
# if __name__ == '__main__':
#     for i in range(20):
#         pool.submit(task)
#     print("11111")




# 默认按照cpu的核数
pool = ThreadPoolExecutor()


# 方式1 自己来保存数据 并执行shutdown    仅在多线程

# res = []
# def task():
#     print("%s is 正在打水" % os.getpid())
#     time.sleep(0.2)
#     w = "%s 打的水" % os.getpid()
#     res.append(w)
#     return w
#
# if __name__ == '__main__':
#     for i in range(20):
#         # 提交任务会返回一个对象  用于回去执行状态和结果
#         f = pool.submit(task)
#         print(f.result()) # 方式2   执行result 它是阻塞的直到任务完成  又变成串行了
#
#     print("11111")
#     # pool.shutdown() # 首先不允许提交新任务 然后等目前所有任务完成后
#     # print(res)
#     print("over")



# 默认按照cpu的核数
pool = ThreadPoolExecutor()


# 方式3 通过回调(什么是回调 任务执行结束后自动调用某个函数)


def task():
    print("%s is 正在打水" % os.getpid())
    # time.sleep(0.2)
    w = "%s 打的水" % os.getpid()
    return w


def task_finish(res):
    print("打水完成! %s" % res)


if __name__ == '__main__':
    for i in range(20):
        # 提交任务会返回一个对象  用于回去执行状态和结果
        f = pool.submit(task)
        f.add_done_callback(task_finish) #添加完成后的回调
    print("11111")
    # pool.shutdown() # 首先不允许提交新任务 然后等目前所有任务完成后
    # print(res)
    print("over")

 

二、同步异步

什么是同步

一般是在多任务或者并行中,简单理解,你经常去做大宝剑那家会所,只有老板娘一个人,先在门口吆喝你,然后你进去后老板娘给你寒暄了几句(“死鬼好久不来看我了!”)又给你到了杯水,之后领你去了小黑屋做宝剑按摩,结束后到柜台收银再欢送你走!整个流程(吆喝-接待-倒水-按摩-收银)全是老板娘一个人做,这是同步任务,如果生意好点你必须等待上一个任务结束。

同步调用,知道本次任务执行完毕拿到res,等待任务work执行的过程中可能有阻塞也可能没有阻塞,但不管该任务是否存在阻塞,同步调用都会在原地等着,只是等的过程中若是任务发生了阻塞就会被夺走cpu的执行权限

from multiprocessing import Pool
import os,time
def work(n):
    print('%s run' %os.getpid())
    time.sleep(3)
    return n**2

if __name__ == '__main__':
    p=Pool(3) #进程池中从无到有创建三个进程,以后一直是这三个进程在执行任务
    res_l=[]
    for i in range(10):
        res=p.apply(work,args=(i,)) 
        res_l.append(res)
    print(res_l)

同步调用apply

什么是异步

异步就是,许久之后你发现会所门口人多了起来,老板娘人好活棒,名声在外,很多人慕名而来,会所的员工也增加了不少,你进来后发现有前台小妹接待你,但你不能立马进小黑屋,(别问为什么?生意火爆)小妹领你进了休息室,后面有服务生给你倒了水,没过多久有位漂亮的技师向你走来,领你进了玻璃房(what?where is 小黑屋?社会主义和谐社会,想什么呢?)然后一顿操作后去前台付了钱,你休息片刻起身要走,这时老板娘认出你了,步行到跟前和你扯了几句,然后笑脸送你出门,(走后,你低落了一会儿,心想老板娘变了,她不叫你死鬼了!),这时人多了(任务多了),拉你进休息室(任务池子)技师领你进玻璃房(处理任务的线程)你发现有好多技师(多进程)这些任务不再是老板娘一个人从头做到尾了,每个节点都有对应处理进程!即时给你响应!

from multiprocessing import Pool
import os,time
def work(n):
    print('%s run' %os.getpid())
    time.sleep(3)
    return n**2

if __name__ == '__main__':
    p=Pool(3) #进程池中从无到有创建三个进程,以后一直是这三个进程在执行任务
    res_l=[]
    for i in range(10):
        res=p.apply_async(work,args=(i,)) #同步运行,阻塞、直到本次任务执行完毕拿到res
        res_l.append(res)

    #异步apply_async用法:如果使用异步提交的任务,主进程需要使用jion,等待进程池内任务都处理完,然后可以用get收集结果,否则,主进程结束,进程池可能还没来得及执行,也就跟着一起结束了
    p.close()
    p.join()
    for res in res_l:
        print(res.get()) #使用get来获取apply_aync的结果,如果是apply,则没有get方法,因为apply是同步执行,立刻获取结果,也根本无需get

异步调用apply_async
# 同步和异步提交的简单实例
def
task(): for i in range(1000000): i += 1000 print("11111") print("start") task() # 同步提交方式 print("end") from threading import Thread print("start1") Thread(target=task).start() # 异步提交 print("end1")

 简单小结:

"""
    线程的三种状态:
        1.就绪
        2.运行
        3.阻塞

    阻塞 遇到了IO操作  代码卡主 无法执行下一行  CPU会切换到其他任务

    非阻塞 与阻塞相反  代码正在执行(运行状态) 或处于就绪状态
    阻塞和非阻塞描述的是运行的状态

    同步 :提交任务必须等待任务完成,才能执行下一行
    异步 :提交任务不需要等待任务完成,立即执行下一行
    指的是一种提交任务的方式
"""

 

三、利用回调完成生产者消费者

from  concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor
from threading import current_thread
import  os
pool = ThreadPoolExecutor()

# 爬虫  1.从网络某个地址获取一个HTML文件

import requests # 该模块用于网络(HTTP)请求

# 生产数据
def get_data_task(url):
    print(os.getpid(),"正在生产数据!")
    # print(current_thread(),"正在生产数据!")

    response = requests.get(url)
    text = response.content.decode("utf-8")
    print(text)
    return text


#   处理数据
def parser_data(f):
    print(os.getpid(),"处理数据")
    # print(current_thread(), "处理数据")
    print("正在解析: 长度%s" % len(f.result()))


urls = [
    "http://www.baidu.com",
    "http://www.baidu.com",
    "http://www.baidu.com",
    "http://www.baidu.com"
]

if __name__ == '__main__':
    for url in urls:
        f = pool.submit(get_data_task,url)
        f.add_done_callback(parser_data)  # 回调函数是主进程在执行
        # 因为子进程是负责获取数据的  然而数据怎么处理 子进程并不知道  应该把数据还给主进程
    print("over")

 

四、线程队列

import queue

# 普通队列 先进先出
q = queue.Queue()
q.put("a")
q.put("b")


print(q.get())
print(q.get())

# 堆栈队列  先进后出 后进先出  函数调用就是进栈  函数结束就出栈 递归造成栈溢出
q2 = queue.LifoQueue()
q2.put("a")
q2.put("b")
print(q2.get())


# 优先级队列
q3 = queue.PriorityQueue()  # 数值越小优先级越高  优先级相同时 比较大小 小的先取
q3.put((-100,"c"))
q3.put((1,"a"))
q3.put((100,"b"))
print(q3.get())

 

五、协程实现

为什么会有协程

cpu在运行任务时,会在两种情况下切走执行权,一种是该任务发生阻塞,另外一种情况是赶任务计算时间过长或有一个优先级更高的程序替代了它

对于单线程下,我们不可避免程序中出现io操作,但如果我们能在自己的程序中(即用户程序级别,而非操作系统级别)控制单线程下的多个任务能在一个任务遇到io阻塞时就切换到另外一个任务去计算,这样就保证了该线程能够最大限度地处于就绪态,即随时都可以被cpu执行的状态,相当于我们在用户程序级别将自己的io操作最大限度地隐藏起来,从而可以迷惑操作系统,让其看到:该线程好像是一直在计算,io比较少,从而更多的将cpu的执行权限分配给我们的线程。

协程的本质就是单线程下,由用户自己控制一个任务遇到IO阻塞了就切换另外一个任务去执行,以此来提升效率,为实现它我们需要满足一下两个条件:

1.可以控制多个任务之间的切换,切换之前将任务的状态保存下来,以便从新运行,可以暂停的位置上继续执行

2.作为1的补充:可以检测IO操作,遇到io操作的情况下才发生切换


什么是协程

就是单线程下实现并发,又叫做微线程,是一种用户态的轻量级线程,及协程由用户程序自己调度的

协程的优缺点

优点:应用层序级别速度远远高于操作系统的切换

缺点:多个任务一旦有一个阻塞没有切,整个线程都阻塞在原地

   该线程内的其他任务也就不执行了

(注意:一旦引入协程,就需要检测单线程下所有的IO行为,实现遇到IO就切换,少一个都不行,以为一旦一个任务阻塞了,整个线程就阻塞了,其他的任务即便是可以计算,但是也无法运行了)

协程的目的:

  想要在单线程下实现并发
  并发指的是多个任务看起来是同时运行的
  并发=切换+保存状态

用yieid模拟程序1阻塞时切换到程序2执行
import time
def task():
    while True:
        print("task1")
        time.sleep(4)
        yield 1


def task2():
    g = task()
    while True:
        try:
            print("task2")
            next(g)
        except Exception:
            print("任务完成")
            break
task2()

 

六、greenlet使用

import greenlet

import time
def task1():
    print("task1 1")
    time.sleep(2)
    g2.switch()
    print("task1 2")
    g2.switch()

def task2():
    print("task2 1")
    g1.switch()
    print("task2 2")

g1 = greenlet.greenlet(task1)
g2 = greenlet.greenlet(task2)

g1.switch()

# 1.实例化greenlet得到一个对象 传入要执行的任务
#   至少需要两个任务
# 2.先让某个任务执行起来 使用对象调用switch
# 3.在任务的执行过程中 手动调用switch来切换
#

 

七、gevent使用

Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。

from gevent import monkey
monkey.patch_all()

import gevent
import time
def eat():
    print('eat food 1')
    time.sleep(2)
    # gevent.sleep(1)
    print('eat food 2')

def play():
    print('play 1')
    time.sleep(1)
    # gevent.sleep(1)
    print('play 2')

g1=gevent.spawn(eat)
g2=gevent.spawn(play)
# g1.join()
# g2.join()

gevent.joinall([g1,g2])
print('')

# 1.spawn函数传入你的任务
# 2.调用join 去开启任务
# 3.检测io操作需要打monkey补丁  就是一个函数 在程序最开始的地方调用它

资料参考:

https://www.cnblogs.com/linhaifeng/articles/7428874.html#_label11

原文地址:https://www.cnblogs.com/wuzhengzheng/p/10273445.html