bounded_pool.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. """有界并发执行 + daemon 线程池(修永久卡死).
  2. 两件事:
  3. 1. `DaemonThreadPoolExecutor`:worker 线程 daemon 化。即使某 worker 卡在 socket(在 read
  4. 超时触发前),也不会阻止解释器退出——配合各外部调用的 read 短超时,卡死 worker 必在有限
  5. 时间内自终,绝不成为永久僵尸。标准库 ThreadPoolExecutor 不暴露 daemon 选项,这里复刻
  6. `_adjust_thread_count` 在 start 前置 daemon;若标准库内部结构变动则回退标准行为。
  7. 2. `run_bounded`:逐 future `result(timeout=)` 收割,**单条超时/异常 → on_timeout 占位、跳过、
  8. 不抛、不中止整批**;收尾 `shutdown(wait=False, cancel_futures=True)`,绝不隐式 `wait=True`
  9. 死等。结果按 offset 归位,与完成顺序无关(并发=串行产物一致)。
  10. """
  11. from __future__ import annotations
  12. import threading
  13. import weakref
  14. from concurrent.futures import ThreadPoolExecutor
  15. from concurrent.futures import thread as _cf_thread
  16. from concurrent.futures import TimeoutError as FutureTimeoutError
  17. from typing import Any, Callable, TypeVar
  18. T = TypeVar("T")
  19. class DaemonThreadPoolExecutor(ThreadPoolExecutor):
  20. def _adjust_thread_count(self) -> None: # noqa: D401 - 复刻标准库,仅令线程 daemon
  21. try:
  22. if self._idle_semaphore.acquire(timeout=0): # type: ignore[attr-defined]
  23. return
  24. def weakref_cb(_: Any, q: Any = self._work_queue) -> None: # type: ignore[attr-defined]
  25. q.put(None)
  26. num_threads = len(self._threads) # type: ignore[attr-defined]
  27. if num_threads >= self._max_workers: # type: ignore[attr-defined]
  28. return
  29. thread_name = "%s_%d" % (self._thread_name_prefix or self, num_threads) # type: ignore[attr-defined]
  30. t = threading.Thread(
  31. name=thread_name,
  32. target=_cf_thread._worker, # type: ignore[attr-defined]
  33. args=(
  34. weakref.ref(self, weakref_cb),
  35. self._work_queue, # type: ignore[attr-defined]
  36. self._initializer, # type: ignore[attr-defined]
  37. self._initargs, # type: ignore[attr-defined]
  38. ),
  39. daemon=True,
  40. )
  41. t.start()
  42. self._threads.add(t) # type: ignore[attr-defined]
  43. _cf_thread._threads_queues[t] = self._work_queue # type: ignore[attr-defined]
  44. except Exception:
  45. # 标准库内部结构与预期不符 → 退回标准行为(read 短超时仍保证 worker 有限时间自终)。
  46. super()._adjust_thread_count()
  47. def run_bounded(
  48. items: list[T],
  49. work_fn: Callable[[T], Any],
  50. *,
  51. max_workers: int,
  52. per_future_timeout: float,
  53. on_timeout: Callable[[T, int], Any],
  54. thread_name_prefix: str = "bounded",
  55. ) -> list[Any]:
  56. """并发执行 work_fn(item),逐条 result(timeout=per_future_timeout)。
  57. 单条超时/未兜住的异常 → on_timeout(item, offset) 占位,不抛、不中止整批。
  58. 返回与 items 同序、同长的结果 list。
  59. """
  60. results: list[Any] = [None] * len(items)
  61. if not items:
  62. return results
  63. workers = max(1, min(int(max_workers), len(items)))
  64. executor = DaemonThreadPoolExecutor(max_workers=workers, thread_name_prefix=thread_name_prefix)
  65. try:
  66. future_to_offset = {
  67. executor.submit(work_fn, item): offset for offset, item in enumerate(items)
  68. }
  69. for future, offset in future_to_offset.items():
  70. try:
  71. results[offset] = future.result(timeout=per_future_timeout)
  72. except FutureTimeoutError:
  73. results[offset] = on_timeout(items[offset], offset)
  74. except Exception:
  75. # worker 内部未兜住的意外 → 也记占位,绝不让 result() 炸主线程。
  76. results[offset] = on_timeout(items[offset], offset)
  77. finally:
  78. executor.shutdown(wait=False, cancel_futures=True)
  79. return results