1
"""
2
This file contains a number of methods for asynchronous operations.
3
"""
4 3
import logging
5 3
from qtpy import QtCore, QtWidgets
6 3
from timeit import default_timer
7 3
logger = logging.getLogger(name=__name__)
8

9 3
from . import APP  # APP is only created once at the startup of PyRPL
10 3
MAIN_THREAD = APP.thread()
11

12 3
try:
13 3
    from asyncio import Future, ensure_future, CancelledError, \
14
        set_event_loop, TimeoutError
15 1
except ImportError:  # this occurs in python 2.7
16 1
    logger.debug("asyncio not found, we will use concurrent.futures "
17
                  "instead of python 3.5 Futures.")
18 1
    from concurrent.futures import Future, CancelledError, TimeoutError
19
else:
20 2
    import quamash
21 2
    set_event_loop(quamash.QEventLoop())
22

23

24

25 3
class MainThreadTimer(QtCore.QTimer):
26
    """
27
    To be able to start a timer from any (eventually non Qt) thread,
28
    we have to make sure that the timer is living in the main thread (in Qt,
29
    each thread potentially has a distinct eventloop...).
30

31
    For example, this is required to use the timer within timeit.
32

33
    we might decide one day to allow 2 event loops to run concurrently in
34
    separate threads, but
35
    1. That should be QThreads and not python threads
36
    2. We would have to make sure that all the gui operations are performed
37
    in the main thread (for instance, by moving all widgets in the
38
    mainthread, and probably, we would have to change some connections in
39
    QueuedConnections)
40
    ==> but this is not a supported feature for the moment and I don't see
41
    the advantage because the whole point of using an eventloop is to
42
    avoid multi-threading.
43

44
    For conveniance, MainThreadTimer is also SingleShot by default and is
45
    initialized with an interval as only argument.
46

47
    Benchmark:
48
    ----------
49

50
     1. keep starting the same timer over and over --> 5 microsecond/call::
51

52
            n = [0]
53
            tics = [default_timer()]
54
            timers = [None]
55
            N = 100000
56
            timer = MainThreadTimer(0)
57
            timer.timeout.connect(func)
58
            def func():
59
                n[0]+=1
60
                if n[0] > N:
61
                    print('done', (default_timer() - tics[0])/N)
62
                    return
63
                timer.start()
64
                timers[0] = timer
65
                return
66
            func() ---> 5 microseconds per call
67

68
     2. Instantiating a new timer at each call --> 15 microsecond/call::
69

70
            n = [0]
71
            tics = [default_timer()]
72
            timers = [None]
73
            N = 100000
74
            def func():
75
                n[0]+=1
76
                if n[0] > N:
77
                    print('done', (default_timer() - tics[0])/N)
78
                    return
79
                timer = MainThreadTimer(0)
80
                timer.timeout.connect(func)
81
                timer.start()
82
                timers[0] = timer
83
                return
84
            func() ---> 15 microseconds per call
85

86
    Moreover, no catastrophe occurs when instantiating >10e6 timers
87
    successively
88

89
    Conclusion: it is OK to instantiate a new timer every time it is needed
90
    as long as a 10 microsecond overhead is not a problem.
91
    """
92

93 3
    def __init__(self, interval):
94 3
        super(MainThreadTimer, self).__init__()
95 3
        self.moveToThread(MAIN_THREAD)
96 3
        self.setSingleShot(True)
97 3
        self.setInterval(interval)
98

99

100 3
class PyrplFuture(Future):
101
    """
102
    A promise object compatible with the Qt event loop.
103

104
    We voluntarily use an object that is different from the native QFuture
105
    because we want a promise object that is compatible with the python 3.5+
106
    asyncio patterns (for instance, it implements an __await__ method...).
107

108
    Attributes:
109
        cancelled: Returns whether the promise has been cancelled.
110
        exception: Blocks until:
111
                a. the result is ready --> returns None
112
                b. an exception accured in the execution --> returns the exception the Qt event-loop is allowed to run in parallel.
113
        done: Checks whether the result is ready or not.
114
        add_done_callback (callback function): add a callback to execute when result becomes available. The callback function takes 1 argument (the result of the promise).
115

116
    Methods to implement in derived class:
117
        _set_data_as_result(): set
118
    """
119

120 3
    def __init__(self):
121 0
        super(PyrplFuture, self).__init__()
122 0
        self._timer_timeout = None  # timer that will be instantiated if
123
        #  result(timeout) is called with a >0 value
124

125 3
    def result(self):
126
        """
127
        Blocks until the result is ready while running the event-loop in the background.
128

129
        Returns:
130
            The result of the future.
131
        """
132 0
        try: #  concurrent.futures.Future (python 2)
133 0
            return super(PyrplFuture, self).result(timeout=0)
134 0
        except TypeError: #  asyncio.Future (python 3)
135 0
            return super(PyrplFuture, self).result()
136

137 3
    def _exit_loop(self, x=None):
