asyncio.Barrier: cancelling a waiting task mid-round can cause a later arrival to reuse its index
Chưa có ai nhận issue này.
- Ngôn ngữ chính
- Python
- Star
- 77.2k
- Fork
- 35.9k
- Chỉ số merge pull request
- Chỉ số pull request đang chờ
Mô tả
Bug description:
asyncio.Barrier.wait() is documented to return "a unique and individual index number from 0 to 'parties-1'" for each task that passes the barrier together. If a task's wait() call is cancelled while the barrier is still filling (i.e. before enough parties have arrived), a task that arrives afterward can be assigned the same index as another task that is already waiting in that same round — so two tasks in the same successful release get the same index, and another index in the range is never handed out at all.
Reproducer
import asyncio
async def main():
b = asyncio.Barrier(3)
results = []
async def party(name):
try:
idx = await b.wait()
results.append((name, idx))
except asyncio.CancelledError:
results.append((name, "cancelled"))
raise
t1 = asyncio.create_task(party("A"))
t2 = asyncio.create_task(party("B"))
await asyncio.sleep(0)
await asyncio.sleep(0)
t1.cancel() # A leaves while the barrier is still filling
try:
await t1
except asyncio.CancelledError:
pass
await asyncio.sleep(0)
t3 = asyncio.create_task(party("C"))
t4 = asyncio.create_task(party("D")) # completes the round of 3
await asyncio.gather(t2, t3, t4)
print(results)
asyncio.run(main())
Output on current main:
[('A', 'cancelled'), ('D', 2), ('B', 1), ('C', 1)]
B and C both get index 1; index 0 is never handed out to any of the three tasks that actually pass the barrier together (B, C, D).
Root cause
In Lib/asyncio/locks.py, Barrier.wait() assigns index = self._count eagerly, at arrival, before the round's final membership is known:
index = self._count
self._count += 1
if index + 1 == self._parties:
await self._release()
else:
await self._wait()
return index
self._count is decremented again when a party leaves early (cancellation), in the finally block. Since index is derived from a counter that can both increase (new arrivals) and decrease (early departures) within the same round, a later arrival can end up with the exact self._count value an earlier, still-waiting task already captured as its own index.
This is specific to the asyncio implementation — threading.Barrier doesn't need to handle a party "un-arriving" mid-wait the way a cancellable asyncio.Task can, so the sync version doesn't have an analogous bug.
I don't see an existing report for this (checked issues/PRs mentioning asyncio.Barrier).
A working fix
Indices can't be assigned correctly at arrival time when membership can still change; they need to be finalized once the round's exact release cohort is known (at release time), assigned once from the current arrival order. I have a fix + regression test ready and will open a PR against this issue.
CPython versions tested on:
3.16 (main)
Operating systems tested on:
Linux (WSL), should be platform-independent (no OS-specific code involved)
Linked PRs
- gh-155235
Hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Hướng nghiên cứu
Bắt đầu với Barrier.wait trong Lib/asyncio/locks.py và chạy reproducer được mô tả trong issue để quan sát index bị trùng sau khi hủy. Xem xét PR gh-155235 được liên kết và regression test của nó. Hoàn tất nghĩa là trong kịch bản hủy, một round được giải phóng với các index duy nhất cho mọi task đi qua.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Đánh giá
- Công nghệ
- python
- Lĩnh vực
- backend
- Loại issue
- Lỗi
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức độ hoạt động
- Đình trệ
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức phù hợp với người mới
- 25/100