Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

depthai::initialize() not thread or exception safe; recommend using singleton #276

Closed
diablodale opened this issue Nov 23, 2021 · 4 comments · Fixed by #277
Closed

depthai::initialize() not thread or exception safe; recommend using singleton #276

diablodale opened this issue Nov 23, 2021 · 4 comments · Fixed by #277

Comments

@diablodale
Copy link
Contributor

depthai::initialize() does not handle simultaneous threads attempting to initialize() with blocking, and does not handle scenarios of exceptions. Easy to fix (see below)

Setup

  • all OS
  • all compilers
  • depthai-core at main

Repro

By code review.

bool initialize(std::string additionalInfo, bool installSignalHandler) {
// atomic bool for checking whether depthai was already initialized
static std::atomic<bool> initialized{false};
if(initialized.exchange(true)) return true;
#ifdef DEPTHAI_ENABLE_BACKWARD
// install backward if specified
auto envSignalHandler = spdlog::details::os::getenv("DEPTHAI_INSTALL_SIGNAL_HANDLER");

Imagine multiple thread running on a multicore CPU which can concurrently run at the same time.

Scenario 1 - thread unsafe

time index thread 1 thread 2
1 initialize() initialize()
2   std::atomic initialized{false}
3 initialized.exchange(true) returns false initialized.exchange(true) returns true, therefore returns back to caller
4 OS chooses to pause this thread 💥other code runs and access/uses the variables and structs which are not yet initialized
5 OS chooses to pause this thread  

The existing code does not distinguish between start/completed initialization. It also does not block other threads while initialization is in progress.

Scenario 2 - exceptions

time index thread 1 thread 2
1 initialize() initialize()
2 std::atomic initialized{false}  
3 initialized.exchange(true) returns false initialized.exchange(true) returns true, therefore returns back to caller
4
5 std::make_uniquebackward::SignalHandling() throws an exception -or- any code in initialize() throws
6   💥other code runs and access/uses the variables and structs which are not yet initialized

The existing code does not distinguish between start/completed initialization. If something throws during initialization, then all threads are locked out and can not try to initialize again.

Expected

Thread safe, exception safe.
Threads are blocked until initialization successfully completes.

Fix

This is easily fixed with a singleton approach. Use a normal static const bool in the function and put all the code in a lambda.
This guarantees threads will block while another thread is attempting to initialize. It allows any thread to attempt init again. And it handles exceptions because c++ guarantees that if an exception occurs in such a local static var initialization, then that init doesn't occur and therefore another thread can attempt again.

For example pseudocode...

bool initialize(std::string additionalInfo, bool installSignalHandler) {
    // singleton for checking whether depthai was already initialized
    static const bool initialized = [&]() {
#ifdef DEPTHAI_ENABLE_BACKWARD
        ...
        return true;
    }();
    return initialized;
}
@themarpe
Copy link
Collaborator

Good catch @diablodale - this should've been a mutex + bool instead to block other callers in the mean time.
Interesting shorthand approach on the lambda singleton.
Any differences compared to a mutex+bool vs the lambda singleton?

And if you'd like, please open a PR for it, either approach seems fine. (otherwise will address by the end of the week)

@diablodale
Copy link
Contributor Author

I have read the meyer single approach (these are based on c++11 "magic" statics) is the best choice. The C++ runtime and compiler manage the "magic" locks internally allowing for widespread optimization since it is a native language feature. When the same features of a meyer singleton are replicated with manual C++ code by the dev, there are less opportunities for these optimizations and most crucially more opportunity for dev mistakes/poor design.

This has been my learning for years. Here is one article which is on topic https://www.modernescpp.com/index.php/thread-safe-initialization-of-a-singleton showing the Meyer Singleton is almost 3 magnitudes faster than a mutex+lock_guard

@diablodale
Copy link
Contributor Author

I'll push a PR. I applied my fix and it passed all 60/61 of the depthai-core test suite. The one test fails due to my OAK-D not having RGBD cali data in EEPROM.

[ctest] 
[ctest] 98% tests passed, 1 tests failed out of 61
[ctest] 
[ctest] Total Test time (real) = 1279.47 sec
[ctest] 
[ctest] The following tests FAILED:
[ctest] 	 51 - rgb_depth_aligned (Failed)
[ctest] Errors while running CTest
[ctest] CTest finished with return code 8

diablodale added a commit to diablodale/depthai-core that referenced this issue Nov 23, 2021
diablodale added a commit to diablodale/depthai-core that referenced this issue Nov 23, 2021
@themarpe themarpe linked a pull request Nov 24, 2021 that will close this issue
themarpe pushed a commit that referenced this issue Nov 24, 2021
@themarpe
Copy link
Collaborator

(Merge defaulted to squash - seems like that's why it didn't close automatically. Closing)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants