2

In my application, I have a class which I call a "Signal" (Think Qt signals if you are familiar with that) that represents a one-to-many connection. Objects can register handlers to the signal which, when "emitted" calls each of the handlers.

This is implemented under the hood as a list of function pointers. Making a "connection" adds a function pointer to the list. "Emitting" the signal loops through the list calling each function pointer in turn.

Since "connections" may be made from a thread different than the thread "emitting" the signal, I needed to make signals thread-safe. Since the list of function pointers is a shared resource, this mean adding a mutex to each signal instance that gets lock both when a connection is occurring and when it is being emitted.

This has all been working beautifully until I realized a problem. Mutexes take a relatively large amount of RAM to implement on my system. And, since each signal has a mutex and there are many signals, this means there is a lot of RAM used just to protect the data structures (which is actually more RAM than the data structure itself).

My first thought was to just use a shared mutex for the class, but this would mean only a single signal could ever be emitted at a time no matter the thread. A shared mutex or each thread isn't much better. In either case, there is an ever more fatal problem. With a shared mutex, if an emit ever caused a connection to occur, thus taking the mutex, the mutex would already be blocked because of the emit, thus resulting in a deadlock!

So, after that long explanation, here's my question. Is there some way in which to protect a data structure keeping in mind that, while one is being accessed, that may result in another simultaneous (i.e, nested) access?

This answer does not need to be specific to any language or framework, I just need some ideas as to the concepts.

4
  • "Mutexes take a relatively large amount of RAM to implement on my system." -- really? How much? For how many signals?
    – Erik Eidt
    Commented Oct 7, 2021 at 21:52
  • Look at volatile, compare & swap, spinlock, atomics. You'll find something that will help you manage concurrently shared data structures without mutexs, especially if you are willing to put some limits on their operations.
    – Erik Eidt
    Commented Oct 7, 2021 at 22:16
  • I understand the need to lock when adding or removing. Why do you think a lock is needed when emitting?
    – John Wu
    Commented Oct 7, 2021 at 22:42
  • There's likely no reason to invoke observers that installed as a result triggering, they're probably waiting for the next trigger, so just need to be able to differentiate between observers that should be run in response to this trigger, and which ones are added new. That can be done a number of ways (and depending on whether the observers are meant to be kept or released on triggering).
    – Erik Eidt
    Commented Oct 8, 2021 at 0:05

1 Answer 1

5

Use an immutable list. Avoid stomping on other threads by taking a temporary copy.

First, define a global pointer to the most current list.

var globalList = new List<EmitAction>();

Never modify this list in place.

Instead, when you need to add, do this:

lock
{
    var newList = globalList.Clone().Append(newEntry);
    globalList = newList;
}

When you need to remove, do this:

lock
{
    var newList = globalList.Clone().Remove(entryToRemove);
    globalList = newList;
}

When you need to emit, do this:

var temporaryCopy = globalList;
foreach (entry in temporaryCopy) entry.Emit();

Since globalList never points to a list that might change, you don't have to lock anything when emitting. The pointer itself might change, but you can make a copy of that.

You still need to lock when adding or removing-- otherwise entries can get lost. But I assume adding and removing is a minority use case compared to emitting.

P.S. Some languages have constructs designed exactly for this purpose, e.g. c#'s ImmutableList.

5
  • 1
    This solution implicitly relies on GC - but it can work in other languages if the pointer to the list head is ref-counted - and that ref-counted pointer is thread safe. BUT!! This implementation is flawed - consider TWO threads adding to the same signal simultaneously. If interleaved "badly" they'll both take a copy of the list, both will add their connection to their copy, and both will try to update the list head - last one will win, other will have its connection lost. It can be made to work with a mutex of course, and it won't need locking during emitting callbacks, only add/remove.
    – davidbak
    Commented Oct 7, 2021 at 23:49
  • Yes, locks are necessary during add/remove-- I thought I said that. I agree GC certainly makes it easier.
    – John Wu
    Commented Oct 8, 2021 at 0:09
  • oh darn, didn't see the lock statement in plain sight.
    – davidbak
    Commented Oct 8, 2021 at 0:18
  • This looks like a performance nightmare. Commented Oct 8, 2021 at 19:11
  • @RobertHarvey I think the advantage is that, even though the performance of adding to the list is really poor, this happens very infrequently (i.e., once or twice per signals' lifetimes) whereas reading from the list happens quite often. Commented Oct 8, 2021 at 19:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.