0
\$\begingroup\$

I have an event_loop implementation (basically an event/command queue and an exec function). I have added interrupt functionality that should end the event processing as soon as possible (i.e. it puts an "exit" command at the front of the queue). In practice, everything seems to work just fine (I use it to interrupt pending redraw calls on a UI window, which it seems to do well).

I cannot seem to write a solid unit test for this code. I have made two attempts: one first, maybe naive version, and one I thought long about to ensure I am gridlocking/synchronizing the scenario everywhere it is necessary. The result is a test that hangs sometimes, and (what else) only on CI, and on one platform (Windows + Visual Studio). All other tested platforms (Mac OS X, Linux, Windows with any combination of GCC/Clang) haven't shown any signs of deadlock issues.

Without further ado, the code is available in full form on my project's git repository. To build and run the test from there, you run only that test (this will prevent ~95% of the other code in the project from being compiled):

ctest -R core/event_loop

This should build the core library and the event_loop test executable, and execute it.

The sometimes deadlocking test code is as follows, after my mental image of what the test should be doing in what order:

//thread 1                     | thread 2
// start                       |
//                             | start
// lock mutex                  |
// ready = false               |
// *add command1*              |
// unlock mutex/wait condition |
//                             | [command1] lock mutex
//                             | [command1] ready = true
//                             | [command1] unlock mutex
//                             | [command1] notify condition
// [condition] locks mutex     |
// *add command 2*             |
// *add interrupt command*     |
// unlock mutex2               |
//                             | [command1] lock mutex2
//                             | [command1] finish
//                             | [interrupt command] exit = false before command2
//                           join
// check 2 commands executed
void test_execute_interrupt()
{
  skui::core::event_loop event_loop;
  int exit_code {0};

  std::thread thread([&event_loop, &exit_code] { exit_code = event_loop.execute(); });

  std::mutex mutex, mutex2;
  std::condition_variable condition;
  std::atomic_bool ready;

  std::unique_lock lock{mutex}; // block command until we wait on condition
  std::unique_lock lock2{mutex2}; // block part 2 of command
  event_loop.push(std::make_unique<skui::core::command>([&mutex, &mutex2,
                                                         &condition,
                                                         &ready]
                                                        {
                                                          // block command until wait on condition has been called
                                                          std::unique_lock thread_lock{mutex};
                                                          ready = true;
                                                          thread_lock.unlock();
                                                          condition.notify_one();

                                                          // block command until main thread has requested interrupt
                                                          std::unique_lock thread_lock2{mutex2};
                                                        }));
  condition.wait(lock,
                 [&ready] { return ready.load(); });

  event_loop.push(std::make_unique<skui::core::command>([this] { f(); }));
  event_loop.interrupt(1);

  lock2.unlock();
  thread.join(); // wait for event_loop to exit

  check(count == 0, "Interrupt prevents further command execution.");
  check(exit_code == 1, "Interrupt returns proper exit code.");
}

I haven't ensured that this is the code causing the hang (it only happens on CI and haven't toyed around with debug prints). The unabridged test executable code can be found in a standalone form here. I "preprocessed" the file, but left all namespaces in. If I can/need to further improve this code, let me know what I can do.

The test failing and succeeding can be seen in e.g. the last build of this project on its Appveyor, where it hangs once out of four Visual Studio build configurations. As far as I can tell, which one it fails on is random.

I'm looking for suggestions and/or solutions specifically for the deadlock issue. All the test needs to do is check that if interrupt is called, it puts an exit event in the front of the queue (i.e. the queued events at the time interrupt is called, are not executed). Any other comments are of course welcome, but surely I'm not asking to review all this code.

\$\endgroup\$
  • \$\begingroup\$ It's hard to review a possible deadlock without seeing the code on the other side. You should at least post the event cue or a runnable stub that simulates that. Does the first mutex protect anything else besides writing to the atomic ? Its not really necessary for that. \$\endgroup\$ – Harald Scheirich Mar 8 at 19:21
  • \$\begingroup\$ The full code is available at the link in my post (coliru.stacked-crooked.com/a/9b2e76f6ac2995c3). The first mutex should ensure the event_loop has actually started processing and has started the first command pushed to it. Without it, the interrupt might be called before the event loop thread has actually started. \$\endgroup\$ – rubenvb Mar 9 at 10:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.