python并发编程之协程,线程/进程池,IO模型,同步异步,阻塞非阻塞

一、基于多线程实现并发通信

服务端:

import socket
from threading import Thread

def communicate(conn, addr):
    # 通信循环
    while True:
        try:
            data = conn.recv(1024)
            if len(data) == 0:
                break
            print(data)
            conn.send(data.upper())
        except ConnectionResetError:
            print("客户端已断开",addr)
            break

def link(ip, port, back_log=5):
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind((ip, port))
    server.listen(back_log)
    # 链接循环
    while True:
        conn, addr = server.accept()
        print('新的连接',addr)
        # 每来一个客户端就开一个线程
        t = Thread(target=communicate, args=(conn, addr))
        t.start()


if __name__ == '__main__':
    s = Thread(target=link, args=('127.0.0.1',8080))
    s.start()

客户端:

import socket

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8080))

while True:
    msg = input(">>:").strip()
    if len(msg) == 0:
        continue
    client.send(msg.encode('utf-8'))
    data = client.recv(1024)
    print(data)

虽然利用多线程可以实现并发,但是,来一个客户端我们就要开一个线程,假设是一个亿级PV的网站,那么需要开多少的线程呢?我们的服务器是否能够满足需要?因此,稳妥起见,我们应该给连接池设置一个上限。 

二、进程池和线程池的使用

池:
    为了减缓计算机硬件的压力,避免计算机硬件设备崩溃
    虽然减轻了计算机硬件的压力,但是一定程度上降低了持续的效率
进程池线程池:
      为了限制开设的进程数和线程数,从而保证计算机硬件的安全

进程池/线程池的使用:

我们需要导入concurrent.futures模块下的ThreadPoolExecutor和ProcessPoolExecutor

from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
import time
import os
# 实例化池对象
# 不指定参数的情况,默认是当前计算机cpu个数乘以5,也可以指定线程个数
pool = ThreadPoolExecutor(5)
# pool = ProcessPoolExecutor(5)  # 创建进程池,默认是当前计算机的cpu个数,其他与线程池代码一样

def task(n):
    print(n, os.getpid())
    time.sleep(2)
    return n ** 2

if __name__ == '__main__':
    t_ls = []
    for i in range(20):
        future = pool.submit(task, i)
        # print(future)  # 如果在此处直接打印结果,会使得每次执行都要等待结果,变成同步提交
        t_ls.append(future)
    # 关闭池子并且等待池子中所有的任务运行完毕,再去拿列表中的结果
    pool.shutdown()
    for p in t_ls:
        print('>>>:',p.result())
    print('主')

# 输出:
'''
0 6168
1 6168
2 6168
3 6168
4 6168

5 61686 6168

78 6168
9 6168
 6168

>>>: 0
>>>: 1
>>>: 4
>>>: 9
>>>: 16
>>>: 25
>>>: 36
>>>: 49
>>>: 64
>>>: 81
主

'''

以上代码可以实现在子线程执行完后,再统一去拿执行的结果,结果是有序的;但是如果我们想一边执行一边获取结果呢?这时候回调函数就派上了用场。

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

# 实例化池对象
# 不指定参数的情况,默认是当前计算机cpu个数乘以5,也可以指定线程个数
pool = ThreadPoolExecutor(5)  # 创建了一个池子,池子里面有5个线程,进程池和线程池的唯一区别就是这里
# 不指定参数的情况,进程池默认是当前计算机cpu个数
# pool = ProcessPoolExecutor(5)  # 创建了一个池子,池子里面有5个进程

def task(n):
    print(n, os.getpid())
    time.sleep(2)
    return n ** 2
"""
提交任务的方式
    同步:提交任务之后,原地等待任务的返回结果,再继续执行下一步代码
    异步:提交任务之后,不等待任务的返回结果(通过回调函数拿到返回结果并处理),直接执行下一步操作
"""
# 回调函数:异步提交之后一旦任务有返回结果,自动交给另外一个去执行
def call_back(n):
    print("拿到了结果:%s" % n.result())


if __name__ == '__main__':
    t_list = []
    for i in range(20):
        future = pool.submit(task, i).add_done_callback(call_back)  # 异步提交任务,通过回掉函数拿到执行结果
        t_list.append(future)  # 如果在此处直接打印结果print(future.result()),会使得每次执行等待结果,变成同步提交
    # print(t_list)  # 前面回调函数拿走结果之后,列表里面全剩了None
    print('主')

'''
执行结果:
0 8516
1 8516
2 8516
3 8516
4主
 8516
拿到了结果:0
5 8516
拿到了结果:1
6 8516
拿到了结果:4拿到了结果:9
7 8516
拿到了结果:16
8 8516

9 8516
拿到了结果:25
拿到了结果:36
拿到了结果:49
拿到了结果:81拿到了结果:64
'''

