python / python/cpython

asyncio: SelectorEventLoop busy-loops at 100% CPU forever when the self-pipe socketpair reaches EOF

未关闭
#156,344 4 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

stdlib topic-asyncio type-bug
主要语言
Python
星标
77.2k
派生
35.9k
PR 合并指标
PR 指标待抓取

描述

Bug description

The selector-side twin of #156333. BaseSelectorEventLoop also wakes itself through a self-socket created by socket.socketpair(). On Windows this is a loopback TCP pair, and if the connection reaches a clean EOF while the loop runs, _read_from_self returns but the reader is still registered on the dead fd — select() reports it readable immediately, forever, pinning one core at 100% with nothing logged.

Lib/asyncio/selector_events.py:

def _read_from_self(self):
    while True:
        try:
            data = self._ssock.recv(4096)
            if not data:
                break
            self._process_self_data(data)
        except InterruptedError:
            continue
        except BlockingIOError:
            break

At EOF, recv() returns b'', the loop breaks, and the callback returns. But the fd is still registered via _add_reader(self._ssock.fileno(), self._read_from_self) from _make_self_pipe(). A closed-for-read socket is permanently "readable", so every select() iteration re-fires the callback — a tight spin. The loop is otherwise idle and never recovers.

The proactor half of this bug is fixed by #156343 (rebuild the pair on the empty result). The same OS teardown of the loopback pair that triggers it triggers this on any WindowsSelectorEventLoopPolicy process.

Reproducer (deterministic, Windows)
import asyncio, socket, time

calls = 0
orig = asyncio.selector_events.BaseSelectorEventLoop._read_from_self
def counting(self):
    global calls
    calls += 1
    return orig(self)
asyncio.selector_events.BaseSelectorEventLoop._read_from_self = counting

async def main():
    loop = asyncio.get_running_loop()
    await asyncio.sleep(0.1)
    loop._csock.shutdown(socket.SHUT_WR)   # graceful half-close = clean EOF
    global calls
    calls = 0
    start = time.process_time()
    await asyncio.sleep(3)
    print(f"_read_from_self calls during 3s idle: {calls}")
    print(f"CPU consumed while sleeping 3s: {time.process_time() - start:.2f}s")

loop = asyncio.SelectorEventLoop()
asyncio.set_event_loop(loop)
loop.run_until_complete(main())

Measured on Windows 11, CPython main (3.16.0a0, self-built): 582,692 callback invocations, 1.23s CPU, during a 3-second idle sleep. Expected ~0.

Note the instrumentation must patch the class before the loop is created — _make_self_pipe() captures the bound method at registration time, so patching an instance afterwards does not intercept the already-armed reader (which is also why the spin is invisible to profilers that hook late).

Environment
  • Windows 11, self-built main (3.16.0a0)
  • The proactor variant (#156333) reproduces on installed 3.12.10 / 3.13.12; the _read_from_self code path shown above is unchanged on main
Notes on scope
  • Unix loops use an AF_UNIX pair, which the OS does not tear down this way, so the organic trigger is Windows-specific; a deterministic test using shutdown(SHUT_WR) works on any platform, though.
  • Related, on the proactor side: the same teardown can also surface as ConnectionResetError from the pending recv instead of a clean EOF; that path is currently unhandled too (noted in #156343).
Suggested direction

On recv() == b'', rebuild the self-pipe the way #156343 does for the proactor loop: allocate the new pair first, move any signal wakeup fd registration (set_wakeup_fd() returns the previous fd, so it can be probed and moved only when it names the old socket — Unix loops with signal handlers), remove the old reader, close the old sockets, and register the reader on the new fd.

Linked PRs
  • gh-156345

贡献指南

打开贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

调研方向

从 Lib/asyncio/selector_events.py 中的 _read_from_self 和 _make_self_pipe 开始,然后比较 #156343 中的 proactor 修复以及 #156345 中的相关工作。重现 Windows self-pipe EOF 情况,并验证空闲的 selector loop 不再反复调用 reader,同时 signal wakeup 注册仍然正确。

由索引模型根据 Issue 内容生成。

评估

技术栈
python
领域
backend, operating-systems
Issue 类型
缺陷
难度
4/5
预计耗时
3-5 天
活跃度
停滞
描述清晰度
描述清楚
新手友好度
25/100

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。