Skip to content

Commit 2bcfe9b

Browse files
facontidavideclaude
andcommitted
blackboard entry deleter: noexcept fast path, destroy values outside the storage lock
Changes requested by two independent reviews: - RetireEntry is noexcept, and neither locks nor allocates when nothing is parked - unset(), clear() and cloneInto() destroy the removed values after releasing storage_mutex_ - parked entries are also swept when a new entry is created, to bound the delay of the stored value's destructor - document that LockedPtr is not valid after unlock() Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 793b4c6 commit 2bcfe9b

4 files changed

Lines changed: 71 additions & 22 deletions

File tree

‎include/behaviortree_cpp/blackboard.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,8 @@ inline T Blackboard::get(const std::string& key) const
255255

256256
inline void Blackboard::unset(const std::string& key)
257257
{
258+
// the entry (i.e. the stored value) is destroyed outside the lock
259+
std::shared_ptr<Entry> removed;
258260
std::unique_lock storage_lock(storage_mutex_);
259261

260262
// check local storage
@@ -265,6 +267,7 @@ inline void Blackboard::unset(const std::string& key)
265267
return;
266268
}
267269

270+
removed = std::move(it->second);
268271
storage_.erase(it);
269272
}
270273

‎include/behaviortree_cpp/utils/locked_reference.hpp‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ namespace BT
1212
*
1313
* As long as the object remains in scope, the mutex is locked, therefore
1414
* you must destroy this instance as soon as the pointer was used.
15+
*
16+
* LockedPtr does not own the object it points to: the owner is only expected
17+
* to keep it alive while the mutex is locked. In particular, the pointer must
18+
* not be considered valid after calling unlock().
1519
*/
1620
template <typename T>
1721
class LockedPtr

‎src/blackboard.cpp‎

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "behaviortree_cpp/json_export.h"
44

55
#include <algorithm>
6+
#include <atomic>
67
#include <mutex>
78
#include <tuple>
89
#include <unordered_set>
@@ -29,6 +30,7 @@ struct RetiredEntries
2930
{
3031
std::mutex mutex;
3132
std::vector<Blackboard::Entry*> entries;
33+
std::atomic_size_t count = 0;
3234
};
3335

3436
RetiredEntries& GetRetiredEntries()
@@ -40,31 +42,44 @@ RetiredEntries& GetRetiredEntries()
4042
return *instance;
4143
}
4244

43-
// Deleter of every shared_ptr<Entry>. Being attached at allocation time, it
44-
// covers all the removal paths, including those inlined in the user's binary.
45-
void RetireEntry(Blackboard::Entry* retired)
45+
// No shared_ptr is left when this is called, so nobody can lock the entry
46+
// anymore: if try_lock() succeeds, the entry is unreachable.
47+
// Note: the calling thread may be the one holding the AnyPtrLocked. A
48+
// try_lock() on a std::mutex owned by the caller is formally undefined, but
49+
// it simply fails on all the supported platforms (pthread: EBUSY).
50+
bool IsUnlocked(Blackboard::Entry* entry)
51+
{
52+
if(entry->entry_mutex.try_lock())
53+
{
54+
entry->entry_mutex.unlock();
55+
return true;
56+
}
57+
return false;
58+
}
59+
60+
// Destroy the parked entries that have been unlocked in the meantime.
61+
// If "retired" is not null, it is parked first.
62+
void SweepRetiredEntries(Blackboard::Entry* retired = nullptr) noexcept
4663
{
64+
auto& parked = GetRetiredEntries();
4765
std::vector<Blackboard::Entry*> unlocked;
66+
try
4867
{
49-
auto& parked = GetRetiredEntries();
5068
const std::scoped_lock lock(parked.mutex);
51-
parked.entries.push_back(retired);
52-
// No shared_ptr is left, so nobody can lock these entries anymore: once
53-
// try_lock() succeeds, the entry is unreachable.
54-
// Note: the calling thread may be the one holding the AnyPtrLocked. A
55-
// try_lock() on a std::mutex owned by the caller is formally undefined, but
56-
// it simply fails on all the supported platforms (pthread: EBUSY).
69+
if(retired != nullptr)
70+
{
71+
parked.entries.push_back(retired);
72+
}
5773
const auto it = std::partition(parked.entries.begin(), parked.entries.end(),
58-
[](Blackboard::Entry* entry) {
59-
if(entry->entry_mutex.try_lock())
60-
{
61-
entry->entry_mutex.unlock();
62-
return false;
63-
}
64-
return true;
65-
});
74+
[](auto* entry) { return !IsUnlocked(entry); });
6675
unlocked.assign(it, parked.entries.end());
6776
parked.entries.erase(it, parked.entries.end());
77+
parked.count = parked.entries.size();
78+
}
79+
catch(...) // NOLINT(bugprone-empty-catch)
80+
{
81+
// Out of memory. Whatever could not be moved to "unlocked" remains parked
82+
// (or leaks, in the case of "retired"), which is safe.
6883
}
6984
// Destroyed outside the lock: the destructor of a stored value may remove
7085
// other entries, i.e. call this function again.
@@ -74,6 +89,19 @@ void RetireEntry(Blackboard::Entry* retired)
7489
}
7590
}
7691

