|
22 | 22 | import re |
23 | 23 | import urllib.parse |
24 | 24 | from queue import Queue |
25 | | -from threading import BoundedSemaphore, Lock, Thread |
26 | | -from typing import Mapping, Optional |
| 25 | +from threading import BoundedSemaphore, Event, Lock, Thread |
| 26 | +from typing import Any, Callable, Mapping, Optional |
27 | 27 |
|
28 | 28 | from . import __title__, __version__ |
29 | 29 | from .compat import HTTPHeaderDict, HTTPQueryDict, quote |
@@ -566,86 +566,89 @@ def build( |
566 | 566 |
|
567 | 567 |
|
568 | 568 | class Worker(Thread): |
569 | | - """ Thread executing tasks from a given tasks queue """ |
| 569 | + """Thread executing tasks from a given tasks queue""" |
570 | 570 |
|
571 | 571 | def __init__( |
572 | | - self, |
573 | | - tasks_queue: Queue, |
574 | | - results_queue: Queue, |
575 | | - exceptions_queue: Queue, |
| 572 | + self, |
| 573 | + tasks_queue: Queue, |
| 574 | + results_queue: Queue, |
| 575 | + exceptions_queue: Queue, |
| 576 | + abort_event: Event, |
576 | 577 | ): |
577 | | - Thread.__init__(self, daemon=True) |
| 578 | + super().__init__(daemon=True) |
578 | 579 | self._tasks_queue = tasks_queue |
579 | 580 | self._results_queue = results_queue |
580 | 581 | self._exceptions_queue = exceptions_queue |
| 582 | + self._abort_event = abort_event |
581 | 583 | self.start() |
582 | 584 |
|
583 | | - def run(self): |
584 | | - """ Continuously receive tasks and execute them """ |
| 585 | + def run(self) -> None: |
| 586 | + """Continuously receive tasks and execute them""" |
585 | 587 | while True: |
586 | 588 | task = self._tasks_queue.get() |
587 | | - if not task: |
| 589 | + |
| 590 | + # Poison pill to stop the thread |
| 591 | + if task is None: |
588 | 592 | self._tasks_queue.task_done() |
589 | 593 | break |
590 | | - func, args, kargs, cleanup_func = task |
591 | | - # No exception detected in any thread, |
592 | | - # continue the execution. |
593 | | - if self._exceptions_queue.empty(): |
| 594 | + |
| 595 | + func, args, kwargs, cleanup_func = task |
| 596 | + |
| 597 | + # 3.14t Optimization: Use an Event check instead of Queue.empty(). |
| 598 | + # This is a thread-safe way to stop processing if another thread |
| 599 | + # failed. |
| 600 | + if not self._abort_event.is_set(): |
594 | 601 | try: |
595 | | - result = func(*args, **kargs) |
| 602 | + result = func(*args, **kwargs) |
596 | 603 | self._results_queue.put(result) |
597 | 604 | except Exception as ex: # pylint: disable=broad-except |
| 605 | + # Signal all threads to stop executing new tasks |
| 606 | + self._abort_event.set() |
598 | 607 | self._exceptions_queue.put(ex) |
599 | 608 |
|
600 | | - # call cleanup i.e. Semaphore.release irrespective of task |
601 | | - # execution to avoid race condition. |
| 609 | + # Always cleanup (release semaphore) and mark task done |
602 | 610 | cleanup_func() |
603 | | - # Mark this task as done, whether an exception happened or not |
604 | 611 | self._tasks_queue.task_done() |
605 | 612 |
|
606 | 613 |
|
607 | 614 | class ThreadPool: |
608 | | - """ Pool of threads consuming tasks from a queue """ |
609 | | - _results_queue: Queue |
610 | | - _exceptions_queue: Queue |
611 | | - _tasks_queue: Queue |
612 | | - _sem: BoundedSemaphore |
613 | | - _num_threads: int |
| 615 | + """Pool of threads consuming tasks from a queue""" |
614 | 616 |
|
615 | 617 | def __init__(self, num_threads: int): |
616 | | - self._results_queue = Queue() |
617 | | - self._exceptions_queue = Queue() |
618 | | - self._tasks_queue = Queue() |
| 618 | + self._results_queue: Queue[Any] = Queue() |
| 619 | + self._exceptions_queue: Queue[Exception] = Queue() |
| 620 | + self._tasks_queue: Queue[tuple | None] = Queue() |
619 | 621 | self._sem = BoundedSemaphore(num_threads) |
| 622 | + self._abort_event = Event() |
620 | 623 | self._num_threads = num_threads |
621 | 624 |
|
622 | | - def add_task(self, func, *args, **kargs): |
623 | | - """ |
624 | | - Add a task to the queue. Calling this function can block |
625 | | - until workers have a room for processing new tasks. Blocking |
626 | | - the caller also prevents the latter from allocating a lot of |
627 | | - memory while workers are still busy running their assigned tasks. |
628 | | - """ |
| 625 | + def add_task(self, func: Callable, *args: Any, **kwargs: Any) -> None: |
| 626 | + """Add a task to the queue. Blocks if the pool is full""" |
629 | 627 | self._sem.acquire() # pylint: disable=consider-using-with |
630 | 628 | cleanup_func = self._sem.release |
631 | | - self._tasks_queue.put((func, args, kargs, cleanup_func)) |
| 629 | + self._tasks_queue.put((func, args, kwargs, cleanup_func)) |
632 | 630 |
|
633 | | - def start_parallel(self): |
634 | | - """ Prepare threads to run tasks""" |
| 631 | + def start_parallel(self) -> None: |
| 632 | + """Prepare threads to run tasks""" |
635 | 633 | for _ in range(self._num_threads): |
636 | 634 | Worker( |
637 | | - self._tasks_queue, self._results_queue, self._exceptions_queue, |
| 635 | + self._tasks_queue, |
| 636 | + self._results_queue, |
| 637 | + self._exceptions_queue, |
| 638 | + self._abort_event |
638 | 639 | ) |
639 | 640 |
|
640 | 641 | def result(self) -> Queue: |
641 | | - """ Stop threads and return the result of all called tasks """ |
642 | | - # Send None to all threads to cleanly stop them |
| 642 | + """Stop threads and return the results""" |
| 643 | + # 1. Send "Poison Pill" to all threads |
643 | 644 | for _ in range(self._num_threads): |
644 | 645 | self._tasks_queue.put(None) |
645 | | - # Wait for completion of all the tasks in the queue |
| 646 | + |
| 647 | + # 2. Wait for completion |
646 | 648 | self._tasks_queue.join() |
647 | | - # Check if one of the thread raised an exception, if yes |
648 | | - # raise it here in the function |
| 649 | + |
| 650 | + # 3. Check for exceptions collected during execution |
649 | 651 | if not self._exceptions_queue.empty(): |
650 | 652 | raise self._exceptions_queue.get() |
| 653 | + |
651 | 654 | return self._results_queue |
0 commit comments