繁体   English   中英

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

[英]Creating a thread safe atomic counter

我在我的一个项目中有一个特定要求,即保持某些操作的“计数”,并最终定期“读取”+“重置”这些计数器(例如 24 小时)。

操作将是:

  • 工作线程 -> 增加计数器(随机)
  • 计时器线程(例如 24 小时)-> 读取计数-> 做某事-> 重置计数器

我感兴趣的平台是 Windows,但如果它可以跨平台更好。 我正在使用 Visual Studio,目标 Windows 体系结构仅为 x64

我不确定结果是否“确定”以及我的实施是否正确。 坦率地说,从来没有使用过很多 std 包装器,而且我的 C++ 知识非常有限。

结果是:

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

以下是一个完全复制/粘贴可重现的示例:

#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();
}

我应该提到,在我的现实生活项目中,没有使用 std::thread 包装器,并且线程是使用 WinApi 函数(如 CreateThread)创建的。 以上只是模拟/测试代码。

请向我指出上述代码有什么问题,可以改进的地方以及我的方向是否正确。

谢谢!

这看起来很可疑:

std::uint32_t IncCounter1() noexcept 
{
    m_Counter1.fetch_add(1, std::memory_order_relaxed) + 1;
    return m_Counter1;
}

该行末尾的+ 1实际上是一个空操作,因为代码没有将该表达式的结果分配给任何东西。 此外,您通过在第二行再次引用m_Counter1创建竞争条件 - 因为在执行fetch_add和再次引用该值以返回结果之间很容易发生上下文切换到另一个线程改变它的值。

我认为你想要这个:

std::uint32_t IncCounter1() noexcept 
{
    return m_Counter1.fetch_add(1, std::memory_order_relaxed) + 1;
}

fetch_add将在增量之前返回先前保存的值。 因此,返回+1将是当时计数器的当前值。

DecCounter1 中的相同问题。 将该函数的实现更改为:

std::uint32_t DecCounter1() noexcept
{
    return m_Counter1.fetch_sub(1, std::memory_order_relaxed) - 1;
}

我认为可能存在问题,具体取决于这些计数器的使用方式。 如果大量DecCounter1和一个ClearCounter1 (或更多)被大约同时调用,那么可能会发生ClearCounter1将计数器设置为0 ,然后执行大量DefCounter1 (在锁阻止它们这样做之前),并且计数器最终为负。 在以下情况下,这可能是一个问题:

  1. 计数器用于任何类型的资源管理(根据计数器的值释放内存/文件/等)
  2. 计数器仅用于某些统计,但计数器的值与线程数不成正比。 例如:如果在您要计算的当天发生了 8 个事件,但线程数超过 8 个,那么在非常不幸的情况下(当事件发生的时间与重置计数器的时间大致相同)您可能最终会出错使统计不准确的下一个时期的起始值。

如果以上任何一条是真的,那么情况就困难得多,我还没有考虑过。 对于像计数器这样的简单统计数据,我认为可以改进代码:原子和锁的作用几乎相同,通过锁定,您可能会降低性能而不会获得任何好处(关于您是否使用锁,上述所有问题都是正确的,原子或两者)。 因此,我会摆脱锁并使用纯粹的原子。 如果上述问题都不适用,这是我对改进版本的建议:

#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();
}

我所做的改变:

  1. 我移除了锁,因为在我看来它没有提供任何额外的东西。
  2. 我换成m_Counter1.fetch_sub(0)m_Counter1.load()因为它做同样的事情,但负荷运转是显而易见的。
  3. 我用atomic_uint替换atomic_uint类型只是为了匹配atomic_uint32_t的返回值的GetTotalCounter1
  4. 添加了函数GetAndClearCounter1以保证在读取和清除值之间没有发生其他操作。
  5. 删除未使用的+ 1- 1IncCounter1DecCounter1
  6. VOID返回类型更改为void ,因为在我看来void更惯用。

作为最终结论,我认为阅读std::atomic文档真的很有帮助。 它不是最短的文档,但绝对足够详细以熟悉操作、内存顺序等。

为什么要编写ThreadSafeCounter类?

std::atomic<size_t>一个 ThreadSafeCounter。 这就是 std::atomic 的全部意义所在。 所以你应该改用它。 不需要再上课了。 大多数原子都有 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));
        }
    };

我忘了提及您的 DecCounter 操作的问题。 您正在使用 atomic_uint,它无法处理负数。 但是不能保证您的 Thread2 不会在 Thread1 之前运行(也就是递减计数器)。 这意味着您的计数器将换行。

所以你可以/应该使用std::atomic<int>代替。 这将为您提供正确数量的 (calls_Thread1 - call_Thread2)。 如果 Thead2 比 Thread1 更频繁地减少值,则该数字将变为负数。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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