I just got burned by using callbacks in a multi-threaded app. I’ve rewritten the part to avoid callbacks, but for my good-deed-of-the-day, I wanted to issue a word of warning.
Before you can safely use a callback / delegate / virtual function, particularly in multi-threaded code, you should answer the following questions:
- Do I hold locks while invoking the callback? If “yes”, then how do you know the callback won’t take a lock that causes a deadlock? If “no”, is this because you’re unsafely releasing a lock just to invoke the callback? In the “no” case, you’re now preventing certain operations from being atomic because you need to toggle a lock to invoke a callback.
- What is the callback allowed to block on? Can the callback take locks? If “no”, then how do you enforce this? If yes, how do you avoid deadlocking? Can the callback make a cross-thread call? If “yes”, how do you ensure that thread is free? If “no”, again, how do you enforce it? Simply calling an STA object may invoke a marshaller that goes across threads. Can it make system calls?
- What is the callback allowed to do? What if the callback throws an exception? What if the callback calls a major function that changes the world out from underneath you (eg, attempting to delete your object)? Can the callback invoke back into your API? Are there reentrancy or layering issues? Could there be infinite recursion here (you invoke the callback, callback invokes you, repeat)?
- Does your function make any assumptions across the callback for which the callback could break underneath you? For example, do you assume that a global variable is constant, when the callback could change the value of that global?
- Is the callback to 3rd-party code outside of my immediate library? That greatly amplifies the unknowns. Now issues of enforcement really come into play. If a 3rd-party callback changes the world underneath you and then returns and your code crashes, then the exception’s callstack is pointing at you and the 3rd-party code has already made a get-away. Even if you think you control all the code, in a large project, that’s still a lot of room for surprises.
- Are you in a restricted context? For example, are you on the UI thread? Do you really want to be calling unpredictable code on your UI thread? That’s a nice recipe to get your app to hang daily.
The questions really could be endless. If you’re code-reviewing somebody else’s code that uses callbacks, these are great questions to ask.
And it applies to managed code too:
Also beware, Virtual functions are basically callbacks. In C#, delegates and events are callbacks, but with a pretty syntax than C++ function pointers.
Callbacks and ICorDebug
The regular reader may realize that ICorDebug uses callbacks to invoke managed debug events (via ICorDebugManagedCallback), and we definitely hit some of these issues. For example, what if you try to Detach() during the middle of a callback? Calling Continue() within a callback vs. outside a callback exercise very different code-paths. These corner cases caused us extra grief when implementing ICorDebug. Since ICD doesn’t have nice short answers here, I’ll blog about that separately.