130 lines
3.7 KiB
C++
130 lines
3.7 KiB
C++
// Copyright The OpenTelemetry Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
#pragma once
|
|
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <thread>
|
|
|
|
#include "opentelemetry/version.h"
|
|
|
|
#if defined(_MSC_VER)
|
|
# ifndef NOMINMAX
|
|
# define NOMINMAX
|
|
# endif
|
|
# include <windows.h>
|
|
#elif defined(__i386__) || defined(__x86_64__)
|
|
# if defined(__clang__)
|
|
# include <emmintrin.h>
|
|
# endif
|
|
#endif
|
|
|
|
OPENTELEMETRY_BEGIN_NAMESPACE
|
|
namespace common
|
|
{
|
|
|
|
constexpr int SPINLOCK_FAST_ITERATIONS = 100;
|
|
constexpr int SPINLOCK_SLEEP_MS = 1;
|
|
|
|
/**
|
|
* A Mutex which uses atomic flags and spin-locks instead of halting threads.
|
|
*
|
|
* This mutex uses an incremental back-off strategy with the following phases:
|
|
* 1. A tight spin-lock loop (pending: using hardware PAUSE/YIELD instructions)
|
|
* 2. A loop where the current thread yields control after checking the lock.
|
|
* 3. Issuing a thread-sleep call before starting back in phase 1.
|
|
*
|
|
* This is meant to give a good balance of perofrmance and CPU consumption in
|
|
* practice.
|
|
*
|
|
* This mutex uses an incremental back-off strategy with the following phases:
|
|
* 1. A tight spin-lock loop (pending: using hardware PAUSE/YIELD instructions)
|
|
* 2. A loop where the current thread yields control after checking the lock.
|
|
* 3. Issuing a thread-sleep call before starting back in phase 1.
|
|
*
|
|
* This is meant to give a good balance of perofrmance and CPU consumption in
|
|
* practice.
|
|
*
|
|
* This class implements the `BasicLockable` specification:
|
|
* https://en.cppreference.com/w/cpp/named_req/BasicLockable
|
|
*/
|
|
class SpinLockMutex
|
|
{
|
|
public:
|
|
SpinLockMutex() noexcept {}
|
|
~SpinLockMutex() noexcept = default;
|
|
SpinLockMutex(const SpinLockMutex &) = delete;
|
|
SpinLockMutex &operator=(const SpinLockMutex &) = delete;
|
|
SpinLockMutex &operator=(const SpinLockMutex &) volatile = delete;
|
|
|
|
/**
|
|
* Attempts to lock the mutex. Return immediately with `true` (success) or `false` (failure).
|
|
*/
|
|
bool try_lock() noexcept
|
|
{
|
|
return !flag_.load(std::memory_order_relaxed) &&
|
|
!flag_.exchange(true, std::memory_order_acquire);
|
|
}
|
|
|
|
/**
|
|
* Blocks until a lock can be obtained for the current thread.
|
|
*
|
|
* This mutex will spin the current CPU waiting for the lock to be available. This can have
|
|
* decent performance in scenarios where there is low lock contention and lock-holders achieve
|
|
* their work quickly. It degrades in scenarios where locked tasks take a long time.
|
|
*/
|
|
void lock() noexcept
|
|
{
|
|
for (;;)
|
|
{
|
|
// Try once
|
|
if (!flag_.exchange(true, std::memory_order_acquire))
|
|
{
|
|
return;
|
|
}
|
|
// Spin-Fast (goal ~10ns)
|
|
for (std::size_t i = 0; i < SPINLOCK_FAST_ITERATIONS; ++i)
|
|
{
|
|
if (try_lock())
|
|
{
|
|
return;
|
|
}
|
|
// Issue a Pause/Yield instruction while spinning.
|
|
#if defined(_MSC_VER)
|
|
YieldProcessor();
|
|
#elif defined(__i386__) || defined(__x86_64__)
|
|
# if defined(__clang__)
|
|
_mm_pause();
|
|
# else
|
|
__builtin_ia32_pause();
|
|
# endif
|
|
#elif defined(__arm__)
|
|
// This intrinsic should fail to be found if YIELD is not supported on the current
|
|
// processor.
|
|
__yield();
|
|
#else
|
|
// TODO: Issue PAGE/YIELD on other architectures.
|
|
#endif
|
|
}
|
|
// Yield then try again (goal ~100ns)
|
|
std::this_thread::yield();
|
|
if (try_lock())
|
|
{
|
|
return;
|
|
}
|
|
// Sleep and then start the whole process again. (goal ~1000ns)
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(SPINLOCK_SLEEP_MS));
|
|
}
|
|
return;
|
|
}
|
|
/** Releases the lock held by the execution agent. Throws no exceptions. */
|
|
void unlock() noexcept { flag_.store(false, std::memory_order_release); }
|
|
|
|
private:
|
|
std::atomic<bool> flag_{false};
|
|
};
|
|
|
|
} // namespace common
|
|
OPENTELEMETRY_END_NAMESPACE
|