Commit 73520d57 authored by Giampaolo Rodola's avatar Giampaolo Rodola

Fix #8684: make sched.scheduler class thread-safe

parent a23d65cc
...@@ -27,6 +27,9 @@ scheduler: ...@@ -27,6 +27,9 @@ scheduler:
.. versionchanged:: 3.3 .. versionchanged:: 3.3
*timefunc* and *delayfunc* parameters are optional. *timefunc* and *delayfunc* parameters are optional.
.. versionchanged:: 3.3
:class:`scheduler` class can be safely used in multi-threaded
environments.
Example:: Example::
...@@ -47,33 +50,6 @@ Example:: ...@@ -47,33 +50,6 @@ Example::
From print_time 930343700.273 From print_time 930343700.273
930343700.276 930343700.276
In multi-threaded environments, the :class:`scheduler` class has limitations
with respect to thread-safety, inability to insert a new task before
the one currently pending in a running scheduler, and holding up the main
thread until the event queue is empty. Instead, the preferred approach
is to use the :class:`threading.Timer` class instead.
Example::
>>> import time
>>> from threading import Timer
>>> def print_time():
... print("From print_time", time.time())
...
>>> def print_some_times():
... print(time.time())
... Timer(5, print_time, ()).start()
... Timer(10, print_time, ()).start()
... time.sleep(11) # sleep while time-delay events execute
... print(time.time())
...
>>> print_some_times()
930343690.257
From print_time 930343695.274
From print_time 930343700.273
930343701.301
.. _scheduler-objects: .. _scheduler-objects:
Scheduler Objects Scheduler Objects
......
...@@ -662,6 +662,10 @@ should be used. For example, this will send a ``'HEAD'`` request:: ...@@ -662,6 +662,10 @@ should be used. For example, this will send a ``'HEAD'`` request::
sched sched
----- -----
* :class:`~sched.scheduler` class can now be safely used in multi-threaded
environments. (Contributed by Josiah Carlson and Giampaolo Rodolà in
:issue:`8684`)
* *timefunc* and *delayfunct* parameters of :class:`~sched.scheduler` class * *timefunc* and *delayfunct* parameters of :class:`~sched.scheduler` class
constructor are now optional and defaults to :func:`time.time` and constructor are now optional and defaults to :func:`time.time` and
:func:`time.sleep` respectively. (Contributed by Chris Clark in :func:`time.sleep` respectively. (Contributed by Chris Clark in
......
...@@ -30,6 +30,7 @@ has another way to reference private data (besides global variables). ...@@ -30,6 +30,7 @@ has another way to reference private data (besides global variables).
import time import time
import heapq import heapq
import threading
from collections import namedtuple from collections import namedtuple
__all__ = ["scheduler"] __all__ = ["scheduler"]
...@@ -48,6 +49,7 @@ class scheduler: ...@@ -48,6 +49,7 @@ class scheduler:
"""Initialize a new instance, passing the time and delay """Initialize a new instance, passing the time and delay
functions""" functions"""
self._queue = [] self._queue = []
self._lock = threading.RLock()
self.timefunc = timefunc self.timefunc = timefunc
self.delayfunc = delayfunc self.delayfunc = delayfunc
...@@ -58,6 +60,7 @@ class scheduler: ...@@ -58,6 +60,7 @@ class scheduler:
if necessary. if necessary.
""" """
with self._lock:
event = Event(time, priority, action, argument, kwargs) event = Event(time, priority, action, argument, kwargs)
heapq.heappush(self._queue, event) heapq.heappush(self._queue, event)
return event # The ID return event # The ID
...@@ -68,6 +71,7 @@ class scheduler: ...@@ -68,6 +71,7 @@ class scheduler:
This is actually the more commonly used interface. This is actually the more commonly used interface.
""" """
with self._lock:
time = self.timefunc() + delay time = self.timefunc() + delay
return self.enterabs(time, priority, action, argument, kwargs) return self.enterabs(time, priority, action, argument, kwargs)
...@@ -78,11 +82,13 @@ class scheduler: ...@@ -78,11 +82,13 @@ class scheduler:
If the event is not in the queue, this raises ValueError. If the event is not in the queue, this raises ValueError.
""" """
with self._lock:
self._queue.remove(event) self._queue.remove(event)
heapq.heapify(self._queue) heapq.heapify(self._queue)
def empty(self): def empty(self):
"""Check whether the queue is empty.""" """Check whether the queue is empty."""
with self._lock:
return not self._queue return not self._queue
def run(self): def run(self):
...@@ -108,6 +114,7 @@ class scheduler: ...@@ -108,6 +114,7 @@ class scheduler:
""" """
# localize variable access to minimize overhead # localize variable access to minimize overhead
# and to improve thread safety # and to improve thread safety
with self._lock:
q = self._queue q = self._queue
delayfunc = self.delayfunc delayfunc = self.delayfunc
timefunc = self.timefunc timefunc = self.timefunc
...@@ -138,5 +145,6 @@ class scheduler: ...@@ -138,5 +145,6 @@ class scheduler:
# Use heapq to sort the queue rather than using 'sorted(self._queue)'. # Use heapq to sort the queue rather than using 'sorted(self._queue)'.
# With heapq, two events scheduled at the same time will show in # With heapq, two events scheduled at the same time will show in
# the actual order they would be retrieved. # the actual order they would be retrieved.
with self._lock:
events = self._queue[:] events = self._queue[:]
return map(heapq.heappop, [events]*len(events)) return map(heapq.heappop, [events]*len(events))
...@@ -409,6 +409,9 @@ Core and Builtins ...@@ -409,6 +409,9 @@ Core and Builtins
Library Library
------- -------
- Issue #8684 sched.scheduler class can be safely used in multi-threaded
environments.
- Alias resource.error to OSError ala PEP 3151. - Alias resource.error to OSError ala PEP 3151.
- Issue #5689: Add support for lzma compression to the tarfile module. - Issue #5689: Add support for lzma compression to the tarfile module.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment