You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python多线程与Socket编程出现'现有连接被远程主机强制关闭'错误求解

如果您想直接查看最优解释,可跳转至本问题最末尾。

原问题

我开发了一个小型Python客户端/服务端项目,连接两端均使用同一个类,代码如下:

from channel import Channel
import time
import constants
import sys
is_py2 = sys.version[0] == '2'
if is_py2:
    import Queue as queue
else:
    import queue as queue

class OneSide:
  def __init__(self, address1, address2, target1, target2):
    self.inQueue1 = queue.Queue()
    self.channel1 = Channel(address1,target1,self.inQueue1)
    time.sleep(.500)
    self.inQueue2 = queue.Queue()
    self.channel2 = Channel(address2,target2,self.inQueue2)

  def close(self):
    self.channel1.stopListening();
    self.channel2.stopListening();

  def from1(self, msg):
    self.channel1.write(msg)

  def from2(self, msg):
    self.channel2.write(msg)

  def read1(self):
    while not self.inQueue1.empty():
      print(self.inQueue1.get())

  def read2(self):
    while not self.inQueue2.empty():
      print(self.inQueue2.get())

为保证信息完整,我也附上Channel类的代码:

import threading
import socket
import sys
is_py2 = sys.version[0] == '2'
if is_py2:
    import Queue as queue
else:
    import queue as queue

class Channel:
  def __init__(self, address, target, outQueue):
    self.address = address
    self.target = target
    self.connection = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
    self.connection.bind(address)
    self.connection.settimeout(5.0)
    self.outQueue = outQueue
####THIS IS THE PROBLEM####
    self.serverThread = threading.Thread(target=self.serverFunction)
    self.startListening()
####THIS IS THE PROBLEM####

  def startListening(self):
    self.running = True
    self.serverThread.start()

  def stopListening(self):
    print("Shutting down server thread port {}".format(self.address[1]))
    self.running = False

  def serverFunction(self):
    # time.sleep(.100) # sleep 100 ms
    print("Server thread started on port {}".format(self.address[1]))
    while self.running:
      try:
        self.outQueue.put(self.read(1024))
      except socket.timeout:
        #no error; this can occur if the read times out
        pass
      except Exception as e:
        # must have been forced close
        print("Server thread failed on port {}".format(self.address[1]))
        print("  Error: {}".format(e))
        self.running = False
    self.connection.shutdown(socket.SHUT_RDWR)
    self.connection.close()
    print("Closed server thread port {}".format(self.address[1]))

  def write(self, bytes):
    self.connection.sendto(bytes, self.target)

  def read(self, bufferSize):
    bytesAddressPair = self.connection.recvfrom(bufferSize)
    message = bytesAddressPair[0]
    return bytearray(message)

我最初写了一个main.py文件,同时创建两个对象互相通信,代码如下:

from oneside import OneSide
import time

c1 = OneSide(("127.0.0.1",2000),("127.0.0.1",2001),("127.0.0.1",1000),("127.0.0.1",1001));
time.sleep(.500)
c2 = OneSide(("127.0.0.1",1000),("127.0.0.1",1001),("127.0.0.1",2000),("127.0.0.1",2001));
time.sleep(.500)

c1.from1("bytes1")
c1.from2("bytes2")

c2.from1("reverse1")
c2.from2("reverse2")

c1.read1()
c1.read2()

c2.read1()
c2.read2()

c2.close()
c1.close()

这段代码运行完全正常。后来我想分开运行两个实例,就拆分出两个独立的main文件分别运行,其中一个的代码如下:

from oneside import OneSide
import time

c1 = OneSide(("127.0.0.1",2000),("127.0.0.1",2001),("127.0.0.1",1000),("127.0.0.1",1001));
time.sleep(.500)

c1.from1("bytes1")
c1.from2("bytes2")

c1.read1()
c1.read2()

c1.close()

但运行时就会报错,哪怕只运行其中一个或者同时运行两个都会出现该错误:

