在Python中,套接字不是线程安全的。
您试图一次解决几个问题:
- 套接字不是线程安全的。
recv
是阻塞的,会阻塞主线程。
sendall
正在从另一个线程中使用。
您可以通过使用asyncio或以类似asyncio内部的方式解决这些问题:通过与socketpair
一起使用select.select
,并使用传入数据的队列。
import select
import socket
import queue
send_queue = queue.Queue()
rsock, ssock = socket.socketpair()
main_socket = socket.socket()
def different_thread():
send_queue.put(data)
ssock.send(b"\x00")
while True:
rlist, _, _ = select.select([main_socket, rsock], [], [])
for ready_socket in rlist:
if ready_socket is main_socket:
data = main_socket.recv(1024)
else:
rsock.recv(1)
main_socket.sendall(send_queue.get())
我们在这里使用多个结构体,您需要使用您选择的代码填充空白空间。至于解释:
首先,我们创建一个send_queue
,它是要发送数据的队列。然后,我们创建一对连接的套接字(socketpair()
)。稍后我们需要这个套接字对,以便唤醒主线程,因为我们不希望recv()
阻塞并防止写入套接字。
然后,我们连接main_socket
并启动回调线程。现在,这里就有了魔力:
在主线程中,我们使用select.select
来知道rsock
或main_socket
是否有任何数据。如果它们中的一个有数据,主线程就会被唤醒。
向队列添加数据时,我们通过信号ssock
唤醒主线程,该线程又唤醒rsock
,从而从select.select
返回。
为了完全理解这一点,您将需要阅读select.select()
,socketpair()
和queue.Queue()
。
@tobias.mcnulty在评论中提出了一个很好的问题:为什么我们应该使用Queue
而不是通过套接字发送所有数据?
您也可以使用socketpair
发送数据,这样做有其优点,但基于多个原因,通过队列发送可能更可取:
- 通过套接字发送数据是一项昂贵的操作。它需要系统调用,需要在系统缓冲区内来回传递数据,并涉及对TCP堆栈的完全使用。使用
Queue
可以保证只有1次调用 - 用于单字节信号 - 而不是多次调用(除了队列的内部锁定外,但该锁定非常便宜)。通过socketpair
发送大量数据将导致多个系统调用。提示一下,您可以使用collections.deque
,因为CPython保证它由于GIL而是线程安全的。这样您就不必要求任何系统调用除了socketpair
。
- 从架构上讲,使用队列允许您以稍后希望的任何类型发送数据,并在之后进行解码。这允许主循环变得更加智能,并可以帮助您创建更简单的接口。
- 您没有大小限制。它可能是一个错误或一个功能。我相信不鼓励更改系统缓冲区大小,这会自然地限制您可以发送的数据量。它可能是一种好处,但应用程序可能希望自行控制它。使用“自然”特性将导致调用线程挂起。
- 对于大数据,与
socketpair.recv
系统调用一样,您还将通过多个select
调用传递。TCP没有消息边界。您要么必须创建人工边界,将套接字设置为非阻塞并处理异步套接字,或将其视为流并连续通过