This is an automated email from the ASF dual-hosted git repository.
chenBright pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brpc.git
The following commit(s) were added to refs/heads/master by this push:
new 71871ee9 Fix potential bvar deadlock by running describe()/dump()
outside the global VarMap lock (#3470)
71871ee9 is described below
commit 71871ee9fb46cdb39565e3f5af1dfa6949f80bca
Author: Bright Chen <[email protected]>
AuthorDate: Sat Aug 22 23:55:40 2026 +0800
Fix potential bvar deadlock by running describe()/dump() outside the global
VarMap lock (#3470)
---
src/bvar/detail/exposed_ref.h | 101 +++++++++++++++++++++++++++++++++++++
src/bvar/mvariable.cpp | 80 +++++++++++++++++++++---------
src/bvar/mvariable.h | 17 +++++--
src/bvar/variable.cpp | 107 ++++++++++++++++++++++++++++------------
src/bvar/variable.h | 17 +++++--
test/bthread_unittest.cpp | 40 +++++++++++++++
test/bvar_variable_unittest.cpp | 54 ++++++++++++++++++++
7 files changed, 353 insertions(+), 63 deletions(-)
diff --git a/src/bvar/detail/exposed_ref.h b/src/bvar/detail/exposed_ref.h
new file mode 100644
index 00000000..a6a036b6
--- /dev/null
+++ b/src/bvar/detail/exposed_ref.h
@@ -0,0 +1,101 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#ifndef BVAR_DETAIL_EXPOSED_REF_H_
+#define BVAR_DETAIL_EXPOSED_REF_H_
+
+#include <memory>
+#include "butil/macros.h"
+#include "butil/scoped_lock.h"
+#include "butil/synchronization/condition_variable.h"
+
+namespace bvar {
+namespace detail {
+
+// Indirection layer shared by Variable and MVariableBase that lets concurrent
+// readers (describe_exposed() / dump_exposed()) access an exposed object
outside
+// the global map lock. That lock is a pthread mutex and must not wrap user
+// callbacks which may yield the bthread, otherwise it deadlocks (see
+// https://github.com/apache/brpc/issues/2888 for details).
+//
+// Protocol:
+// - A reader increments the reference via acquire() while still holding the
+// global map lock (so it is serialized with the owner's erase from the
+// map), then releases the map lock, uses the returned pointer outside the
+// lock, and finally calls release().
+// - The owner's hide() erases itself from the map and then calls
hide_and_wait(),
+// which blocks until all references acquired before this point are
released,
+// guaranteeing the owner stays alive throughout the reader's use. The
handle
+// is single-use: once hidden it stays hidden, and the owner creates a
fresh
+// one on re-expose.
+template <typename T>
+class ExposedRef {
+public:
+ explicit ExposedRef(T* obj)
+ : _cond(&_mutex), _obj(obj), _nref(0), _hidden(false) {}
+
+ DISALLOW_COPY_AND_ASSIGN(ExposedRef);
+
+ // Must be called while holding the global map lock. Returns nullptr if the
+ // owner is being hidden/destructed. On a non-nullptr return the caller
must
+ // call release() once it finishes using the pointer.
+ T* acquire() {
+ BAIDU_SCOPED_LOCK(_mutex);
+ if (_hidden) {
+ return nullptr;
+ }
+ ++_nref;
+ return _obj;
+ }
+
+ void release() {
+ BAIDU_SCOPED_LOCK(_mutex);
+ if (--_nref == 0 && _hidden) {
+ _cond.Broadcast();
+ }
+ }
+
+ // Called by the owner's hide() after erasing itself from the map. Blocks
+ // until all references acquired before this point are released.
+ void hide_and_wait() {
+ BAIDU_SCOPED_LOCK(_mutex);
+ _hidden = true;
+ while (_nref > 0) {
+ _cond.Wait();
+ }
+ }
+
+private:
+ butil::Mutex _mutex;
+ butil::ConditionVariable _cond;
+ T* _obj;
+ int _nref;
+ bool _hidden;
+};
+
+template <typename T>
+using SharedExposedRef = std::shared_ptr<ExposedRef<T>>;
+
+template <typename T>
+SharedExposedRef<T> make_exposed_ref(T* obj) {
+ return std::make_shared<ExposedRef<T>>(obj);
+}
+
+} // namespace detail
+} // namespace bvar
+
+#endif // BVAR_DETAIL_EXPOSED_REF_H_
diff --git a/src/bvar/mvariable.cpp b/src/bvar/mvariable.cpp
index 45517a28..6fbceb07 100644
--- a/src/bvar/mvariable.cpp
+++ b/src/bvar/mvariable.cpp
@@ -69,11 +69,8 @@ DEFINE_uint32(max_multi_dimension_stats_count, 20000, "Max
stats count of a mult
BUTIL_VALIDATE_GFLAG(max_multi_dimension_stats_count,
validator_max_multi_dimension_stats_count);
-class MVarEntry {
-public:
- MVarEntry() : var(nullptr) {}
-
- MVariableBase* var;
+struct MVarEntry {
+ MVariableBase::SharedExposedRef ref;
};
typedef butil::FlatMap<std::string, MVarEntry> MVarMap;
@@ -119,12 +116,24 @@ std::string MVariableBase::get_description() {
int MVariableBase::describe_exposed(const std::string& name,
std::ostream& os) {
MVarMapWithLock& m = get_mvar_map();
- BAIDU_SCOPED_LOCK(m.mutex);
- MVarEntry* entry = m.seek(name);
- if (entry == nullptr) {
+ MVariableBase* var = nullptr;
+ SharedExposedRef ref;
+ {
+ BAIDU_SCOPED_LOCK(m.mutex);
+ MVarEntry* entry = m.seek(name);
+ if (entry == nullptr) {
+ return -1;
+ }
+ ref = entry->ref;
+ var = ref->acquire();
+ }
+ if (var == nullptr) {
return -1;
}
- entry->var->describe(os);
+ // Call describe() outside the MVarMap lock to avoid deadlock when the user
+ // callback (e.g. Dumper) yields the bthread.
+ var->describe(os);
+ ref->release();
return 0;
}
@@ -149,8 +158,12 @@ int MVariableBase::expose_impl(const butil::StringPiece&
prefix,
// expose a variable more than once and calls to expose() are unlikely
// to contend heavily.
- // remove previous pointer from the map if needed.
+ // Remove previous exposure if needed (hide() waits for in-flight readers
+ // and invalidates `_ref`).
+ // Always start the new exposure with a fresh `_ref`, because a previous
+ // hide() may have permanently hidden the old `_ref`.
hide();
+ _ref = detail::make_exposed_ref(this);
// Build the name.
_name.clear();
@@ -164,7 +177,8 @@ int MVariableBase::expose_impl(const butil::StringPiece&
prefix,
to_underscored_name(&_name, name);
if (count_exposed() >
(size_t)FLAGS_bvar_max_multi_dimension_metric_number) {
- LOG(ERROR) << "Too many metric seen, overflow detected, max metric
count:" << FLAGS_bvar_max_multi_dimension_metric_number;
+ LOG(ERROR) << "Too many metric seen, overflow detected, max metric
count:"
+ << FLAGS_bvar_max_multi_dimension_metric_number;
return -1;
}
@@ -174,7 +188,7 @@ int MVariableBase::expose_impl(const butil::StringPiece&
prefix,
MVarEntry* entry = m.seek(_name);
if (entry == nullptr) {
entry = &m[_name];
- entry->var = this;
+ entry->ref = _ref;
return 0;
}
}
@@ -200,14 +214,23 @@ bool MVariableBase::hide() {
}
MVarMapWithLock& m = get_mvar_map();
- BAIDU_SCOPED_LOCK(m.mutex);
- MVarEntry* entry = m.seek(_name);
- if (entry) {
- CHECK_EQ(1UL, m.erase(_name));
- } else {
- CHECK(false) << "`" << _name << "' must exist";
+ {
+ BAIDU_SCOPED_LOCK(m.mutex);
+ MVarEntry* entry = m.seek(_name);
+ if (entry) {
+ CHECK_EQ(1UL, m.erase(_name));
+ } else {
+ CHECK(false) << "`" << _name << "' must exist";
+ }
}
_name.clear();
+ // Remove previous exposure if needed (hide() waits for in-flight readers
+ // and invalidates `_ref`).
+ // Always start the new exposure with a fresh `_ref`, because a previous
+ // hide() may have permanently hidden the old `_ref`.
+ if (_ref != nullptr) {
+ _ref->hide_and_wait();
+ }
return true;
}
@@ -253,11 +276,22 @@ size_t MVariableBase::dump_exposed(Dumper* dumper, const
DumpOptions* options) {
list_exposed(&mvars);
size_t n = 0;
for (auto& mvar : mvars) {
- MVarMapWithLock& m = get_mvar_map();
- BAIDU_SCOPED_LOCK(m.mutex);
- MVarEntry* entry = m.seek(mvar);
- if (entry) {
- n += entry->var->dump(dumper, &opt);
+ MVariableBase* var = nullptr;
+ SharedExposedRef ref;
+ {
+ MVarMapWithLock& m = get_mvar_map();
+ BAIDU_SCOPED_LOCK(m.mutex);
+ MVarEntry* entry = m.seek(mvar);
+ if (entry) {
+ ref = entry->ref;
+ var = ref->acquire();
+ }
+ }
+ if (var != nullptr) {
+ // Call dump() outside the MVarMap lock to avoid deadlock when the
dump()
+ // yields the bthread.
+ n += var->dump(dumper, &opt);
+ ref->release();
}
if (n >
static_cast<size_t>(FLAGS_bvar_max_dump_multi_dimension_metric_number)) {
LOG(WARNING) << "truncated because of exceed max dump multi
dimension label number["
diff --git a/src/bvar/mvariable.h b/src/bvar/mvariable.h
index ec6eb1ce..95c79264 100644
--- a/src/bvar/mvariable.h
+++ b/src/bvar/mvariable.h
@@ -24,8 +24,11 @@
#include <sstream> // std::ostringstream
#include <list> // std::list
#include <string> // std::string
+#include <vector> // std::vector
+#include <memory> // std::shared_ptr
#include "butil/macros.h" // DISALLOW_COPY_AND_ASSIGN
#include "butil/strings/string_piece.h" // butil::StringPiece
+#include "bvar/detail/exposed_ref.h" // detail::ExposedRef
namespace bvar {
@@ -34,8 +37,16 @@ struct DumpOptions;
class MVariableBase {
public:
+ // Shared, single-use handle that lets describe_exposed()/dump_exposed()
+ // call describe()/dump() OUTSIDE the global MVarMap lock (issue #2888).
+ using SharedExposedRef = detail::SharedExposedRef<MVariableBase>;
+
MVariableBase() = default;
+ // mbvar uses bvar, bvar uses TLS, thus copying/assignment need to copy
TLS stuff as well,
+ // which is heavy. We disable copying/assignment now.
+ DISALLOW_COPY_AND_ASSIGN(MVariableBase);
+
virtual ~MVariableBase();
// Implement this method to print the mvariable info into ostream.
@@ -107,10 +118,8 @@ protected:
protected:
std::string _name;
-
- // mbvar uses bvar, bvar uses TLS, thus copying/assignment need to copy
TLS stuff as well,
- // which is heavy. We disable copying/assignment now.
- DISALLOW_COPY_AND_ASSIGN(MVariableBase);
+ // Shared indirection handle for describe()/dump() outside the MVarMap
lock.
+ SharedExposedRef _ref;
};
template <typename KeyType>
diff --git a/src/bvar/variable.cpp b/src/bvar/variable.cpp
index 17ffed30..688997d6 100644
--- a/src/bvar/variable.cpp
+++ b/src/bvar/variable.cpp
@@ -64,9 +64,11 @@ BAIDU_CASSERT(!(SUB_MAP_COUNT & (SUB_MAP_COUNT - 1)),
must_be_power_of_2);
class VarEntry {
public:
- VarEntry() : var(nullptr), display_filter(DISPLAY_ON_ALL) {}
+ VarEntry() : display_filter(DISPLAY_ON_ALL) {}
- Variable* var;
+ // Indirection handle shared with the Variable. describe_exposed() acquires
+ // it under the VarMap lock and calls describe() outside the lock.
+ Variable::SharedExposedRef ref;
DisplayFilter display_filter;
};
@@ -79,12 +81,7 @@ struct VarMapWithLock : public VarMap {
if (init(1024) != 0) {
LOG(WARNING) << "Fail to init VarMap";
}
-
- pthread_mutexattr_t attr;
- pthread_mutexattr_init(&attr);
- pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
- pthread_mutex_init(&mutex, &attr);
- pthread_mutexattr_destroy(&attr);
+ pthread_mutex_init(&mutex, nullptr);
}
};
@@ -104,7 +101,8 @@ inline size_t sub_map_index(const std::string& str) {
return 0;
}
size_t h = 0;
- // we're assume that str is ended with '\0', which may not be in general
+ // We assume that str is ended with '\0', which may not be in general;
+ // otherwise the hash stops early.
for (const char* p = str.c_str(); *p; ++p) {
h = h * 5 + *p;
}
@@ -140,8 +138,12 @@ int Variable::expose_impl(const butil::StringPiece& prefix,
// expose a variable more than once and calls to expose() are unlikely
// to contend heavily.
- // remove previous pointer from the map if needed.
+ // Remove previous exposure if needed (hide() waits for in-flight readers
+ // and invalidates `_ref`).
+ // Always start the new exposure with a fresh `_ref`, because a previous
+ // hide() may have permanently hidden the old `_ref`.
hide();
+ _ref = detail::make_exposed_ref(this);
// Build the name.
_name.clear();
@@ -160,7 +162,7 @@ int Variable::expose_impl(const butil::StringPiece& prefix,
VarEntry* entry = m.seek(_name);
if (entry == nullptr) {
entry = &m[_name];
- entry->var = this;
+ entry->ref = _ref;
entry->display_filter = display_filter;
return 0;
}
@@ -189,14 +191,23 @@ bool Variable::hide() {
return false;
}
VarMapWithLock& m = get_var_map(_name);
- BAIDU_SCOPED_LOCK(m.mutex);
- VarEntry* entry = m.seek(_name);
- if (entry) {
- CHECK_EQ(1UL, m.erase(_name));
- } else {
- CHECK(false) << "`" << _name << "' must exist";
+ {
+ BAIDU_SCOPED_LOCK(m.mutex);
+ VarEntry* entry = m.seek(_name);
+ if (entry) {
+ CHECK_EQ(1UL, m.erase(_name));
+ } else {
+ CHECK(false) << "`" << _name << "' must exist";
+ }
}
_name.clear();
+ // Remove previous exposure if needed (hide() waits for in-flight readers
+ // and invalidates `_ref`).
+ // Always start the new exposure with a fresh `_ref`, because a previous
+ // hide() may have permanently hidden the old `_ref`.
+ if (_ref != nullptr) {
+ _ref->hide_and_wait();
+ }
return true;
}
@@ -249,15 +260,28 @@ int Variable::describe_exposed(const std::string& name,
std::ostream& os,
bool quote_string,
DisplayFilter display_filter) {
VarMapWithLock& m = get_var_map(name);
- BAIDU_SCOPED_LOCK(m.mutex);
- VarEntry* p = m.seek(name);
- if (p == nullptr) {
- return -1;
+ Variable* var = nullptr;
+ SharedExposedRef ref;
+ {
+ BAIDU_SCOPED_LOCK(m.mutex);
+ VarEntry* p = m.seek(name);
+ if (p == nullptr) {
+ return -1;
+ }
+ if (!(display_filter & p->display_filter)) {
+ return -1;
+ }
+ ref = p->ref;
+ var = ref->acquire();
}
- if (!(display_filter & p->display_filter)) {
+ if (var == nullptr) {
+ // The variable is being destructed.
return -1;
}
- p->var->describe(os, quote_string);
+ // Call describe() outside the VarMap lock to avoid deadlock when the user
+ // callback (e.g. PassiveStatus) yields the bthread.
+ var->describe(os, quote_string);
+ ref->release();
return 0;
}
@@ -289,23 +313,44 @@ int Variable::describe_series_exposed(const std::string&
name,
std::ostream& os,
const SeriesOptions& options) {
VarMapWithLock& m = get_var_map(name);
- BAIDU_SCOPED_LOCK(m.mutex);
- VarEntry* p = m.seek(name);
- if (p == nullptr) {
+ Variable* var = nullptr;
+ SharedExposedRef ref;
+ {
+ BAIDU_SCOPED_LOCK(m.mutex);
+ VarEntry* p = m.seek(name);
+ if (p == nullptr) {
+ return -1;
+ }
+ ref = p->ref;
+ var = ref->acquire();
+ }
+ if (var == nullptr) {
return -1;
}
- return p->var->describe_series(os, options);
+ const int rc = var->describe_series(os, options);
+ ref->release();
+ return rc;
}
#ifdef BAIDU_INTERNAL
int Variable::get_exposed(const std::string& name, boost::any* value) {
VarMapWithLock& m = get_var_map(name);
- BAIDU_SCOPED_LOCK(m.mutex);
- VarEntry* p = m.seek(name);
- if (p == nullptr) {
+ Variable* var = nullptr;
+ SharedExposedRef ref;
+ {
+ BAIDU_SCOPED_LOCK(m.mutex);
+ VarEntry* p = m.seek(name);
+ if (p == nullptr) {
+ return -1;
+ }
+ ref = p->ref;
+ var = ref->acquire();
+ }
+ if (var == nullptr) {
return -1;
}
- p->var->get_value(value);
+ var->get_value(value);
+ ref->release();
return 0;
}
#endif
diff --git a/src/bvar/variable.h b/src/bvar/variable.h
index b13f000f..cbc14cfb 100644
--- a/src/bvar/variable.h
+++ b/src/bvar/variable.h
@@ -23,9 +23,11 @@
#include <ostream> // std::ostream
#include <string> // std::string
#include <vector> // std::vector
+#include <memory> // std::shared_ptr
#include <gflags/gflags_declare.h>
#include "butil/macros.h" // DISALLOW_COPY_AND_ASSIGN
#include "butil/strings/string_piece.h" // butil::StringPiece
+#include "bvar/detail/exposed_ref.h" // detail::ExposedRef
#ifdef BAIDU_INTERNAL
#include <boost/any.hpp>
@@ -117,7 +119,14 @@ struct SeriesOptions {
// safely (provided that there's no non-const methods going on).
class Variable {
public:
- Variable() {}
+ using SharedExposedRef = detail::SharedExposedRef<Variable>;
+
+ Variable() = default;
+
+ // bvar uses TLS, thus copying/assignment need to copy TLS stuff as well,
+ // which is heavy. We disable copying/assignment now.
+ DISALLOW_COPY_AND_ASSIGN(Variable);
+
virtual ~Variable();
// Implement this method to print the variable into ostream.
@@ -234,10 +243,8 @@ protected:
private:
std::string _name;
-
- // bvar uses TLS, thus copying/assignment need to copy TLS stuff as well,
- // which is heavy. We disable copying/assignment now.
- DISALLOW_COPY_AND_ASSIGN(Variable);
+ // Shared indirection handle for calling describe() outside the VarMap
lock.
+ SharedExposedRef _ref;
};
// Make name only use lowercased alphabets / digits / underscores, and append
diff --git a/test/bthread_unittest.cpp b/test/bthread_unittest.cpp
index 2d1afa00..ce2b368c 100644
--- a/test/bthread_unittest.cpp
+++ b/test/bthread_unittest.cpp
@@ -22,9 +22,12 @@
#include "butil/macros.h"
#include "butil/logging.h"
#include "gperftools_helper.h"
+#include <vector>
+#include <sstream>
#include "bthread/bthread.h"
#include "bthread/unstable.h"
#include "bthread/task_meta.h"
+#include "bvar/bvar.h"
int main(int argc, char* argv[]) {
testing::InitGoogleTest(&argc, argv);
@@ -701,4 +704,41 @@ TEST_F(BthreadTest, trace) {
}
#endif // BRPC_BTHREAD_TRACER
+// Regression test for https://github.com/apache/brpc/issues/2888 .
+// Reproduce the real deadlock: many bthreads (>> bthread_concurrency) call
+// describe_exposed() on the same variable concurrently, and the user callback
+// yields the bthread. In the buggy version describe() runs while holding the
+// global VarMap pthread mutex, so once a bthread yields inside the callback
the
+// pthread worker picks up another describing bthread that blocks on the same
+// submap mutex; with enough bthreads all workers get stuck and the process
+// deadlocks. With the fix (describe() runs OUTSIDE the lock) this finishes.
+int yielding_getfn(void*) {
+ bthread_usleep(500);
+ return 0;
+}
+
+void* describe_same_var(void*) {
+ std::ostringstream os;
+ bvar::Variable::describe_exposed("bthread_describe_deadlock", os);
+ return nullptr;
+}
+
+TEST_F(BthreadTest, describe_exposed_yields_in_bthread_no_deadlock) {
+ bvar::PassiveStatus<int> ps(
+ "bthread_describe_deadlock", yielding_getfn, nullptr);
+
+ // n >> bthread_concurrency, so that in the buggy version every worker ends
+ // up blocked on the same submap mutex held by a yielded bthread.
+ const int N = 1000;
+ std::vector<bthread_t> tids(N);
+ for (int i = 0; i < N; ++i) {
+ ASSERT_EQ(0, bthread_start_background(
+ &tids[i], nullptr, describe_same_var, nullptr));
+ }
+ for (int i = 0; i < N; ++i) {
+ ASSERT_EQ(0, bthread_join(tids[i], nullptr));
+ }
+ // Reaching here (instead of hanging) means there was no deadlock.
+}
+
} // namespace
diff --git a/test/bvar_variable_unittest.cpp b/test/bvar_variable_unittest.cpp
index b24c3057..f2a3edb7 100644
--- a/test/bvar_variable_unittest.cpp
+++ b/test/bvar_variable_unittest.cpp
@@ -18,9 +18,11 @@
// Date: Fri Jul 24 17:19:40 CST 2015
#include <pthread.h> // pthread_*
+#include <unistd.h> // usleep
#include <cstddef>
#include <memory>
+#include <thread>
#include <iostream>
#include <sstream>
#include "butil/time.h"
@@ -408,6 +410,58 @@ TEST_F(VariableTest, recursive_mutex) {
LOG(INFO) << "Each recursive mutex lock/unlock pair take "
<< timer.n_elapsed() / N << "ns";
}
+
+struct BlockingDescribeCtx {
+ butil::atomic<bool> entered;
+ butil::atomic<bool> release;
+ BlockingDescribeCtx() : entered(false), release(false) {}
+};
+
+int blocking_getfn(void* arg) {
+ BlockingDescribeCtx* c = static_cast<BlockingDescribeCtx*>(arg);
+ c->entered.store(true);
+ while (!c->release.load()) {
+ usleep(1000);
+ }
+ return 42;
+}
+
+// A Variable dtor must block until all in-flight describe_exposed() finish,
+// otherwise a concurrent describe() would use-after-free the Variable.
+TEST_F(VariableTest, dtor_waits_for_inflight_describe) {
+ BlockingDescribeCtx ctx;
+ auto a = new bvar::PassiveStatus<int>(
+ "lockfree_describe_b", blocking_getfn, &ctx);
+
+ std::thread describer([]{
+ std::ostringstream os;
+ bvar::Variable::describe_exposed("lockfree_describe_b", os);
+ });
+ while (!ctx.entered.load()) {
+ usleep(1000);
+ }
+
+ // Destruct in another thread. It must block inside hide()'s
hide_and_wait()
+ // until the in-flight describe finishes.
+ butil::atomic<bool> destructed(false);
+ std::thread destroyer([&]{
+ delete a;
+ destructed.store(true);
+ });
+
+ // While the callback is still blocked, the dtor must not have completed.
+ usleep(200 * 1000);
+ ASSERT_FALSE(destructed.load())
+ << "Variable dtor did not wait for the in-flight describe() to
finish.";
+
+ // Release the callback; describe() returns, ref-count drops to 0, and the
+ // dtor is unblocked.
+ ctx.release.store(true);
+ describer.join();
+ destroyer.join();
+
+ ASSERT_TRUE(destructed.load());
+}
} // namespace
int main(int argc, char** argv) {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]