3
\$\begingroup\$

I'm making some helper functions for the (fairly simple) UI framework I'm developing, among them, a helper to run app initialization tasks. Here's a helper class I've developed for oneshot tasks:

from threading import Lock

class Oneshot():
    _running = False
    _finished = False

    def __init__(self, func, *args, **kwargs):
        self.func = func
        self.args = args
        self.kwargs = kwargs

    def run(self):
        if self.running or self.finished:
            return
        self.active_lock.acquire()
        self._running = True
        self.active_lock.release()

        self.func(*self.args, **self.kwargs)

        self.active_lock.acquire()
        self._running = False
        self._finished = True
        self.active_lock.release()

    @property
    def running(self):
        self.active_lock.acquire()
        value = self._running
        self.active_lock.release()
        return value

    @property
    def finished(self):
        self.active_lock.acquire()
        value = self._finished
        self.active_lock.release()
        return value

Is it actually thread-safe, as I intend it to be? Any other thoughts on this?

I'd also like to add a reset() method. My idea is like this:

def reset(self):
    if self.running:
        raise Exception("Runner can't be reset while still running")
    self._finished = False

My only doubt is about whether raising an exception is the best course of action, but, since I have to signal the user that they are trying to perform an illegal action, and it's better to do it explicitly as the action user desired did not complete, I don't see a better way - do you?

\$\endgroup\$
3
  • \$\begingroup\$ Here's a question for you: "what would happen if the task raised an exception?" \$\endgroup\$ Commented Nov 5, 2017 at 23:30
  • \$\begingroup\$ @Frank: Nice catch, thought about it myself! Before, I thought that, if the task raises an exception, the user will just get it through run(), so setting any variables wouldn't make much sense - they won't be checking whether the task finished running because they know it did (and failed). Now that I added reset() method, though, I should be thinking about it again, indeed. To capture the exception and re-raise it for the user, should I need to use sys.exc_info() or would I preserve the stacktrace by just using raise? \$\endgroup\$ Commented Nov 5, 2017 at 23:58
  • 1
    \$\begingroup\$ The propertyself.active_lock is never assigned. \$\endgroup\$ Commented Nov 6, 2017 at 12:51

1 Answer 1

1
\$\begingroup\$

If run is called from multiple threads, the task may execute multiple times because this code is checking and setting the flag in separate operations:

    if self.running or self.finished:
        return
    self.active_lock.acquire()
    self._running = True
    self.active_lock.release()

Make it an atomic operation by holding the lock. Note also how the with statement releases the lock automatically:

    with self.active_lock:
        if self._running or self._finished:
            return
        self._running = True
\$\endgroup\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.