模拟爬虫:

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


def get(url):
    print('%s GET %s' %(current_thread().name,url))
    time.sleep(3)
    response=requests.get(url)
    if response.status_code == 200:
        res=response.text
    else:
        res='下载失败'
    return res

def parse(future):
    time.sleep(1)
    res=future.result()
    print('%s 解析结果为%s' %(current_thread().name,len(res)))

if __name__ == '__main__':
    urls=[
        'https://www.baidu.com',
        'https://www.sina.com.cn',
        'https://www.tmall.com',
        'https://www.jd.com',
        'https://www.python.org',
        'https://www.openstack.org',
        'https://www.baidu.com',
        'https://www.baidu.com',
        'https://www.baidu.com',

    ]

    p=ThreadPoolExecutor(4)
    
    for url in urls:
        future=p.submit(get,url)
		# 异步调用:提交完一个任务之后,不在原地等待,而是直接执行下一行代码,会导致任务是并发执行的,,结果futrue对象会在任务运行完毕后自动传给回调函数
        future.add_done_callback(parse)  # parse会在任务运行完毕后自动触发,然后接收一个参数future对象

    p.shutdown(wait=True)

    print('主',current_thread().name)

三、协程

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

协程的本质就是在单线程下,由用户自己控制一个任务遇到io阻塞了就切换另外一个任务去执行,以此来提升效率。这里就不得不会想到多道技术,它的核心是切换+保存状态,但是切换+保存状态就一定能提高效率吗?

答案当然是不一定了。如果你的任务是计算密集型的,总是切换反而会导致执行效率降低。但如果你的任务是IO密集型,自然会提升效率了。

协程,又称微线程。是一种用户态的轻量级线程。
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。

好处:
1、无需上下文切换的开销
2、无需原子操作锁定及同步的开销
3、方便切换控制流,简化编程逻辑
4、高并发+高扩展性+低成本:一个cpu支持上万的协程都没问题,适合高并发处理
缺点:
1、无法利用多核资源:协程的本质是个单线程,它不能同时将单个cpu的多个核用上,协程需要和进程配合才能运行在
多cpu上,当然我们日常编写的绝大所数应用都没有这个必要,除非是cpu密集型应用
2、进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序

单线程下实现并发(能够在多个任务之间切换和保存状态来节省IO),这里注意区分操作系统的切换+保存状态是
针对多个线程而言,而我们现在是想在单个线程下自己手动实现操作系统的切换+保存状态的功能

注意协程这个概念完全是程序员自己想出来的东西,它对于操作系统来说根本不存在。操作系统只知道进程
和线程。并且需要注意的是并不是单个线程下实现切换+保存状态就能提升效率,因为你可能是没有遇到io
也切,那反而会降低效率

再回过头来想上面的socket服务端实现并发的例子,单个线程服务端在建立连接的时候无法去干通信的活,
在干通信的时候也无法去干连接的活。这两者肯定都会有IO,如果能够实现通信io了我就去干建连接,
建连接io了我就去干通信,那其实我们就可以实现单线程下实现并发

将单个线程的效率提升到最高,多进程下开多线程,多线程下用协程>>> 实现高并发!!!

串行执行一段代码:

import time
def func1():
    for i in range(10000000):
        i + 1

def func2():
    for i in range(10000000):
        i + 1

start = time.time()
func1()
func2()
stop = time.time()
print("执行耗时:", stop - start)  # 执行耗时: 1.356902837753296

基于yield并发执行,虽然yield能够实现保存上次运行状态,但是无法识别遇到io就切换

import time
def func1():
    while True:
        10000000 + 1
        yield

def func2():
    g = func1()
    for i in range(10000000):
        # time.sleep(100)  # 模拟IO,加这句后,程序进入阻塞态,yield并不会捕捉到并自动切换
        i + 1
        next(g)

start = time.time()
func2()
stop = time.time()
print("执行耗时:", stop - start)  # 不加time延时的时候,执行耗时: 2.036350965499878

为了监测IO,需要引入gevent模块

gevent模块的使用:

gevent本身无法识别time.sleep()等不属于该模块内的IO操作,需要导入gevent下的monkey,利用它监测代码中所有的IO。

一个spawn就是一个帮你管理任务的对象

from gevent import monkey;monkey.patch_all()  # 监测代码中所有IO
from gevent import spawn
import time

def heng(name):
    print("%s 哼" % name)
    time.sleep(2)
    print("%s 哼" % name)

