Library is not thread-safe: deadlock can happen
sergey-shambir opened this issue · comments
Here library calls foreign callbacks under mutex lock:
template <typename... A>
void operator()(A && ... a) {
lock_type lock(m_mutex);
slot_ptr *prev = nullptr;
slot_ptr *curr = m_slots ? &m_slots : nullptr;
while (curr) {
// call non blocked, non connected slots
if ((*curr)->connected()) {
if (!m_block && !(*curr)->blocked())
(*curr)->operator()(std::forward<A>(a)...);
prev = curr;
curr = (*curr)->next ? &((*curr)->next) : nullptr;
}
// remove slots marked as disconnected
else {
if (prev) {
(*prev)->next = (*curr)->next;
curr = (*prev)->next ? &((*prev)->next) : nullptr;
}
else
curr = (*curr)->next ? &((*curr)->next) : nullptr;
}
}
}
Imagine that one thread connects to "signal1" own function "slot1" which fires "signal2". First, it locks "signal1" mutex, then attempts to lock "signal2" mutex.
Another thread connects to "signal2" own function "slot2" which fires "signal1". First, it locks "signal2" mutex, then attempts to lock "signal1" mutex.
In some cases, threads will enter into deadlock: thread1 always waits "signal2" mutex, while thread2 always waits "signal1" mutex.
You are absolutely right.
I think the right think to do may be to just collect the callbacks to be called in a first pass, then release the lock and finally loop over the collected slots and call them one after another.
I worry that doing so may need some dynamic allocation to hold the list of slots, but the good news is that the contention on the lock is likely to drop a lot.
I pushed a fix and a unit test for this issue.
Let me know if you think this is not sufficient.