Python 中的套接字不是线程安全的。
您试图同时解决几个问题:
- 套接字不是线程安全的。
- recv 是阻塞的并阻塞主线程。
- sendall 正在从不同的线程使用。
您可以使用以下方法解决这些问题asyncio https://docs.python.org/3/library/asyncio.html或者以 asyncio 内部解决的方式解决它:通过使用select.select
与一个socketpair
,并使用队列来接收传入数据。
import select
import socket
import queue
# Any data received by this queue will be sent
send_queue = queue.Queue()
# Any data sent to ssock shows up on rsock
rsock, ssock = socket.socketpair()
main_socket = socket.socket()
# Create the connection with main_socket, fill this up with your code
# Your callback thread
def different_thread():
# Put the data to send inside the queue
send_queue.put(data)
# Trigger the main thread by sending data to ssock which goes to rsock
ssock.send(b"\x00")
# Run the callback thread
while True:
# When either main_socket has data or rsock has data, select.select will return
rlist, _, _ = select.select([main_socket, rsock], [], [])
for ready_socket in rlist:
if ready_socket is main_socket:
data = main_socket.recv(1024)
# Do stuff with data, fill this up with your code
else:
# Ready_socket is rsock
rsock.recv(1) # Dump the ready mark
# Send the data.
main_socket.sendall(send_queue.get())
我们在这里使用多个构造。您必须用您选择的代码填充空白区域。至于解释:
我们首先创建一个send_queue
这是要发送的数据队列。然后,我们创建一对连接的套接字(socketpair()
)。稍后我们需要这个来唤醒主线程,因为我们不希望这样做recv()
阻止并阻止写入套接字。
然后,我们连接main_socket
并启动回调线程。现在神奇之处在于:
在主线程中,我们使用select.select
知道是否rsock
or main_socket
有任何数据。如果其中之一有数据,则主线程被唤醒。
将数据添加到队列后,我们通过信号唤醒主线程ssock
哪个醒来rsock
从而返回select.select
.
为了完全理解这一点,您必须阅读select.select() https://docs.python.org/3/library/select.html#select.select, socketpair() https://docs.python.org/3/library/socket.html#socket.socketpair and queue.Queue() https://docs.python.org/3/library/queue.html#queue.Queue.
@tobias.mcnulty 在评论中提出了一个很好的问题:为什么我们应该使用Queue
而不是通过套接字发送所有数据?
您可以使用socketpair
也发送数据,这有其好处,但由于多种原因,通过队列发送可能更可取:
- 通过套接字发送数据是一项昂贵的操作。它需要系统调用,需要在系统缓冲区内来回传递数据,并且需要充分利用 TCP 堆栈。用一个
Queue
保证我们只有 1 次调用 - 对于单字节信号 - 而不是更多(除了队列的内部锁,但那是相当便宜的)。通过发送大数据socketpair
将导致多个系统调用。作为提示,您也可以使用collections.deque
由于 GIL,CPython 保证了线程安全。这样你就不需要任何系统调用了socketpair
.
- 从架构角度来看,使用队列可以让您稍后进行更细粒度的控制。例如,数据可以以您希望的任何类型发送,然后进行解码。这使得主循环变得更加智能,并且可以帮助您创建更简单的界面。
- 你没有尺寸限制。它可能是一个错误或一个功能。我相信改变系统的缓冲区大小并不完全被鼓励,这对您可以发送的数据量产生了自然的限制。这可能是一个好处,但应用程序可能希望自己控制它。使用“自然”功能将导致调用线程挂起。
- 就像
socketpair.recv
系统调用,对于大数据,您将通过多个select
也可以打电话。 TCP 没有消息边界。您要么必须创建人工套接字,将套接字设置为非阻塞并处理异步套接字,要么将其视为流并连续通过select
根据您的操作系统,调用可能会很昂贵。
- 支持同一套接字对上的多个线程。从多个线程通过套接字发送 1 个字节用于发信号就可以了,这正是 asyncio 的工作原理。发送超过此数量可能会导致数据以错误的顺序发送。
总而言之,在内核和用户空间之间来回传输数据是可能的并且可行,但我个人不推荐这样做。