Error: [Errno 10054] An existing connection was forcibly closed by the remote host

我有三个疑问:

  1. 该错误的触发原因是什么?
  2. 如何修复这个问题?
  3. 为什么同时运行两个服务端的时候不会出现该错误?

补充信息

我后续调试时两个OneSide对象之间的通信已经可以正常运行,但改用多线程模式运行时又会出现同样的错误。


最终编辑

请注意我仍然在寻找相关原理说明,目前我没有找到任何文档解释为什么多线程和Socket不能混用。不过我自己排查出了以下结论:
我是逐步迭代开发该程序的,最初我给Channel设置了一个线程化的server函数用于读取数据并打印,之后改成将读取到的数据存入队列供其他线程读取,后来我又把读队列的逻辑也放进了独立线程,全程Socket都是在主线程中创建的。

当我意识到Socket不应该在创建它的线程之外被访问后,情况有所好转。我重写了多线程版本的STOneSide(线程安全OneSide类),改为在子线程内部创建Channel对象,代码如下:

def startup(self, channel_id, address, target):
        self.channels[channel_id] = Channel(address, target, self.queues[channel_id])
        port = self.addresses[channel_id][1]
        safe_print("Port {} Stage 1".format(port))

        while not self.states[channel_id] == CONNECTED:
            self.states[channel_id] = STAGE1
...

此时还是会偶现报错,但已经可以偶尔连接成功。我当时还没意识到Channel内部也会创建新线程,当我发现这点后,我去掉了Channel内部的线程,临时修改代码,直接在创建Channel和Socket的STOneSide子线程函数中读取数据,问题就彻底解决了,所有连接都正常,不再报错。修改后的代码如下:

#From STOneSide
    def read_all(self, channel_id):
        self.channels[channel_id].read_all()
        output = []
        while not self.queues[channel_id].empty():
            output.append(self.queues[channel_id].get())
        return output

# From Channel
  def read_all(self):
    running = True
    while running:
      try:
        self.outQueue.put(self.read(1024))
      except socket.timeout:
        #end of all data
        running = False
        pass
      except Exception as e:
        # must have been forced close
        safe_print("Server thread failed on port {}".format(self.address[1]))
        safe_print("  Error: {}".format(e))
        running = False

现在我已经知道问题所在和修复方法,但还是希望能获得官方文档说明,解释为什么不能跨线程访问Socket。


最优解释

1. 10054错误触发原因

你使用的是UDP协议,本身属于无连接协议,但Windows系统内核会缓存ICMP端口不可达报文:当你发出去的UDP报文对应的目标端口没有进程监听时,对端会返回ICMP不可达,内核会把这个错误关联到对应的socket上,下一次调用该socket的recvfrom方法时就会抛出10054错误。
单独运行单个实例的时候,你发送的报文没有对应接收端监听端口,自然会触发这个错误;单进程同时运行两个实例的时候,双方端口都有进程监听,不会返回ICMP不可达报文,所以不会报错。

2. 跨线程操作socket的官方说明

Python标准库的socket对象本身是线程不安全的,官方文档明确标注:socket对象的所有方法都没有内部同步机制,如果多个线程同时操作同一个socket对象,需要用户自行实现互斥锁保证访问顺序,否则会出现不可预期的状态错误。
你之前的问题本质是多个线程(主线程创建socket,子线程读、主线程写/关闭)同时操作同一个socket,没有做同步,导致socket状态混乱,才会偶发报错。你后续改成所有socket操作都在创建它的同一个线程内完成,天然避免了竞态条件,所以问题消失。

3. 修复方案

和你排查的结果一致,两种方案可选:

  • 保证每个socket的所有操作(创建、读、写、关闭)都在同一个线程内完成
  • 给所有跨线程的socket操作加互斥锁,避免并发访问

内容的提问来源于stack exchange,提问作者BSD

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.09 11:48:03