[英]Creating a thread safe atomic counter
I have a specific requirement in one of the my projects, that is keeping "count" of certain operations and eventually "reading" + "resetting" these counters periodically (eg. 24 hours).我在我的一个项目中有一个特定要求,即保持某些操作的“计数”,并最终定期“读取”+“重置”这些计数器(例如 24 小时)。
Operation will be:操作将是:
The platform I am interested in is Windows, but if this can be cross platform even better.我感兴趣的平台是 Windows,但如果它可以跨平台更好。 I'm using Visual Studio and target Windows architecture is x64 only .
我正在使用 Visual Studio,目标 Windows 体系结构仅为 x64 。
I am uncertain if the result is "ok" and if my implementation is correct.我不确定结果是否“确定”以及我的实施是否正确。 Frankly, never used much std wrappers and my c++ knowledge is quite limited.
坦率地说,从来没有使用过很多 std 包装器,而且我的 C++ 知识非常有限。
Result is:结果是:
12272 Current: 2
12272 After: 0
12272 Current: 18
12272 After: 0
12272 Current: 20
12272 After: 0
12272 Current: 20
12272 After: 0
12272 Current: 20
12272 After: 0
Below is a fully copy/paste reproducible example:以下是一个完全复制/粘贴可重现的示例:
#include <iostream>
#include <chrono>
#include <thread>
#include <Windows.h>
class ThreadSafeCounter final
{
private:
std::atomic_uint m_Counter1;
std::atomic_uint m_Counter2;
std::atomic_uint m_Counter3;
public:
ThreadSafeCounter(const ThreadSafeCounter&) = delete;
ThreadSafeCounter(ThreadSafeCounter&&) = delete;
ThreadSafeCounter& operator = (const ThreadSafeCounter&) = delete;
ThreadSafeCounter& operator = (ThreadSafeCounter&&) = delete;
ThreadSafeCounter() : m_Counter1(0), m_Counter2(0), m_Counter3(0) {}
~ThreadSafeCounter() = default;
std::uint32_t IncCounter1() noexcept
{
m_Counter1.fetch_add(1, std::memory_order_relaxed) + 1;
return m_Counter1;
}
std::uint32_t DecCounter1() noexcept
{
m_Counter1.fetch_sub(1, std::memory_order_relaxed) - 1;
return m_Counter1;
}
VOID ClearCounter1() noexcept
{
m_Counter1.exchange(0);
}
};
int main()
{
static ThreadSafeCounter Threads;
auto Thread1 = []() {
while (true)
{
auto test = Threads.IncCounter1();
std::cout << std::this_thread::get_id() << " Threads.IncCounter1() -> " << test << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
}
};
auto Thread2 = []() {
while (true)
{
auto test = Threads.DecCounter1();
std::cout << std::this_thread::get_id() << " Threads.DecCounter1() -> " << test << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
}
};
auto Thread3 = []() {
while (true)
{
Threads.ClearCounter1();
std::cout << std::this_thread::get_id() << " Threads.ClearCounter1()" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
}
};
std::thread th1(Thread1);
std::thread th2(Thread2);
std::thread th3(Thread3);
th1.join();
th2.join();
th3.join();
}
I should mention that in my real life project there is no usage of std::thread wrapper, and the threads are created using WinApi functions like CreateThread.我应该提到,在我的现实生活项目中,没有使用 std::thread 包装器,并且线程是使用 WinApi 函数(如 CreateThread)创建的。 The above is just to simulate/test the code.
以上只是模拟/测试代码。
Please point out to me what is wrong with the above code, what could be improved and if I'm on the right direction at all.请向我指出上述代码有什么问题,可以改进的地方以及我的方向是否正确。
Thank you!谢谢!
This looks suspicious:这看起来很可疑:
std::uint32_t IncCounter1() noexcept
{
m_Counter1.fetch_add(1, std::memory_order_relaxed) + 1;
return m_Counter1;
}
The + 1
is at the end of that line is effectively a no-op since the code does not assign the result of that expression to anything.该行末尾的
+ 1
实际上是一个空操作,因为代码没有将该表达式的结果分配给任何东西。 Further, you create a race condition by referencing m_Counter1
again on the second line - as there could have easily been a context switch to another thread changing it's value between performing the fetch_add
and then referencing the value again for the return result.此外,您通过在第二行再次引用
m_Counter1
创建竞争条件 - 因为在执行fetch_add
和再次引用该值以返回结果之间很容易发生上下文切换到另一个线程改变它的值。
I think you want this instead:我认为你想要这个:
std::uint32_t IncCounter1() noexcept
{
return m_Counter1.fetch_add(1, std::memory_order_relaxed) + 1;
}
fetch_add
will return the previously held value before the increment. fetch_add
将在增量之前返回先前保存的值。 So returning that +1
will be the current value of the counter at that moment.因此,返回
+1
将是当时计数器的当前值。
Same issue in DecCounter1. DecCounter1 中的相同问题。 Change that function's implementation to be this:
将该函数的实现更改为:
std::uint32_t DecCounter1() noexcept
{
return m_Counter1.fetch_sub(1, std::memory_order_relaxed) - 1;
}
I think there might be an issue depending on how these counters are used.我认为可能存在问题,具体取决于这些计数器的使用方式。 If a lot of
DecCounter1
and a ClearCounter1
(or more) is called around the same time, then it might happen that ClearCounter1
sets the counter to 0
, then a lot of DefCounter1
is executed (before the lock was preventing them to do so), and the counter ends up being negative.如果大量
DecCounter1
和一个ClearCounter1
(或更多)被大约同时调用,那么可能会发生ClearCounter1
将计数器设置为0
,然后执行大量DefCounter1
(在锁阻止它们这样做之前),并且计数器最终为负。 This can be a problem when:在以下情况下,这可能是一个问题:
If any of the above is true, then the situation is much harder and I haven't though about it yet.如果以上任何一条是真的,那么情况就困难得多,我还没有考虑过。 For a simple, statistic like counters I think the code can be improved: the atomics and the locks are doing almost the same and by locking you might degrade the performance without gaining anything (all of the above problems are true regarding whether your are using locks, atomics or both).
对于像计数器这样的简单统计数据,我认为可以改进代码:原子和锁的作用几乎相同,通过锁定,您可能会降低性能而不会获得任何好处(关于您是否使用锁,上述所有问题都是正确的,原子或两者)。 Therefore I would just get rid of the locks and use purely atomics.
因此,我会摆脱锁并使用纯粹的原子。 Here is my proposal for an improved version in case of none of the above mentioned problems apply :
如果上述问题都不适用,这是我对改进版本的建议:
#include <iostream>
#include <chrono>
#include <thread>
class ThreadSafeCounter final
{
private:
std::atomic_uint32_t m_Counter1;
std::atomic_uint32_t m_Counter2;
std::atomic_uint32_t m_Counter3;
public:
ThreadSafeCounter(const ThreadSafeCounter&) = delete;
ThreadSafeCounter(ThreadSafeCounter&&) = delete;
ThreadSafeCounter& operator = (const ThreadSafeCounter&) = delete;
ThreadSafeCounter& operator = (ThreadSafeCounter&&) = delete;
ThreadSafeCounter() : m_Counter1(0), m_Counter2(0), m_Counter3(0) {}
~ThreadSafeCounter() = default;
void IncCounter1() noexcept
{
m_Counter1.fetch_add(1, std::memory_order_relaxed);
}
void DecCounter1() noexcept
{
m_Counter1.fetch_sub(1, std::memory_order_relaxed);
}
std::uint32_t GetTotalCounter1()
{
return m_Counter1.load();
}
std::uint32_t GetAndClearCounter1()
{
return m_Counter1.exchange(0);
}
};
int main()
{
static ThreadSafeCounter Threads;
auto WorkerThread1 = []() {
while (true)
{
Threads.IncCounter1();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
};
auto WorkerThread2 = []() {
while (true)
{
Threads.DecCounter1();
std::this_thread::sleep_for(std::chrono::milliseconds(20));
}
};
auto FinalThread = []() {
while (true)
{
auto Current = Threads.GetTotalCounter1();
std::cout << std::this_thread::get_id() << " Current: " << Current << std::endl;
const auto Before = Threads.GetAndClearCounter1();
std::cout << std::this_thread::get_id() << " Before: " << Before << std::endl;
auto After = Threads.GetTotalCounter1();
std::cout << std::this_thread::get_id() << " After: " << After << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
};
std::thread th1(WorkerThread1);
std::thread th2(WorkerThread2);
std::thread th3(FinalThread);
th1.join();
th2.join();
th3.join();
}
The changes I made:我所做的改变:
m_Counter1.fetch_sub(0)
by m_Counter1.load()
because it does the same thing, but the load operation is obvious.m_Counter1.fetch_sub(0)
由m_Counter1.load()
因为它做同样的事情,但负荷运转是显而易见的。atomic_uint
type by atomic_uint32_t
just to match the type of the return value of GetTotalCounter1
.atomic_uint
替换atomic_uint
类型只是为了匹配atomic_uint32_t
的返回值的GetTotalCounter1
。GetAndClearCounter1
to guarantee that there is no other operation is happening between reading and clearing the value.GetAndClearCounter1
以保证在读取和清除值之间没有发生其他操作。+ 1
and - 1
in IncCounter1
and DecCounter1
.+ 1
和- 1
在IncCounter1
和DecCounter1
。VOID
return types to void
, because in my opinion void
is more idiomatic.VOID
返回类型更改为void
,因为在我看来void
更惯用。 As a final verdict, I think reading over the documentation of std::atomic
can be really helpful.作为最终结论,我认为阅读
std::atomic
的文档真的很有帮助。 It isn't the shortest docs, but definitely detailed enough to get familiar with operations, memory order etc.它不是最短的文档,但绝对足够详细以熟悉操作、内存顺序等。
Why are you writing a ThreadSafeCounter
class at all?为什么要编写
ThreadSafeCounter
类?
std::atomic<size_t>
is a ThreadSafeCounter. std::atomic<size_t>
是一个 ThreadSafeCounter。 That's the whole point of std::atomic.这就是 std::atomic 的全部意义所在。 So you should use it instead.
所以你应该改用它。 No need for another class.
不需要再上课了。 Most atomics have operator++/operator-- specializations, so your main loop could easily be rewritten like this:
大多数原子都有 operator++/operator-- 特化,所以你的主循环可以很容易地重写如下:
static std::atomic_int ThreadCounter1(0);
auto Thread1 = []() {
while (true)
{
auto test = ++ThreadCounter1; // or ThreadCounter1++, whatever you need
std::cout << std::this_thread::get_id() << " Threads.IncCounter1() -> " << test << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
}
};
auto Thread2 = []() {
while (true)
{
auto test = --ThreadCounter1;
std::cout << std::this_thread::get_id() << " Threads.DecCounter1() -> " << test << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
}
};
auto Thread3 = []() {
while (true)
{
/* Note: You could simply "ThreadCounter1 = 0" assign it here.
But exchange not only assigns a new value, it returns the previous value.
*/
auto ValueAtReset=ThreadCounter1.exchange(0);
std::cout << std::this_thread::get_id() << " Threads.ClearCounter1() called at value" << ValueAtReset << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
}
};
I forgot to mention a problem with your DecCounter operation.我忘了提及您的 DecCounter 操作的问题。 You are using atomic_uint, which cannot handle negative numbers.
您正在使用 atomic_uint,它无法处理负数。 But there is no guarantee, that your Thread2 will not run (aka decrement the counter) before Thread1.
但是不能保证您的 Thread2 不会在 Thread1 之前运行(也就是递减计数器)。 Which means that your counter will wrap.
这意味着您的计数器将换行。
So you could/should use std::atomic<int>
instead.所以你可以/应该使用
std::atomic<int>
代替。 That will give you the correct number of (calls_Thread1 - calls_Thread2).这将为您提供正确数量的 (calls_Thread1 - call_Thread2)。 The number will get negative if Thead2 has decremented the value more often than Thread1.
如果 Thead2 比 Thread1 更频繁地减少值,则该数字将变为负数。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.