简体   繁体   English

创建线程安全的原子计数器

[英]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:操作将是:

  • Worker threads -> increment counters (randomly)工作线程 -> 增加计数器(随机)
  • Timer thread (eg. 24 hours) -> read count -> do something -> reset counters计时器线程(例如 24 小时)-> 读取计数-> 做某事-> 重置计数器

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:在以下情况下,这可能是一个问题:

  1. The counters are used for any kind of resource management (releasing memory/files/etc based on the value of counters)计数器用于任何类型的资源管理(根据计数器的值释放内存/文件/等)
  2. The counters are used only for some statistics, but the value of the counters are not proportional to the number of threads.计数器仅用于某些统计,但计数器的值与线程数不成正比。 Eg: if 8 event happens during the day that you want to count, but there are more than 8 threads, then in a very unlucky situation (when the events are happening around the same time as resetting the counter) you might end up a wrong starting value for the next period which makes the statistic inaccurate.例如:如果在您要计算的当天发生了 8 个事件,但线程数超过 8 个,那么在非常不幸的情况下(当事件发生的时间与重置计数器的时间大致相同)您可能最终会出错使统计不准确的下一个时期的起始值。

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:我所做的改变:

  1. I removed the lock, because it doesn't give anything extra in my opinion.我移除了锁,因为在我看来它没有提供任何额外的东西。
  2. I replaced 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()因为它做同样的事情,但负荷运转是显而易见的。
  3. I replaced the 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
  4. Added a function GetAndClearCounter1 to guarantee that there is no other operation is happening between reading and clearing the value.添加了函数GetAndClearCounter1以保证在读取和清除值之间没有发生其他操作。
  5. Removed the unused + 1 and - 1 in IncCounter1 and DecCounter1 .删除未使用的+ 1- 1IncCounter1DecCounter1
  6. Changed the 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM