1

I would like to know what precautions are needed to be able to safely add callbacks to a libuv event loop from multiple threads in C++.

More details

I have some multi-threaded C++11 code that I want to modify to use make use of libuv's network communication API. I do not want to create a new libuv event loop every time network communication is required (for that would use up resources). So I created a libuv loop in a separate thread (I prevent the loop from closing by registering a "keep-alive" timer). This event loop is currently passed to other threads using a singleton. Callbacks are then registered (from other threads) while the loop is running.

I am worried about concurrent accesses to the libuv event loop when registering new callbacks: when calling uv_tcp_init the loop is explicitly passed (rather, a pointer to the loop); when calling uv_tcp_connect the loop is not explicitly mentioned but a pointer to it is stored in the uv_tcp_t struct passed. I haven't checked whether any of the above-mentioned functions actually modify the loop, but my intuition is at least one of them must do to (otherwise, libuv couldn't keep track of active handles).

My first thought was to add a mutex attribute to the singleton used to access the event loop and use it to prevent concurrent access to the event loop when calling any of the above functions:

EventLoop & loop = EventLoop::get(); // Access the singleton
{
    std::lock_guard<std::mutex> lock(loop.mutex_attribute);
    // Register callbacks, etc
}

However, this does not protect the event loop from concurrent accesses between my thread (which successfully acquired the lock) and some libuv internal function (or a registered callback triggered by libuv) since the latter are not aware of my using a singleton to protect access.

Should I be worried about said concurrent accesses? What steps may I take to mitigate the risks?

Aimery
  • 1,559
  • 1
  • 19
  • 24

1 Answers1

1

The solution I settled for was to not add handles directly to the libuv event loop from other threads, but rather to have other threads add handles to a queue (stored in the same singleton as the pointer to the event loop). Access to the queue is protected by a mutex.

The "keep-alive" timer then periodically empties the queue (the timer callback is aware of the mutex protecting the queue) by:

  • getting the first handle from the queue,
  • registering that handle with the libuv event loop (since we register the handle from a callback within the libuv event loop, there shouldn't be any risks of concurrent access), and performing any other operation needed on this handle (in my case, call uv_tcp_init and uv_tcp_connect),
  • repeating until the queue is empty.
Aimery
  • 1,559
  • 1
  • 19
  • 24
  • 1
    You could write to a PIPE when a callback is added from another thread to trigger a callback in the main thread (which will then add the callbacks from the queue) – Joshua Griffiths Oct 15 '20 at 09:47