92+
// Deleter of every shared_ptr<Entry>. Being attached at allocation time, it
93+
// covers all the removal paths, including those inlined in the user's binary.
94+
void RetireEntry(Blackboard::Entry* retired) noexcept
95+
{
96+
// Common case: nothing is parked and nobody holds this entry
97+
if(GetRetiredEntries().count == 0 && IsUnlocked(retired))
98+
{
99+
delete retired; // NOLINT(cppcoreguidelines-owning-memory)
100+
return;
101+
}
102+
SweepRetiredEntries(retired);
103+
}
104+
77105
std::shared_ptr<Blackboard::Entry> MakeEntry(const TypeInfo& info)
78106
{
79107
return { new Blackboard::Entry(info), &RetireEntry };
@@ -221,8 +249,10 @@ std::vector<StringView> Blackboard::getKeys() const
221249

222250
void Blackboard::clear()
223251
{
252+
// the entries (i.e. the stored values) are destroyed outside the lock
253+
decltype(storage_) removed;
224254
const std::unique_lock storage_lock(storage_mutex_);
225-
storage_.clear();
255+
removed.swap(storage_);
226256
}
227257

228258
void Blackboard::createEntry(const std::string& key, const TypeInfo& info)
@@ -322,14 +352,20 @@ void Blackboard::cloneInto(Blackboard& dst) const
322352
// Step 3: insert new entries and remove stale ones under dst.storage_mutex_.
323353
if(!new_entries.empty() || !keys_to_remove.empty())
324354
{
355+
// the stale entries are destroyed outside the lock
356+
std::vector<std::shared_ptr<Entry>> removed;
325357
const std::unique_lock dst_lock(dst.storage_mutex_);
326358
for(auto& [key, entry] : new_entries)
327359
{
328360
dst.storage_.try_emplace(key, std::move(entry));
329361
}
330362
for(const auto& key : keys_to_remove)
331363
{
332-
dst.storage_.erase(key);
364+
if(auto it = dst.storage_.find(key); it != dst.storage_.end())
365+
{
366+
removed.push_back(std::move(it->second));
367+
dst.storage_.erase(it);
368+
}
333369
}
334370
}
335371
}
@@ -355,6 +391,12 @@ std::shared_ptr<Blackboard::Entry> Blackboard::createEntryImpl(const std::string
355391
return rootBlackboard()->createEntryImpl(key.substr(1, key.size() - 1), info);
356392
}
357393

394+
// Bound the lifetime of the entries that were removed while locked
395+
if(GetRetiredEntries().count != 0)
396+
{
397+
SweepRetiredEntries();
398+
}
399+
358400
const std::unique_lock storage_lock(storage_mutex_);
359401
// This function might be called recursively, when we do remapping, because we move
360402
// to the top scope to find already existing entries

‎tests/gtest_blackboard.cpp‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -415,9 +415,9 @@ TEST(BlackboardTest, AnyPtrLockedDeferredEntryIsDestroyed)
415415
// still alive, since we hold the lock
416416
ASSERT_FALSE(weak_value.expired());
417417
}
418-
// the lock was released: the entry is destroyed by the next removal
418+
// the lock was released: the entry is destroyed when the next one is
419+
// created (or removed)
419420
blackboard->set("other", 1);
420-
blackboard->unset("other");
421421
ASSERT_TRUE(weak_value.expired());
422422
}
423423

0 commit comments

Comments
 (0)