一 ThreadLocal
我们知道多线程环境下,每一个线程均可以使用所属进程的全局变量。如果一个线程对全局变量进行了修改,将会影响到其他所有的线程。为了避免多个线程同时对变量进行修改,引入了线程同步机制,通过互斥锁,条件变量或者读写锁来控制对全局变量的访问。
只用全局变量并不能满足多线程环境的需求,很多时候线程还需要拥有自己的私有数据,这些数据对于其他线程来说不可见。因此线程中也可以使用局部变量,局部变量只有线程自身可以访问,同一个进程下的其他线程不可访问。
python 还提供了 ThreadLocal 变量,它本身是一个全局变量,但是每个线程却可以利用它来保存属于自己的私有数据,这些私有数据对其他线程也是不可见的。
ThreadLocal 真正做到了线程之间的数据隔离,并且使用时不需要手动获取自己的线程 ID,如下示例:
from threading import local,current_thread,Thread
import time
# 创建一个Goods对象class Goods(object):
def __init__(self,name,weight):
self.name = name
self.weight = weight
def show(self):
print("线程信息:%s" %current_thread().name)
print("GoodsName=> %s\r\nGoods Weight=>%s" %(self.name,self.weight))
# 创建全局ThreadLocal对象:global_goods = local()
def process(goods):
global_goods.goods = goods
show_goods()
def show_goods():
goods = global_goods.goods
print(goods.show())
time.sleep(1)
goods1 = Goods("大米",50)
goods2 = Goods("面粉",25)
goods3 = Goods("啤酒",40)
t1 = Thread(target=process, args=(goods1,),name="线程-----A")
t2 = Thread(target=process, args=(goods2,),name="线程-----B")
t3 = Thread(target=process, args=(goods3,),name="线程-----C")
t = []
t.append(t1)
t.append(t2)
t.append(t3)
for i in t:
i.start()
for i in t:
i.join()
二 协程
理解协程之前,我们先看一下函数,我们知道函数都是顺序执行的,而且函数调用函数是通过栈实现的,比如函数1调用函数2,函数2调用函数3, 然后函数3返回,函数2返回,函数1返回
协程,又称为微线程。可以在执行的过程中,然后中断,接着干别的事情,然后回来接着执行。类似于CPU中断。比如CPU执行某一个线程,然后执行一半,切换到别的线程,然后再切回来,根据上下文可以知道上次执行到哪儿了。
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,因此:协程能够保留上一次调用的状态
协程可以实现多线程的效果:
我们知道单核CPU可以跑多个线程,不是同时跑多个而是切换速度太快,已达到同时执行多个的目的。
那么协程也就是这个原理,单线程在不同的代码块或者函数中切换,每一次的状态可以存储到寄存器,类似于每一次多线程切换,上下文信息存储到CPU的寄存器。不同的是多线程切换是CPU控制的,而协程代码块或者函数切换是用户控制的
协程优点:
# 单线程,无需线程切换,只是代码块或者函数切换,时间比CPU线程切换花的更少
# 由于串行执行,且同一时间只有一个操作执行,那么就无需原子操作,也就不需要锁,减少了同步的开销
# 方便控制切换
协程缺点:
# 无法利用多核资源
# 阻塞操作,会阻塞整个程序
Gevent:
Python通过yield提供了对协程的基本支持,但是不完全。而第三方的gevent为Python提供了比较完善的协程支持。
gevent是第三方库,通过greenlet实现协程,其基本思想是:
当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。
不改变源代码而对功能进行追加和变更,统称为“猴子补丁”。所以猴子补丁并不是Python中专有的。猴子补丁这种东西充分利用了动态语言的灵活性,可以对现有的语言API进行追加,替换,修改Bug,甚至性能优化等等。
使用猴子补丁的方式,gevent能够修改标准库里面大部分的阻塞式系统调用,包括socket、ssl、threading和 select等模块,而变为协作式运行。也就是通过猴子补丁的monkey.patch_xxx()来将python标准库中模块或函数改成gevent中的响应的具有协程的协作式对象。这样在不改变原有代码的情况下,将应用的阻塞式方法,变成协程式的。
import gevent
def func1():
print('\033[31;1m[func1]Hi,Andy\033[0m')
gevent.sleep(2)
print('\033[31;1m[func1]Hi, Lady\033[0m')
def func2():
print('\033[32;1m[func2]Hello,Judy\033[0m')
gevent.sleep(1)
print('\033[32;1m[func2]Hello,Tidy\033[0m')
'''
gevent.spawn 发起两个操作
当遇到gevent.sleep()则会切换到另外一个操作执行,如果另外一个操作也遇到sleep,那么又切回到原始操作
'''gevent.joinall([
gevent.spawn(func1),
gevent.spawn(func2),
])
遇到IO阻塞时会自动切换任务
from gevent import monkey;
monkey.patch_all()
import gevent
from urllib.request import urlopen
def f(url):
print('GET: %s' % url)
resp = urlopen(url)
data = resp.read()
print('%d bytes received from %s.' % (len(data), url))
gevent.joinall([
gevent.spawn(f, 'https://www.python.org/'),
gevent.spawn(f, 'https://www.yahoo.com/'),
gevent.spawn(f, 'https://github.com/'),
])
通过gevent实现单线程下的多socket并发
import sys
import socket
import time
import gevent
from gevent import socket, monkey
monkey.patch_all()
def server(port):
s =socket.socket()
s.bind('localhost',port)
s.listen(500)
while True:
sock, addr = s.accept()
gevent.spawn(handle, sock)
def handle(conn):
try:
while True:
data = conn.recv(1024)
print("recv:", data)
conn.send(data)
if not data:
conn.shutdown(socket.SHUT_WR)
except Exception as ex:
print(ex)
finally:
conn.close()
if __name__ == '__main__':
server(8000)
import socket
HOST = 'localhost' PORT = 8000
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
while True:
msg = bytes(input(">>:"), encoding="utf8")
s.sendall(msg)
data = s.recv(1024)
# print(data)
print('Received', repr(data))
s.close()
三 事件驱动与异步IO模型
我们写服务器处理模型的程序时,一般有以下几种模型:
第一:创建一个新的进程处理请求
优点:简单
缺点:开销较大,导致服务器性能比较差
第二:创建一个新的线程处理请求
优点:在性能开销方面稍微好点
缺点:但是会涉及到线程同步问题
第三:请求放入一个事件列表,主进程通过非阻塞I/O方式来处理
优点:
缺点:写应用程序逻辑复杂
大部分UI编程模型也都是基于事件驱动的
3.1 传统编程模型和事件驱动模型的区别
创建一个线程,该线程一直循环检测是否有鼠标点击,那么这个方式有以下几个缺点:
# 有可能点击频率很少,但是扫描线程还是会一直循环检测,这会造成很多的CPU资源浪费
# 如果是堵塞的,又会出现下面这样的问题,如果我们不但要扫描鼠标点击,还要扫描键盘是否按下,由于扫描鼠标时被堵塞了,那么可能永远不会去扫描键盘;
# 如果一个循环需要扫描的设备非常多,这又会引来响应时间的问题
事件驱动模型:
# 有一个事件(消息)队列;
# 鼠标按下时,往这个队列中增加一个点击事件(消息);
# 有个循环,不断从队列取出事件,根据不同的事件,调用不同的函数,如onClick()、onKeyDown()等;
# 事件(消息)一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数;
3.2 事件驱动模型的使用场景
当我们面对如下的环境时,事件驱动模型通常是一个好的选择:
第一: 程序中有许多任务、
第二: 任务之间高度独立(因此它们不需要互相通信,或者等待彼此)
第三: 在等待事件到来时,某些任务会阻塞。
四 select、poll、epoll
Python select()直接调用操作系统的IO接口,他监控sockets,opened files 和pipes何时变成可读,可写或者通信错误,select()使得同时监控多个连接变得简单,并且这比写一个长循环来等待和监控多客户端要高效
import select
import socket
import sys
import queue
# Create a TCP/IP socketserver = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setblocking(False)
# Bind the socket to the portserver_address = ('localhost', 10000)
print(sys.stderr, 'starting upon %s port %s' % server_address)
server.bind(server_address)
# Listen for incoming connectionsserver.listen(5)
# Sockets from which we expect to readinputs = [server]
# Sockets to which we expect to writeoutputs = []
message_queues = {}
while inputs:
# Wait for atleast one of the sockets to be ready for processing
print('\nwaiting for the next event')
readable, writable, exceptional= select.select(inputs, outputs, inputs)
# Handle inputs
for s in readable:
if s is server:
# A "readable" server socket is ready to accept a connection
connection, client_address = s.accept()
print('new connectionfrom', client_address)
connection.setblocking(False)
inputs.append(connection)
# Give the connection a queue for data we want to send
message_queues[connection] = queue.Queue()
else:
data = s.recv(1024)
if data:
# A readable client socket has data
print(sys.stderr, 'received"%s" from %s' % (data, s.getpeername()))
message_queues[s].put(data)
# Add output channel for response
if s not in outputs:
outputs.append(s)
else:
# Interpret empty result as closed connection
print('closing', client_address, 'after readingno data')
# Stop listening for input on the connection
if s in outputs:
outputs.remove(s) # 既然客户端都断开了,我就不用再给它返回数据了,所以这时候如果这个客户端的连接对象还在outputs列表中,就把它删掉
inputs.remove(s) # inputs中也删除掉
s.close() # 把这个连接关闭掉
# Remove message queue
del message_queues[s]
# Handle outputs
for s in writable:
try:
next_msg = message_queues[s].get_nowait()
except queue.Empty:
# No messages waiting so stop checking for writability.
print('output queuefor', s.getpeername(), 'is empty')
outputs.remove(s)
else:
print('sending"%s" to %s' % (next_msg, s.getpeername()))
s.send(next_msg)
# Handle"exceptional conditions"
for s in exceptional:
print('handlingexceptional condition for', s.getpeername())
# Stop listening for input on the connection
inputs.remove(s)
if s in outputs:
outputs.remove(s)
s.close()
# Remove message queue
del message_queues[s]
客户端代码:
mport
socket
import sys
messages = ['This is the message. ',
'It will besent ',
'in parts.',
]
server_address = ('localhost', 10000)
# Create a TCP/IP socketsocks = [socket.socket(socket.AF_INET, socket.SOCK_STREAM),
socket.socket(socket.AF_INET, socket.SOCK_STREAM),
]
# Connect the socket to the port where the server is listening
print >> sys.stderr, 'connecting to%s port %s' % server_address
for s in socks:
s.connect(server_address)
for message in messages:
# Send messageson both sockets
for s in socks:
print >> sys.stderr, '%s: sending"%s"' % (s.getsockname(), message)
s.send(message)
# Read responseson both sockets
for s in socks:
data = s.recv(1024)
print >> sys.stderr, '%s: received"%s"' % (s.getsockname(), data)
if not data:
print >> sys.stderr, 'closingsocket', s.getsockname()
s.close()