138
        """
139
        Parameter x=None is there such that the function can be set as
140
        a callback at the same time for timer_timeout.timeout (no
141
        argument) and for self.done (1 argument).
142
        """
143 0
        self.loop.quit()
144

145 3
    def _wait_for_done(self, timeout):
146
        """
147
        Will not return until either timeout expires or future becomes "done".
148
        There is one potential deadlock situation here:
149

150
        The deadlock occurs if we await_result while at the same
151
        time, this future needs to await_result from another future
152
        ---> To be safe, don't use await_result() in a Qt slot...
153
        """
154 0
        if self.cancelled():
155 0
            raise CancelledError("Future was cancelled")  # pragma: no-cover
156 0
        if not self.done():
157 0
            self.timer_timeout = None
158 0
            if (timeout is not None) and timeout > 0:
159 0
                self._timer_timeout = MainThreadTimer(timeout*1000)
160 0
                self._timer_timeout.timeout.connect(self._exit_loop)
161 0
                self._timer_timeout.start()
162 0
            self.loop = QtCore.QEventLoop()
163 0
            self.add_done_callback(self._exit_loop)
164 0
            self.loop.exec_()
165 0
            if self._timer_timeout is not None:
166 0
                if not self._timer_timeout.isActive():
167 0
                    return TimeoutError("Timeout occured")  # pragma: no-cover
168
                else:
169 0
                    self._timer_timeout.stop()
170

171 3
    def await_result(self, timeout=None):
172
        """
173
        Return the result of the call that the future represents.
174
        Will not return until either timeout expires or future becomes "done".
175

176
        There is one potential deadlock situation here:
177
        The deadlock occurs if we await_result while at the same
178
        time, this future needs to await_result from another future since
179
        the eventloop will be blocked.
180
        ---> To be safe, don't use await_result() in a Qt slot. You should
181
        rather use result() and add_done_callback() instead.
182

183
        Args:
184
            timeout: The number of seconds to wait for the result if the future
185
                isn't done. If None, then there is no limit on the wait time.
186

187
        Returns:
188
            The result of the call that the future represents.
189

190
        Raises:
191
            CancelledError: If the future was cancelled.
192
            TimeoutError: If the future didn't finish executing before the
193
                          given timeout.
194
            Exception: If the call raised then that exception will be raised.
195
        """
196

197 0
        self._wait_for_done(timeout)
198 0
        return self.result()
199

200 3
    def await_exception(self, timeout=None):  # pragma: no-cover
201
        """
202
        Return the exception raised by the call that the future represents.
203

204
        Args:
205
            timeout: The number of seconds to wait for the exception if the
206
                future isn't done. If None, then there is no limit on the wait
207
                time.
208

209
        Returns:
210
            The exception raised by the call that the future represents or None
211
            if the call completed without raising.
212

213
        Raises:
214
            CancelledError: If the future was cancelled.
215
            TimeoutError: If the future didn't finish executing before the
216
            given  timeout.
217
        """
218 0
        self._wait_for_done(timeout)
219 0
        return self.exception()
220

221 3
    def cancel(self):
222
        """
223
        Cancels the future.
224
        """
225 0
        if self._timer_timeout is not None:
226 0
            self._timer_timeout.stop()
227 0
        super(PyrplFuture, self).cancel()
228

229

230 3
def sleep(delay):
231
    """
232
    Sleeps for :code:`delay` seconds + runs the event loop in the background.
233

234
        * This function will never return until the specified delay in seconds is elapsed.
235
        * During the execution of this function, the qt event loop (== asyncio event-loop in pyrpl) continues to process events from the gui, or from other coroutines.
236
        * Contrary to time.sleep() or async.sleep(), this function will try to achieve a precision much better than 1 millisecond (of course, occasionally, the real delay can be longer than requested), but on average, the precision is in the microsecond range.
237
        * Finally, care has been taken to use low level system-functions to reduce CPU-load when no events need to be processed.
238

239
    More details on the implementation can be found on the page: `<https://github.com/lneuhaus/pyrpl/wiki/Benchmark-asynchronous-sleep-functions>`_.
240
    """
241 3
    tic = default_timer()
242 3
    end_time = tic + delay
243

244
    # 1. CPU-free sleep for delay - 1ms
245 3
    if delay > 1e-3:
246 3
        new_delay = delay - 1e-3
247 3
        loop = QtCore.QEventLoop()
248 3
        timer = MainThreadTimer(new_delay * 1000)
249 3
        timer.timeout.connect(loop.quit)
250 3
        timer.start()
251 3
        try:
252 3
            loop.exec_()
253 0
        except KeyboardInterrupt as e:  # pragma: no-cover
254
            # try to recover from KeyboardInterrupt by finishing the current task
255 0
            timer.setInterval(1)
256 0
            timer.start()
257 0
            loop.exec_()
258 0
            raise e
259
    # 2. For high-precision, manually process events 1-by-1 during the last ms
260 3
    while default_timer() < end_time:
261 3
        APP.processEvents()

Read our documentation on viewing source code .

Loading