socketsever模块与线程
并发聊天
在之前的不间断聊天中,sk.listen(3)
规定了等待的客户端最多有3个,但是只能有一个客户端在和服务端聊天,当前连接断开,等待队伍中的第一个客户端才能连接上服务端。如果我们需要服务端同时和几个客户端聊天该怎么做呢?可以使用socketsever来实现并发。由于每次建立连接的过程是类似的,所以被封装在 socketserver.BaseRequestHandler 中(默认listen为5),不需要自己实现,而代码的逻辑部分需要通过重写这个类中的 handle 方法来实现。最后,并发的实现通过 socketserver.ThreadingTCPServer 类来实现。参考代码:
# sever.py
import socketserver
class MyServer(socketserver.BaseRequestHandler):
def handle(self):
print("服务端启动...")
while True:
conn = self.request # client传过来的 Socket
print(self.client_address)
while True:
client_data = conn.recv(1024)
print(str(client_data, "utf8"))
print("waiting...")
server_response = input(">>>")
conn.sendall(bytes(server_response, "utf8"))
conn.close()
if __name__ == '__main__':
server = socketserver.ThreadingTCPServer(('127.0.0.1', 8098), MyServer)
server.serve_forever()
# =============================================================================
# client.py 不需要改动
import socket
ip_port = ('127.0.0.1', 8098)
sk = socket.socket()
sk.connect(ip_port)
print("客户端启动:")
while True:
inp = input('>>>')
sk.sendall(bytes(inp, "utf8"))
server_response = sk.recv(1024)
print(str(server_response, "utf8"))
if inp == 'exit':
break
sk.close()
此时运行多个 client 都可以和 sever 进行聊天。
线程
说到并行,一定要提到线程,Python的线程和其他语言类似,举个例子:
import threading
import time
def func1(s):
time.sleep(2) # 花费两秒
print('func1', s)
def func2(s):
time.sleep(1) # 花费一秒
print('func2', s)
begin = time.time()
t1 = threading.Thread(target=func1, args=('first',))
t2 = threading.Thread(target=func2, args=('second',))
t1.start()
t2.start()
print('main...')
t1.join()
t2.join()
end = time.time()
print(end - begin)
# main...
# func2 second
# func1 first
# 2.0019052028656006
可以看出来,3个线程并驾齐驱,总共用时2秒钟。一个进程(process)可以有多个线程(threads),一般来说我们所写的比较简单的代码只有一个主线程。线程之间可以进行资源共享,但是进程之间不可以。
IO密集型任务有很多IO堵塞(可以理解为有很多时间在等待输入输出),上面的 time.sleep() 也属于IO堵塞,在IO密集型任务中不会一直使用CPU,所以在多线程中如果出现IO堵塞,CPU就去执行其他线程。而计算密集型任务会一直占用CPU,所以假设有两个线程都是计算累加和的函数,那么它们会互相抢占CPU,CPU在两个线程的切换中浪费了资源,时间上反而不如先执行一个函数,再执行另一个函数。当然如果CPU是多核的,就可以把两个任务分给两个CPU,这样就通过多线程并行来提高效率。但是这样的设想在Java可行,在Python还是会出现并行不如串行的现象,明明电脑是多核的,却看起来只能使用一个CPU,要解释这个问题需要了解Python的GIL。
Python的GIL
上面所提到的问题其实是CPython解释器的一个bug,GIL(Global Interpreter Lock) 全局解释器锁是CPython解释器上的一把锁,它规定了在同一时刻,只能有一个线程。这其实是一个历史遗留问题,可以说Python没有真正意义上的多线程,因为在Python不会出现两个CPU同时处理线程的情况。所以只能把事件放在不同进程中来执行以解决这个问题,这不是一个完美的解决方案。“协程 + 多进程”也是一种解决方案,相比于线程的抢占CPU,协程是用协商的方式来使用CPU。
同步
Java中的多线程同步通过对象锁来实现,在程序中用 synchronized(引用类型表达式) { 语句序列} 来表示。下面用一个买票程序来说明同步锁的问题,如果有100张票给10个窗口卖会出现以下情况:
import threading
num = 100 # 100张票
# lock = threading.Lock()
def sell_ticket(t):
global num
# lock.acquire()
while num > 0:
print(t, 'sell', num)
num -= 1
# lock.release()
threads = []
# 10个卖票窗口
for i in range(1, 11):
threads.append(threading.Thread(target=sell_ticket, args=('t'+str(i),)))
for t in threads: t.start()
for t in threads: t.join()
print('final number:', num)
最后的结果是这样的:
会出现这种情况的原因是,有的票被卖了超过一次:
Python加锁通过treading.Lock 类实现,取消上面代码的注释语句就能让程序正确运行了。