diff --git a/include/behaviortree_cpp/blackboard.h b/include/behaviortree_cpp/blackboard.h index c88924400..b2ab22b14 100644 --- a/include/behaviortree_cpp/blackboard.h +++ b/include/behaviortree_cpp/blackboard.h @@ -255,17 +255,25 @@ inline T Blackboard::get(const std::string& key) const inline void Blackboard::unset(const std::string& key) { - std::unique_lock storage_lock(storage_mutex_); - - // check local storage - auto it = storage_.find(key); - if(it == storage_.end()) + std::shared_ptr entry; { - // No entry, nothing to do. - return; - } + std::unique_lock storage_lock(storage_mutex_); - storage_.erase(it); + // check local storage + auto it = storage_.find(key); + if(it == storage_.end()) + { + // No entry, nothing to do. + return; + } + entry = std::move(it->second); + storage_.erase(it); + } + // Wait for any AnyPtrLocked still holding entry_mutex, so that the entry + // is not destroyed while its mutex is locked. Note: for the same reason, + // unset() must not be called while holding an AnyPtrLocked to the same + // entry in the calling thread. + const std::scoped_lock entry_lock(entry->entry_mutex); } template diff --git a/src/blackboard.cpp b/src/blackboard.cpp index 7858b4655..04f4cd93b 100644 --- a/src/blackboard.cpp +++ b/src/blackboard.cpp @@ -23,18 +23,30 @@ void Blackboard::enableAutoRemapping(bool remapping) AnyPtrLocked Blackboard::getAnyLocked(const std::string& key) { - if(auto entry = getEntry(key)) + while(auto entry = getEntry(key)) { - return AnyPtrLocked(&entry->value, &entry->entry_mutex); + AnyPtrLocked locked(&entry->value, &entry->entry_mutex); + // Re-check under entry_mutex: unset()/clear() erase the entry first and only + // then wait on entry_mutex before letting it be destroyed. If the key still + // resolves to this entry, it will stay alive as long as the lock is held; + // otherwise release the lock and look the key up again. + if(getEntry(key) == entry) + { + return locked; + } } return {}; } AnyPtrLocked Blackboard::getAnyLocked(const std::string& key) const { - if(auto entry = getEntry(key)) + while(auto entry = getEntry(key)) { - return AnyPtrLocked(&entry->value, const_cast(&entry->entry_mutex)); + AnyPtrLocked locked(&entry->value, const_cast(&entry->entry_mutex)); + if(getEntry(key) == entry) + { + return locked; + } } return {}; } @@ -157,8 +169,18 @@ std::vector Blackboard::getKeys() const void Blackboard::clear() { - const std::unique_lock storage_lock(storage_mutex_); - storage_.clear(); + std::unordered_map> removed; + { + const std::unique_lock storage_lock(storage_mutex_); + removed.swap(storage_); + } + // Wait for any AnyPtrLocked still holding an entry_mutex, so that no entry + // is destroyed while its mutex is locked ("removed" goes out of scope after + // the last lock below has been released). + for(const auto& [key, entry] : removed) + { + const std::scoped_lock entry_lock(entry->entry_mutex); + } } void Blackboard::createEntry(const std::string& key, const TypeInfo& info) @@ -258,14 +280,28 @@ void Blackboard::cloneInto(Blackboard& dst) const // Step 3: insert new entries and remove stale ones under dst.storage_mutex_. if(!new_entries.empty() || !keys_to_remove.empty()) { - const std::unique_lock dst_lock(dst.storage_mutex_); - for(auto& [key, entry] : new_entries) + std::vector> removed_entries; { - dst.storage_.try_emplace(key, std::move(entry)); + const std::unique_lock dst_lock(dst.storage_mutex_); + for(auto& [key, entry] : new_entries) + { + dst.storage_.try_emplace(key, std::move(entry)); + } + for(const auto& key : keys_to_remove) + { + auto it = dst.storage_.find(key); + if(it != dst.storage_.end()) + { + removed_entries.push_back(std::move(it->second)); + dst.storage_.erase(it); + } + } } - for(const auto& key : keys_to_remove) + // Wait for any AnyPtrLocked still holding an entry_mutex before the + // removed entries are destroyed. + for(const auto& entry : removed_entries) { - dst.storage_.erase(key); + const std::scoped_lock entry_lock(entry->entry_mutex); } } } diff --git a/tests/gtest_blackboard.cpp b/tests/gtest_blackboard.cpp index f4ce45689..d921beaf3 100644 --- a/tests/gtest_blackboard.cpp +++ b/tests/gtest_blackboard.cpp @@ -13,6 +13,8 @@ #include "behaviortree_cpp/blackboard.h" #include "behaviortree_cpp/bt_factory.h" +#include + #include #include "../sample_nodes/dummy_nodes.h" @@ -303,6 +305,34 @@ TEST(BlackboardTest, AnyPtrLocked) } #endif +TEST(BlackboardTest, AnyPtrLockedSurvivesUnset) +{ + auto blackboard = Blackboard::create(); + blackboard->set("value", 42); + + auto locked = blackboard->getAnyLocked("value"); + ASSERT_TRUE(bool(locked)); + + // unset() erases the key and then waits on entry_mutex before letting the + // entry be destroyed, so it must run in a separate thread. + std::thread unsetter([&blackboard] { blackboard->unset("value"); }); + + // wait until the key has been erased from the storage + while(!blackboard->getKeys().empty()) + { + std::this_thread::yield(); + } + + // the entry must stay alive as long as we hold the lock + ASSERT_EQ(locked.get()->cast(), 42); + + // release the lock, allowing unset() to complete + locked = {}; + unsetter.join(); + + ASSERT_FALSE(bool(blackboard->getAnyLocked("value"))); +} + TEST(BlackboardTest, SetStringView) { auto bb = Blackboard::create();