Skip to content

Architecture: Threading

localghost edited this page Feb 15, 2015 · 45 revisions

Tasks

Information related to tasks can be found here.

Global threads

There will probably be a couple of threads that would be accessed by different classes, e.g. thread through which all the libspotify API calls should be made.

List of global threads:

  • spotify_thread : all the libspotify API calls should be done through this thread
  • main_thread : UI should run on this thread (this requires creating message loop that would also run on GLib/Mac UI loop, etc.)

Callbacks on main thread

Callbacks should not run on the spotify_thread, since client might want to call other API that behind the scenes uses spotify_thread synchronously (e.g. in the search completed callback client will probably invoke API of search response which internally calls some spotify API synchronously). Thus, all callbacks will be invoked on the main thread. As for now (see next paragraph) this is the most logical choice since most probably receiver objects will be created on the main thread.

However, when task scheduler is implemented this could be changed so that callbacks are run on an arbitrary worker thread. Otherwise, main thread could become a bottleneck and it has to be very responsive since most probably it will be extensively used by a UI FW.

Currently, (until main thread is not implemented) all callbacks will be invoked on the internal thread of the object that owns the signals (this will allow to easily cancel launching of the callbacks in dtor). Until, cancellation tokens are introduced.

Things to reconsider

Queue a task to a not running thread?

Possible solutions for adding tasks to a not running thread:

  • Add a task so that it is executed whenever the thread is started
  • Change the function's signature to return bool(false) and do not add task to the queue
  • Assert when thread is not running (current approach)
  • Throw exception if thread is not running

Start/stop thread

Currently, a thread can be stopped and re-started. However, the restart causes a new platform thread (std::thread) to be created. This is a common approach AFAIK but if there are tasks left not executed before the thread is stopped then on a restart they will be run but on a different thread! Reconsider whether this is a valid approach - client developer might expect that if she puts tasks on a specific thread then that's where they are going to be executed.

Suggested solutions:

  • clear the message loop on stop or before re-starting (add appropriate API in base::message_loop)
  • add a possibility to wait on the message loop until start() is called; however, that would require introducing yet another API to shutdown the message loop entirely (the start/stop would become rather pause/resume kind of API)

Thread ID

It would be convenient to have API that would return a unique ID for a thread. It could be the same ID as the ID of the underlying std::thread object, however, when user re-starts the thread a new std::thread object is created with a new ID but the base::thread object, from the user perspective, is still the same object so its ID should not change, right?

Interrupt-able threads

Introduce interruption points into the base::thread. Maybe I should use boost::thread instead of the std::thread. I would get interruption points almost for free (but then I would need to use all the synchronization primitives from boost as well - some of them, e.g. boost::condition_variable are implicit interruption points). This seems to have low priority.

On threads and tasks

lock-free queue

Currently, message loop is built on top of a lock-based priority queue. It might worth to check how lock-free/wait-free priority queue could be implemented (if at all) and estimate whether cost of implementing and maintaining it would be less then the gain of using it.

Synchronisation vs. single thread

Many of the components have to use synchronisation primitives to protect their internal state due to the fact that their API may be called from different threads. This may lead to many hard to detect errors (in particular deadlocks). Solution to this might be for each component to have a private thread to which all the API calls would be directed to behind the scenes. Component's internal state would be referenced only from within this one private thread making explicit synchronisation needless. However, such thread might become a bottleneck since all the API calls would become single-threaded making impossible to increase synchronisation granularity.