def ha(name):
    print("%s 哈" % name)
    time.sleep(3)
    print("%s 哈" % name)
'''
start = time.time()
heng('egon')
ha('lsb')
stop = time.time()
print("执行时间:", stop - start)

串行执行结果:
egon 哼
egon 哼
lsb 哈
lsb 哈
执行时间: 5.005341291427612
'''
# 利用gevent模块
start = time.time()
s1 = spawn(heng,'egon')
s2 = spawn(ha,'lsb')
s1.join()  # 此处必须加join等待执行结束
s2.join()
stop = time.time()
print("主执行时间:", stop - start)  # 单线程下实现并发,提升效率
'''
执行结果:
egon 哼
lsb 哈
egon 哼
lsb 哈
主执行时间: 3.067040205001831
'''

由以上代码可以看到,并发执行更加节省效率。遇到Io操作自行切换去执行其他的任务。

单线程利用协程实现并发通信

服务端:

'''
链接和通信都是io密集型操作,我们只需要在这两者之间来回切换其实就能实现并发的效果
服务端监测链接和通信任务,客户端起多线程同时链接服务端
'''
import socket
from gevent import monkey;monkey.patch_all()
from gevent import spawn

def communicate(conn):
    while True:
        try:
            data = conn.recv(1024)
            if len(data) == 0:
                break
            print(data.decode('utf-8'))
            conn.send(data.upper())
        except ConnectionResetError:
            break
    conn.close()


def server(ip, port, back_log=5):
    s = socket.socket()
    s.bind((ip, port))
    s.listen(back_log)
    while True:
        conn, addr = s.accept()
        spawn(communicate, conn)

if __name__ == '__main__':
    s1 = spawn(server,'127.0.0.1',8080)
    s1.join()
# 原本服务端需要开启500个线程才能跟500个客户端通信,现在只需要一个线程就可以扛住500客户端
# 进程下面开多个线程,线程下面再开多个协程,最大化提升软件运行效率

客户端:

from threading import Thread,current_thread
import socket
def client_main():
    client = socket.socket()
    client.connect(('127.0.0.1', 8080))
    n = 1
    while True:
        data = "%s %s" % (current_thread().name, n)
        n += 1
        client.send(data.encode("utf-8"))
        info = client.recv(1024)
        print(info.decode('utf-8'))

if __name__ == '__main__':
    for i in range(200):
        t = Thread(target=client_main)
        t.start()

四、IO模型

具体可以参考:http://www.cnblogs.com/linhaifeng/articles/7454717.html

IO发生时涉及的对象和步骤。对于一个network IO (这里我们以read举例),它会涉及到两个系统对象,一个是调用这个IO的process (or thread),另一个就是系统内核(kernel)。当一个read操作发生时,该操作会经历两个阶段:

1)等待数据准备 (Waiting for the data to be ready)
2)将数据从内核拷贝到进程中(Copying the data from the kernel to the process)

1、阻塞IO:IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block了

2、非阻塞IO:在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有。

3、多路复用IO:优势在于处理多个连接

4、异步IO:用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

用自己的理解来说一下这几个模型

阻塞IO:
比如我正在敲代码,刘志鹏跑过来问我,我是不是他爸爸?这时候我还在思考,他就在旁边等着,
哪儿也不去,等我思考好了,告诉他,我不是你爸爸,然后他就走了。

非阻塞IO:
我正在敲代码,刘志鹏跑过来问我,我是不是他爸爸?我跟他说我想想,他就不等了去问别人了。
过会儿,他又跑过来问我是不是他爸爸,我说我还没想好,他就又走了,直到我告诉他,我真不是你爸爸,
他就执行其他任务去了。

异步IO:
我正在敲代码,刘志鹏跑过来问我,我是不是他爸爸?我跟他说我想想,他就去干其他事儿了,
不再来问我。等我想好了再告诉他,孩子,我真的不是你爸爸啊!你的头被蜜蜂蛰了吗?

多路复用IO:
我有个代理人苏光体,我在敲代码,所有找我的都先去找我的代理人。刘志鹏和一堆刘志鹏跑过来问苏光体,
孙郝洁到底是不是我爸爸,苏光体跟他们说,孙郝洁还没回答我,我俩是心意相通的,等他有答案了我再告诉你们。

五、同步异步,阻塞非阻塞

针对任务的提交:
    同步:提交完任务后,在原地等待执行结果,期间不做任何事,直到拿到结果
    异步:提交完任务后,可以继续提交其他任务
针对程序的执行:
    阻塞:在调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会被唤醒执行后续的操作。
    非阻塞:在结果没有返回之前,该调用不会阻塞住当前线程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值