spin_lock_mutex.h 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. // Copyright The OpenTelemetry Authors
  2. // SPDX-License-Identifier: Apache-2.0
  3. #pragma once
  4. #include <atomic>
  5. #include <chrono>
  6. #include <thread>
  7. #include "opentelemetry/version.h"
  8. #if defined(_MSC_VER)
  9. # define _WINSOCKAPI_ // stops including winsock.h
  10. # include <windows.h>
  11. #elif defined(__i386__) || defined(__x86_64__)
  12. # if defined(__clang__)
  13. # include <emmintrin.h>
  14. # elif defined(__INTEL_COMPILER)
  15. # include <immintrin.h>
  16. # endif
  17. #endif
  18. OPENTELEMETRY_BEGIN_NAMESPACE
  19. namespace common
  20. {
  21. constexpr int SPINLOCK_FAST_ITERATIONS = 100;
  22. constexpr int SPINLOCK_SLEEP_MS = 1;
  23. /**
  24. * A Mutex which uses atomic flags and spin-locks instead of halting threads.
  25. *
  26. * This mutex uses an incremental back-off strategy with the following phases:
  27. * 1. A tight spin-lock loop (pending: using hardware PAUSE/YIELD instructions)
  28. * 2. A loop where the current thread yields control after checking the lock.
  29. * 3. Issuing a thread-sleep call before starting back in phase 1.
  30. *
  31. * This is meant to give a good balance of perofrmance and CPU consumption in
  32. * practice.
  33. *
  34. * This mutex uses an incremental back-off strategy with the following phases:
  35. * 1. A tight spin-lock loop (pending: using hardware PAUSE/YIELD instructions)
  36. * 2. A loop where the current thread yields control after checking the lock.
  37. * 3. Issuing a thread-sleep call before starting back in phase 1.
  38. *
  39. * This is meant to give a good balance of perofrmance and CPU consumption in
  40. * practice.
  41. *
  42. * This class implements the `BasicLockable` specification:
  43. * https://en.cppreference.com/w/cpp/named_req/BasicLockable
  44. */
  45. class SpinLockMutex
  46. {
  47. public:
  48. SpinLockMutex() noexcept {}
  49. ~SpinLockMutex() noexcept = default;
  50. SpinLockMutex(const SpinLockMutex &) = delete;
  51. SpinLockMutex &operator=(const SpinLockMutex &) = delete;
  52. static inline void fast_yield() noexcept
  53. {
  54. // Issue a Pause/Yield instruction while spinning.
  55. #if defined(_MSC_VER)
  56. YieldProcessor();
  57. #elif defined(__i386__) || defined(__x86_64__)
  58. # if defined(__clang__) || defined(__INTEL_COMPILER)
  59. _mm_pause();
  60. # else
  61. __builtin_ia32_pause();
  62. # endif
  63. #elif defined(__armel__) || defined(__ARMEL__)
  64. asm volatile("nop" ::: "memory");
  65. #elif defined(__arm__) || defined(__aarch64__) // arm big endian / arm64
  66. __asm__ __volatile__("yield" ::: "memory");
  67. #else
  68. // TODO: Issue PAGE/YIELD on other architectures.
  69. #endif
  70. }
  71. /**
  72. * Attempts to lock the mutex. Return immediately with `true` (success) or `false` (failure).
  73. */
  74. bool try_lock() noexcept
  75. {
  76. return !flag_.load(std::memory_order_relaxed) &&
  77. !flag_.exchange(true, std::memory_order_acquire);
  78. }
  79. /**
  80. * Blocks until a lock can be obtained for the current thread.
  81. *
  82. * This mutex will spin the current CPU waiting for the lock to be available. This can have
  83. * decent performance in scenarios where there is low lock contention and lock-holders achieve
  84. * their work quickly. It degrades in scenarios where locked tasks take a long time.
  85. */
  86. void lock() noexcept
  87. {
  88. for (;;)
  89. {
  90. // Try once
  91. if (!flag_.exchange(true, std::memory_order_acquire))
  92. {
  93. return;
  94. }
  95. // Spin-Fast (goal ~10ns)
  96. for (std::size_t i = 0; i < SPINLOCK_FAST_ITERATIONS; ++i)
  97. {
  98. if (try_lock())
  99. {
  100. return;
  101. }
  102. fast_yield();
  103. }
  104. // Yield then try again (goal ~100ns)
  105. std::this_thread::yield();
  106. if (try_lock())
  107. {
  108. return;
  109. }
  110. // Sleep and then start the whole process again. (goal ~1000ns)
  111. std::this_thread::sleep_for(std::chrono::milliseconds(SPINLOCK_SLEEP_MS));
  112. }
  113. return;
  114. }
  115. /** Releases the lock held by the execution agent. Throws no exceptions. */
  116. void unlock() noexcept { flag_.store(false, std::memory_order_release); }
  117. private:
  118. std::atomic<bool> flag_{false};
  119. };
  120. } // namespace common
  121. OPENTELEMETRY_END_NAMESPACE