| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "base/synchronization/lock.h" |
| |
| #include <stdint.h> |
| |
| #include <algorithm> |
| #include <atomic> |
| #include <cmath> |
| #include <cstdint> |
| #include <memory> |
| #include <utility> |
| |
| #include "base/android/background_thread_pool_field_trial.h" |
| #include "base/check.h" |
| #include "base/compiler_specific.h" |
| #include "base/dcheck_is_on.h" |
| #include "base/features.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/functional/function_ref.h" |
| #include "base/location.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/raw_ref.h" |
| #include "base/profiler/thread_delegate.h" |
| #include "base/rand_util.h" |
| #include "base/synchronization/lock_impl.h" |
| #include "base/synchronization/lock_subtle.h" |
| #include "base/system/sys_info.h" |
| #include "base/test/bind.h" |
| #include "base/test/gtest_util.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/thread_annotations.h" |
| #include "base/threading/platform_thread.h" |
| #include "base/time/time.h" |
| #include "base/timer/elapsed_timer.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using testing::UnorderedElementsAre; |
| using testing::UnorderedElementsAreArray; |
| |
| namespace base { |
| |
| // Basic test to make sure that Acquire()/Release()/Try() don't crash ---------- |
| |
| class BasicLockTestThread : public PlatformThread::Delegate { |
| public: |
| explicit BasicLockTestThread(Lock* lock) : lock_(lock) {} |
| |
| BasicLockTestThread(const BasicLockTestThread&) = delete; |
| BasicLockTestThread& operator=(const BasicLockTestThread&) = delete; |
| |
| void ThreadMain() override { |
| for (int i = 0; i < 10; i++) { |
| lock_->Acquire(); |
| acquired_++; |
| lock_->Release(); |
| } |
| for (int i = 0; i < 10; i++) { |
| lock_->Acquire(); |
| acquired_++; |
| PlatformThread::Sleep(RandTimeDeltaUpTo(Milliseconds(20))); |
| lock_->Release(); |
| } |
| for (int i = 0; i < 10; i++) { |
| if (lock_->Try()) { |
| acquired_++; |
| PlatformThread::Sleep(RandTimeDeltaUpTo(Milliseconds(20))); |
| lock_->Release(); |
| } |
| } |
| } |
| |
| int acquired() const { return acquired_; } |
| |
| private: |
| raw_ptr<Lock> lock_; |
| int acquired_ = 0; |
| }; |
| |
| TEST(LockTest, Basic) { |
| Lock lock; |
| BasicLockTestThread thread(&lock); |
| PlatformThreadHandle handle; |
| |
| ASSERT_TRUE(PlatformThread::Create(0, &thread, &handle)); |
| |
| int acquired = 0; |
| for (int i = 0; i < 5; i++) { |
| lock.Acquire(); |
| acquired++; |
| lock.Release(); |
| } |
| for (int i = 0; i < 10; i++) { |
| lock.Acquire(); |
| acquired++; |
| PlatformThread::Sleep(RandTimeDeltaUpTo(Milliseconds(20))); |
| lock.Release(); |
| } |
| for (int i = 0; i < 10; i++) { |
| if (lock.Try()) { |
| acquired++; |
| PlatformThread::Sleep(RandTimeDeltaUpTo(Milliseconds(20))); |
| lock.Release(); |
| } |
| } |
| for (int i = 0; i < 5; i++) { |
| lock.Acquire(); |
| acquired++; |
| PlatformThread::Sleep(RandTimeDeltaUpTo(Milliseconds(20))); |
| lock.Release(); |
| } |
| |
| PlatformThread::Join(handle); |
| |
| EXPECT_GE(acquired, 20); |
| EXPECT_GE(thread.acquired(), 20); |
| } |
| |
| // Test that Try() works as expected ------------------------------------------- |
| |
| class TryLockTestThread : public PlatformThread::Delegate { |
| public: |
| explicit TryLockTestThread(Lock* lock) : lock_(lock) {} |
| |
| TryLockTestThread(const TryLockTestThread&) = delete; |
| TryLockTestThread& operator=(const TryLockTestThread&) = delete; |
| |
| void ThreadMain() override { |
| // The local variable is required for the static analyzer to see that the |
| // lock is properly released. |
| bool got_lock = lock_->Try(); |
| got_lock_ = got_lock; |
| if (got_lock) { |
| lock_->Release(); |
| } |
| } |
| |
| bool got_lock() const { return got_lock_; } |
| |
| private: |
| raw_ptr<Lock> lock_; |
| bool got_lock_ = false; |
| }; |
| |
| TEST(LockTest, TryLock) { |
| Lock lock; |
| |
| ASSERT_TRUE(lock.Try()); |
| lock.AssertAcquired(); |
| |
| // This thread will not be able to get the lock. |
| { |
| TryLockTestThread thread(&lock); |
| PlatformThreadHandle handle; |
| |
| ASSERT_TRUE(PlatformThread::Create(0, &thread, &handle)); |
| |
| PlatformThread::Join(handle); |
| |
| ASSERT_FALSE(thread.got_lock()); |
| } |
| |
| lock.Release(); |
| |
| // This thread will.... |
| { |
| TryLockTestThread thread(&lock); |
| PlatformThreadHandle handle; |
| |
| ASSERT_TRUE(PlatformThread::Create(0, &thread, &handle)); |
| |
| PlatformThread::Join(handle); |
| |
| ASSERT_TRUE(thread.got_lock()); |
| // But it released it.... |
| ASSERT_TRUE(lock.Try()); |
| lock.AssertAcquired(); |
| } |
| |
| lock.Release(); |
| } |
| |
| // Tests that locks actually exclude ------------------------------------------- |
| |
| class MutexLockTestThread : public PlatformThread::Delegate { |
| public: |
| MutexLockTestThread(Lock* lock, int* value) : lock_(lock), value_(value) {} |
| |
| MutexLockTestThread(const MutexLockTestThread&) = delete; |
| MutexLockTestThread& operator=(const MutexLockTestThread&) = delete; |
| |
| // Static helper which can also be called from the main thread. |
| static void DoStuff(Lock* lock, int* value) { |
| for (int i = 0; i < 40; i++) { |
| lock->Acquire(); |
| int v = *value; |
| PlatformThread::Sleep(RandTimeDeltaUpTo(Milliseconds(10))); |
| *value = v + 1; |
| lock->Release(); |
| } |
| } |
| |
| void ThreadMain() override { DoStuff(lock_, value_); } |
| |
| private: |
| raw_ptr<Lock> lock_; |
| raw_ptr<int> value_; |
| }; |
| |
| TEST(LockTest, MutexTwoThreads) { |
| Lock lock; |
| int value = 0; |
| |
| MutexLockTestThread thread(&lock, &value); |
| PlatformThreadHandle handle; |
| |
| ASSERT_TRUE(PlatformThread::Create(0, &thread, &handle)); |
| |
| MutexLockTestThread::DoStuff(&lock, &value); |
| |
| PlatformThread::Join(handle); |
| |
| EXPECT_EQ(2 * 40, value); |
| } |
| |
| TEST(LockTest, MutexFourThreads) { |
| Lock lock; |
| int value = 0; |
| |
| MutexLockTestThread thread1(&lock, &value); |
| MutexLockTestThread thread2(&lock, &value); |
| MutexLockTestThread thread3(&lock, &value); |
| PlatformThreadHandle handle1; |
| PlatformThreadHandle handle2; |
| PlatformThreadHandle handle3; |
| |
| ASSERT_TRUE(PlatformThread::Create(0, &thread1, &handle1)); |
| ASSERT_TRUE(PlatformThread::Create(0, &thread2, &handle2)); |
| ASSERT_TRUE(PlatformThread::Create(0, &thread3, &handle3)); |
| |
| MutexLockTestThread::DoStuff(&lock, &value); |
| |
| PlatformThread::Join(handle1); |
| PlatformThread::Join(handle2); |
| PlatformThread::Join(handle3); |
| |
| EXPECT_EQ(4 * 40, value); |
| } |
| |
| // Test invariant checking ----------------------------------------------------- |
| |
| TEST(LockTest, InvariantIsCalled) { |
| // This test should compile and execute safely regardless of invariant |
| // checking, but if `kInvariantsActive` is false, we don't expect the |
| // invariant to be checked when the lock state changes. |
| constexpr bool kInvariantsActive = DCHECK_IS_ON(); |
| |
| class InvariantChecker { |
| public: |
| explicit InvariantChecker(const Lock& lock LIFETIME_BOUND) : lock(lock) {} |
| void Check() ASSERT_EXCLUSIVE_LOCK(lock) { |
| lock->AssertAcquired(); |
| invariant_called = true; |
| } |
| bool TestAndReset() { return std::exchange(invariant_called, false); } |
| |
| private: |
| const raw_ref<const Lock> lock; |
| bool invariant_called = false; |
| }; |
| |
| // Awkward construction order here allows `checker` to refer to `lock`, which |
| // refers to `check_ref`, which refers to `check`, which refers to `checker`. |
| std::unique_ptr<InvariantChecker> checker; |
| auto check = [&] { checker->Check(); }; |
| auto check_ref = base::FunctionRef<void()>(check); |
| Lock lock([&](Lock* lp) { |
| checker = std::make_unique<InvariantChecker>(*lp); |
| return check_ref; |
| }(&lock)); |
| |
| EXPECT_FALSE(checker->TestAndReset()); |
| |
| lock.Acquire(); |
| EXPECT_EQ(kInvariantsActive, checker->TestAndReset()); |
| |
| lock.Release(); |
| EXPECT_EQ(kInvariantsActive, checker->TestAndReset()); |
| } |
| |
| // AutoLock tests -------------------------------------------------------------- |
| |
| TEST(LockTest, AutoLockMaybe) { |
| Lock lock; |
| { |
| AutoLockMaybe auto_lock(&lock); |
| lock.AssertAcquired(); |
| } |
| EXPECT_DCHECK_DEATH(lock.AssertAcquired()); |
| } |
| |
| TEST(LockTest, AutoLockMaybeNull) { |
| AutoLockMaybe auto_lock(nullptr); |
| } |
| |
| TEST(LockTest, ReleasableAutoLockExplicitRelease) { |
| Lock lock; |
| ReleasableAutoLock auto_lock(&lock); |
| lock.AssertAcquired(); |
| auto_lock.Release(); |
| EXPECT_DCHECK_DEATH(lock.AssertAcquired()); |
| } |
| |
| TEST(LockTest, ReleasableAutoLockImplicitRelease) { |
| Lock lock; |
| { |
| ReleasableAutoLock auto_lock(&lock); |
| lock.AssertAcquired(); |
| } |
| EXPECT_DCHECK_DEATH(lock.AssertAcquired()); |
| } |
| |
| class TryLockTest : public testing::Test { |
| protected: |
| Lock lock_; |
| int x_ GUARDED_BY(lock_) = 0; |
| }; |
| |
| // Verifies thread safety annotations do not prevent correct `AutoTryLock` usage |
| // from compiling. A dual of this test exists in lock_nocompile.nc. For more |
| // context, see <https://crbug.com/340196356>. |
| TEST_F(TryLockTest, CorrectlyCheckIsAcquired) { |
| AutoTryLock maybe(lock_); |
| // Should compile because we correctly check whether the lock is acquired |
| // before writing to `x_`. |
| if (maybe.is_acquired()) { |
| x_ = 5; |
| } |
| } |
| |
| #if DCHECK_IS_ON() |
| |
| TEST(LockTest, GetTrackedLocksHeldByCurrentThread) { |
| Lock lock_a; |
| Lock lock_b; |
| Lock lock_c; |
| const uintptr_t lock_a_ptr = reinterpret_cast<uintptr_t>(&lock_a); |
| const uintptr_t lock_b_ptr = reinterpret_cast<uintptr_t>(&lock_b); |
| const uintptr_t lock_c_ptr = reinterpret_cast<uintptr_t>(&lock_c); |
| |
| EXPECT_THAT(subtle::GetTrackedLocksHeldByCurrentThread(), |
| UnorderedElementsAre()); |
| ReleasableAutoLock auto_lock_a(&lock_a, subtle::LockTracking::kEnabled); |
| EXPECT_THAT(subtle::GetTrackedLocksHeldByCurrentThread(), |
| UnorderedElementsAre(lock_a_ptr)); |
| ReleasableAutoLock auto_lock_b(&lock_b, subtle::LockTracking::kEnabled); |
| EXPECT_THAT(subtle::GetTrackedLocksHeldByCurrentThread(), |
| UnorderedElementsAre(lock_a_ptr, lock_b_ptr)); |
| auto_lock_a.Release(); |
| EXPECT_THAT(subtle::GetTrackedLocksHeldByCurrentThread(), |
| UnorderedElementsAre(lock_b_ptr)); |
| ReleasableAutoLock auto_lock_c(&lock_c, subtle::LockTracking::kEnabled); |
| EXPECT_THAT(subtle::GetTrackedLocksHeldByCurrentThread(), |
| UnorderedElementsAre(lock_b_ptr, lock_c_ptr)); |
| auto_lock_c.Release(); |
| EXPECT_THAT(subtle::GetTrackedLocksHeldByCurrentThread(), |
| UnorderedElementsAre(lock_b_ptr)); |
| auto_lock_b.Release(); |
| EXPECT_THAT(subtle::GetTrackedLocksHeldByCurrentThread(), |
| UnorderedElementsAre()); |
| } |
| |
| TEST(LockTest, GetTrackedLocksHeldByCurrentThread_AutoLock) { |
| Lock lock; |
| const uintptr_t lock_ptr = reinterpret_cast<uintptr_t>(&lock); |
| AutoLock auto_lock(lock, subtle::LockTracking::kEnabled); |
| EXPECT_THAT(subtle::GetTrackedLocksHeldByCurrentThread(), |
| UnorderedElementsAre(lock_ptr)); |
| } |
| |
| TEST(LockTest, GetTrackedLocksHeldByCurrentThread_MovableAutoLock) { |
| Lock lock; |
| const uintptr_t lock_ptr = reinterpret_cast<uintptr_t>(&lock); |
| MovableAutoLock auto_lock(lock, subtle::LockTracking::kEnabled); |
| EXPECT_THAT(subtle::GetTrackedLocksHeldByCurrentThread(), |
| UnorderedElementsAre(lock_ptr)); |
| } |
| |
| TEST(LockTest, GetTrackedLocksHeldByCurrentThread_AutoTryLock) { |
| Lock lock; |
| const uintptr_t lock_ptr = reinterpret_cast<uintptr_t>(&lock); |
| AutoTryLock auto_lock(lock, subtle::LockTracking::kEnabled); |
| EXPECT_THAT(subtle::GetTrackedLocksHeldByCurrentThread(), |
| UnorderedElementsAre(lock_ptr)); |
| } |
| |
| TEST(LockTest, GetTrackedLocksHeldByCurrentThread_AutoLockMaybe) { |
| Lock lock; |
| const uintptr_t lock_ptr = reinterpret_cast<uintptr_t>(&lock); |
| AutoLockMaybe auto_lock(&lock, subtle::LockTracking::kEnabled); |
| EXPECT_THAT(subtle::GetTrackedLocksHeldByCurrentThread(), |
| UnorderedElementsAre(lock_ptr)); |
| } |
| |
| TEST(LockTest, GetTrackedLocksHeldByCurrentThreadOverCapacity) |
| // Thread-safety analysis doesn't handle the array of locks properly. |
| NO_THREAD_SAFETY_ANALYSIS { |
| constexpr size_t kHeldLocksCapacity = 10; |
| std::array<Lock, kHeldLocksCapacity + 1> locks; |
| |
| for (size_t i = 0; i < kHeldLocksCapacity; ++i) { |
| locks[i].Acquire(subtle::LockTracking::kEnabled); |
| } |
| |
| EXPECT_CHECK_DEATH({ |
| locks[kHeldLocksCapacity].Acquire(subtle::LockTracking::kEnabled); |
| locks[kHeldLocksCapacity].Release(); |
| }); |
| |
| for (size_t i = 0; i < kHeldLocksCapacity; ++i) { |
| locks[i].Release(); |
| |
| std::vector<uintptr_t> expected_locks; |
| for (size_t j = i + 1; j < kHeldLocksCapacity; ++j) { |
| expected_locks.push_back(reinterpret_cast<uintptr_t>(&locks[j])); |
| } |
| |
| EXPECT_THAT(subtle::GetTrackedLocksHeldByCurrentThread(), |
| UnorderedElementsAreArray(expected_locks)); |
| } |
| } |
| |
| TEST(LockTest, TrackingDisabled) { |
| Lock lock; |
| AutoLock auto_lock(lock, subtle::LockTracking::kDisabled); |
| EXPECT_TRUE(subtle::GetTrackedLocksHeldByCurrentThread().empty()); |
| } |
| |
| // Priority Inheritance Tests -------------------------------------------------- |
| |
| #if BUILDFLAG(ENABLE_MUTEX_PRIORITY_INHERITANCE) |
| namespace { |
| class PriorityInheritanceTest { |
| public: |
| // The average value of MeasureRunTime() over |num_samples| iterations. |
| static TimeDelta MeasureAverageRunTime(int num_samples = 10) { |
| TimeDelta total_runtime; |
| for (int i = 0; i < num_samples; i++) { |
| total_runtime += MeasureRunTime(); |
| } |
| |
| return total_runtime / num_samples; |
| } |
| |
| // Measure the time taken for a low-priority thread (kBackground) to perform |
| // CPU bound work when it holds a lock that is awaited by a high-priority |
| // thread (kRealtimeAudio). |
| static TimeDelta MeasureRunTime() { |
| Lock lock; |
| TimeDelta test_run_time; |
| std::atomic<bool> signal_cpu_bound_worker_threads_shutdown{false}, |
| signal_thread_a_will_lock{false}; |
| |
| // Keep all the cores busy with a workload of CPU bound thread to reduce |
| // flakiness in the test by skewing the CPU time between the high-priority |
| // and low-priority measurement threads. |
| std::vector<TestThread> cpu_bound_worker_threads; |
| for (int i = 0; i < 15; i++) { |
| cpu_bound_worker_threads.emplace_back( |
| ThreadType::kDefault, base::BindLambdaForTesting([&]() { |
| while (!signal_cpu_bound_worker_threads_shutdown.load( |
| std::memory_order_relaxed)) { |
| BusyLoop(10); |
| } |
| })); |
| } |
| |
| for (auto& worker_thread : cpu_bound_worker_threads) { |
| worker_thread.Create(); |
| } |
| |
| TestThread thread_a( |
| ThreadType::kRealtimeAudio, base::BindLambdaForTesting([&]() { |
| // Signal to thread B that the current thread will acquire the lock |
| // next, so that it can to start its CPU bound work. |
| signal_thread_a_will_lock.store(true, std::memory_order_relaxed); |
| |
| // Wait on the lock to be released once the low-priority thread is |
| // done. In the case when priority inheritance mutexes are enabled, |
| // this should boost the priority of the low-priority thread to the |
| // priority of the highest priority waiter (i.e. the current thread). |
| AutoLock auto_lock(lock); |
| BusyLoop(10); |
| })); |
| |
| TestThread thread_b( |
| ThreadType::kBackground, base::BindLambdaForTesting([&]() { |
| // Acquire the lock before creating the high-priority thread, so that |
| // the higher priority thread is blocked on the current thread while |
| // the current thread performs CPU-bound work. |
| AutoLock auto_lock(lock); |
| thread_a.Create(); |
| |
| // Before performing the CPU bound work, wait for the thread A to |
| // signal that it has started running and will acquire the lock next. |
| // While it is not a perfectly reliable signal (thread A may get |
| // descheduled immediately after signalling), given the relative |
| // priorities of the two threads it is good enough to reduce large |
| // variations due to latencies in thread bring up. |
| while (!signal_thread_a_will_lock.load(std::memory_order_relaxed)) { |
| usleep(10); |
| } |
| |
| ElapsedTimer timer; |
| BusyLoop(1000000); |
| test_run_time = timer.Elapsed(); |
| })); |
| |
| // Create the low-priority thread which is responsible for creating the |
| // high-priority thread. Wait for both threads to finish before recording |
| // the elapsed time. |
| thread_b.Create(); |
| thread_b.Join(); |
| thread_a.Join(); |
| |
| signal_cpu_bound_worker_threads_shutdown.store(true, |
| std::memory_order_relaxed); |
| for (auto& worker_thread : cpu_bound_worker_threads) { |
| worker_thread.Join(); |
| } |
| |
| return test_run_time; |
| } |
| |
| private: |
| // CPU bound work for the threads to eat up CPU cycles. |
| static void BusyLoop(size_t n) { |
| __unused int sum = 0; |
| for (int i = 0; i < n; i++) { |
| if (base::ShouldRecordSubsampledMetric(0.5)) { |
| sum += 1; |
| } |
| } |
| } |
| |
| class TestThread : public PlatformThread::Delegate { |
| public: |
| explicit TestThread(ThreadType thread_type, base::OnceClosure body) |
| : thread_type_(thread_type), body_(std::move(body)) {} |
| |
| void Create() { |
| ASSERT_TRUE( |
| PlatformThread::CreateWithType(0, this, &handle_, thread_type_)); |
| } |
| |
| void ThreadMain() override { std::move(body_).Run(); } |
| |
| void Join() { PlatformThread::Join(handle_); } |
| |
| private: |
| ThreadType thread_type_; |
| PlatformThreadHandle handle_; |
| base::OnceClosure body_; |
| }; |
| }; |
| |
| } // namespace |
| |
| // Tests that the time taken by a higher-priority thread to acquire a lock held |
| // by a lower-priority thread is indeed reduced by priority inheritance. |
| TEST(LockTest, PriorityIsInherited) { |
| TimeDelta avg_test_run_time_with_pi, avg_test_run_time_without_pi; |
| |
| // Priority inheritance mutexes are not supported on Android kernels < 6.1 |
| if (!base::KernelSupportsPriorityInheritanceFutex()) { |
| GTEST_SKIP() << "base::Lock does not handle multiple thread priorities " |
| << "(Kernel version: " |
| << base::SysInfo::KernelVersionNumber::Current() << ")"; |
| } |
| |
| { |
| base::android::ScopedUsePriorityInheritanceLocksForTesting use_pi_locks; |
| ASSERT_TRUE(base::android::BackgroundThreadPoolFieldTrial:: |
| ShouldUsePriorityInheritanceLocks()); |
| avg_test_run_time_with_pi = |
| PriorityInheritanceTest::MeasureAverageRunTime(); |
| } |
| |
| { |
| ASSERT_TRUE(!base::android::BackgroundThreadPoolFieldTrial:: |
| ShouldUsePriorityInheritanceLocks()); |
| avg_test_run_time_without_pi = |
| PriorityInheritanceTest::MeasureAverageRunTime(); |
| } |
| |
| // During the time in which the thread A is waiting on the lock to be released |
| // by the thread B, the thread B runs at kBackground priority in the non-PI |
| // case and at kRealtimeAudio priority in the PI case. |
| // |
| // Based on the Linux kernel's allocation of CPU shares documented in |
| // https://elixir.bootlin.com/linux/v6.12.5/source/kernel/sched/core.c#L9998, |
| // a thread running at kRealtimeAudio (nice value = -16) gets 36291 shares |
| // of the CPU, a thread at kDefault (nice value = 0) get 1024 shares and a |
| // thread at kBackground (nice value = 10) gets 110 shares of the CPU. |
| // |
| // Assuming no other threads except the ones created by this test are running, |
| // during the time in which thread A is waiting on the lock to be released by |
| // thread B, thread B gets 110/(15*1024 + 110) ≈ 0.7% of the CPU time in the |
| // non-PI case and 36291/(36291 + 15*1024) ≈ 70% of the CPU time in the PI |
| // case. This is approximately a 100x difference in CPU shares allocated to |
| // the thread B when it is doing CPU-bound work. |
| // |
| // The test is thus designed such that the measured run time is thread B's CPU |
| // bound work. While there are other factors at play that determine the |
| // measured run time such as the frequency at which the CPU is running, we can |
| // expect that there will be at least an order of magnitude of disparity in |
| // the test run times with and without PI. |
| // |
| // In order to reduce test flakiness while still eliminating the possibility |
| // of variance in measurements accounting for the test results, we |
| // conservatively expect a 3x improvement. |
| EXPECT_GT(avg_test_run_time_without_pi, 3 * avg_test_run_time_with_pi); |
| } |
| #endif // BUILDFLAG(ENABLE_MUTEX_PRIORITY_INHERITANCE) |
| |
| #endif // DCHECK_IS_ON() |
| |
| } // namespace base |