diff --git a/doc/cn/policies.md b/doc/cn/policies.md index 34a5df4..0d5a5dc 100644 --- a/doc/cn/policies.md +++ b/doc/cn/policies.md @@ -184,7 +184,7 @@ struct MultipleThreading 对于 `SingleThreading` 而言,所有的 `Mutex` 、 `Atomic` 和 `ConditionVariable` 类型都是不会起任何作用的假类型。 -对于多线程而言,默认的 `Mutex` 是 `std::mutex` 。 eventpp 也提供了一个使用自旋锁作为互斥量的 `SpinLock` 类。 +对于多线程而言,默认的 `Mutex` 是 `std::mutex` 。 eventpp 也提供了一个使用自旋锁作为互斥量的 `SpinLock` 类。 `SpinLock` 提供 `lock` 、 `try_lock` 和 `unlock` ,因此可以与 `std::lock_guard` 和 `std::unique_lock` 一起使用。等待的线程在自旋一段时间后会让出 CPU ,所以在锁被长时间持有时不会空耗 CPU 。 当只有较少的线程时(和 CPU 核心数差不多的线程数),`eventpp::SpinLock` 的性能比 `std::mutex` 更高一些。当线程数超过 CPU 核心数时, `eventpp::SpinLock` 的性能弱于 `std::mutex` 。 diff --git a/doc/policies.md b/doc/policies.md index b0a0a19..6263aa1 100644 --- a/doc/policies.md +++ b/doc/policies.md @@ -168,7 +168,7 @@ struct MultipleThreading ``` For `SingleThreading`, all the types `Mutex`, `Atomic`, and `ConditionVariable` are dummy types that don't do anything. -For multiple threading, the default `Mutex` is `std::mutex`. `eventpp` also provides a `SpinLock` class which uses spinlock as the mutex. +For multiple threading, the default `Mutex` is `std::mutex`. `eventpp` also provides a `SpinLock` class which uses spinlock as the mutex. `SpinLock` has `lock`, `try_lock` and `unlock`, so it can be used with `std::lock_guard` and `std::unique_lock`. The waiting thread yields after spinning for a while, so it doesn't burn the CPU when the lock is held for long. When there are fewer threads (about around the number of CPU cores), `eventpp::SpinLock` has better performance than `std::mutex`. When there are much more threads than CPU cores, `eventpp::SpinLock` has worse performance than `std::mutex`. Please [read the benchmark](benchmark.md) for benchmark data. diff --git a/include/eventpp/eventpolicies.h b/include/eventpp/eventpolicies.h index 886ac10..83490e7 100644 --- a/include/eventpp/eventpolicies.h +++ b/include/eventpp/eventpolicies.h @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -34,20 +35,36 @@ struct TagHeterCallbackList : public TagHeter {}; struct TagHeterEventDispatcher : public TagHeter {}; struct TagHeterEventQueue : public TagHeter {}; +// A simple spin lock. The waiting thread spins on a plain load (test and test-and-set), so it doesn't keep +// invalidating the cache line of the lock, and it yields after spinning for a while, so it doesn't burn +// the CPU when the lock is held for long or when there are more threads than CPU cores. struct SpinLock { public: void lock() { - while(locked.test_and_set(std::memory_order_acquire)) { + while(locked.exchange(true, std::memory_order_acquire)) { + int spinCount = 0; + while(locked.load(std::memory_order_relaxed)) { + if(++spinCount >= spinCountBeforeYield) { + spinCount = 0; + std::this_thread::yield(); + } + } } } + bool try_lock() { + return ! locked.exchange(true, std::memory_order_acquire); + } + void unlock() { - locked.clear(std::memory_order_release); + locked.store(false, std::memory_order_release); } - + private: - std::atomic_flag locked = ATOMIC_FLAG_INIT; + enum { spinCountBeforeYield = 100 }; + + std::atomic locked { false }; }; template < diff --git a/tests/unittest/CMakeLists.txt b/tests/unittest/CMakeLists.txt index e9f8598..3566364 100644 --- a/tests/unittest/CMakeLists.txt +++ b/tests/unittest/CMakeLists.txt @@ -28,6 +28,7 @@ set(SRC_TEST test_conditionalfunctor.cpp test_anyid.cpp test_anydata.cpp + test_spinlock.cpp ) add_executable( diff --git a/tests/unittest/test_spinlock.cpp b/tests/unittest/test_spinlock.cpp new file mode 100644 index 0000000..8bd1637 --- /dev/null +++ b/tests/unittest/test_spinlock.cpp @@ -0,0 +1,96 @@ +// eventpp library +// Copyright (C) 2018 Wang Qi (wqking) +// Github: https://github.com/wqking/eventpp +// Licensed 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. + +#include "test.h" +#include "eventpp/eventpolicies.h" +#include "eventpp/callbacklist.h" + +#include +#include +#include + +TEST_CASE("SpinLock, lock, try_lock, unlock") +{ + eventpp::SpinLock spinLock; + + REQUIRE(spinLock.try_lock()); + REQUIRE(! spinLock.try_lock()); + spinLock.unlock(); + REQUIRE(spinLock.try_lock()); + spinLock.unlock(); + + spinLock.lock(); + REQUIRE(! spinLock.try_lock()); + spinLock.unlock(); + + { + std::lock_guard lockGuard(spinLock); + REQUIRE(! spinLock.try_lock()); + } + REQUIRE(spinLock.try_lock()); + spinLock.unlock(); +} + +TEST_CASE("SpinLock, multi threading") +{ + eventpp::SpinLock spinLock; + + // more threads than cores to exercise the yield + constexpr int threadCount = 64; + constexpr int incrementCountPerThread = 1024 * 16; + + int counter = 0; + + std::vector threadList; + for(int i = 0; i < threadCount; ++i) { + threadList.emplace_back([&spinLock, &counter, incrementCountPerThread]() { + for(int k = 0; k < incrementCountPerThread; ++k) { + std::lock_guard lockGuard(spinLock); + ++counter; + } + }); + } + for(int i = 0; i < threadCount; ++i) { + threadList[i].join(); + } + + REQUIRE(counter == threadCount * incrementCountPerThread); +} + +TEST_CASE("CallbackList, SpinLock, multi threading") +{ + using CL = eventpp::CallbackList >; + CL callbackList; + + constexpr int threadCount = 16; + constexpr int callbackCountPerThread = 1024; + + std::vector threadList; + for(int i = 0; i < threadCount; ++i) { + threadList.emplace_back([&callbackList, callbackCountPerThread]() { + for(int k = 0; k < callbackCountPerThread; ++k) { + callbackList.append([](int) {}); + } + }); + } + for(int i = 0; i < threadCount; ++i) { + threadList[i].join(); + } + + int count = 0; + callbackList.forEach([&count](const CL::Callback &) { + ++count; + }); + REQUIRE(count == threadCount * callbackCountPerThread); +} +