一、基于多线程实现并发通信
服务端:
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:
我有个代理人苏光体,我在敲代码,所有找我的都先去找我的代理人。刘志鹏和一堆刘志鹏跑过来问苏光体,
孙郝洁到底是不是我爸爸,苏光体跟他们说,孙郝洁还没回答我,我俩是心意相通的,等他有答案了我再告诉你们。
五、同步异步,阻塞非阻塞
针对任务的提交:
同步:提交完任务后,在原地等待执行结果,期间不做任何事,直到拿到结果
异步:提交完任务后,可以继续提交其他任务
针对程序的执行:
阻塞:在调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会被唤醒执行后续的操作。
非阻塞:在结果没有返回之前,该调用不会阻塞住当前线程。