解锁 C 并发编程的钥匙:探索 Atomic 变量
创始人
2025-07-08 01:51:14
0

最近在用c++搞项目,因为多线程要做一个类似cnt的保护,今天学习了c++的原子操作。

探索c++的原子类型

std::atomic 类型是 C++ 提供的一种机制,用于实现多线程之间的安全共享数据。它通过原子操作来确保对共享变量的操作是不可分割的。在多线程环境下,如果没有适当的同步机制,对共享变量的读写可能会导致竞争条件,进而引发不确定的行为。std::atomic 类型提供了一种解决方案,让我们能够以线程安全的方式访问这些变量。

关于具体的函数和详细介绍可以访问这里:https://cplusplus.com/reference/atomic/atomic/?kw=atomic

这里介绍几个常用的:

  • load 和 store:用于读取和写入原子变量的值。
  • exchange:交换原子变量的值,并返回之前的值。
  • compare_exchange_strong 和 compare_exchange_weak:比较并交换操作,可在特定条件下修改原子变量的值。
  • fetch_add 和 fetch_sub:原子地执行加法和减法操作,并返回之前的值。

这里原子操作后为什么要返回之前的值呢?

以fetch_add为例,fetch_add是用于对原子变量进行原子性地增加操作。它执行一个原子的加法操作,并返回加法操作之前的原子变量的值。

这种设计是基于并发编程中的常见需求。返回之前的值允许程序员在执行加法操作后,获取加法之前的原始值。这样做有以下几个方面的优点:

  • 原子性操作的完整性:在多线程并发环境下,如果需要进行原子性的加法操作,同时又需要获取加法前的值,fetch_add 的设计能够保证这两个操作的原子性。它在单个原子操作中完成增加操作,并返回增加前的值,避免了在多线程环境下的竞态条件。
  • 避免竞态条件:返回之前的值可以让程序员在进行加法操作之后,检查原子变量的旧值,并根据旧值进行后续的操作。这对于实现一些特定的同步模式或算法是非常有用的,因为它避免了因为操作间的竞争导致的意外结果。

举个栗子

这里做一个简单的线程池,并实现一个task,task的任务就是对原子变量counter进行递增,最后我们看结果是否与预期一致,这里线程池实现10个线程,给线程池推送100000个task。

#include 
#include 
#include 
#include 
#include 
#include 
#include 

class ThreadPool {
public:
    ThreadPool(size_t numThreads) : stop(false) {
        for (size_t i = 0; i < numThreads; ++i) {
            threads.emplace_back([this] {
                while (true) {
                    std::function task;
                    {
                        std::unique_lock lock(queueMutex);
                        condition.wait(lock, [this] { return stop || !tasks.empty(); });
                        if (stop && tasks.empty()) {
                            return;
                        }
                        task = std::move(tasks.front());
                        tasks.pop();
                    }
                    task();
                }
            });
        }
    }

    template 
    void AddTask(F&& f) {
        {
            std::lock_guard lock(queueMutex);
            tasks.emplace(std::forward(f));
        }
        condition.notify_one();
    }

    ~ThreadPool() {
        {
            std::lock_guard lock(queueMutex);
            stop = true;
        }
        condition.notify_all();
        for (std::thread& worker : threads) {
            worker.join();
        }
    }

private:
    std::vector threads;
    std::queue> tasks;
    std::mutex queueMutex;
    std::condition_variable condition;
    bool stop;
};

int main() {
    std::atomic counter(0);
    ThreadPool pool(10);

    constexpr int numTasks = 100000;
    for (int i = 0; i < numTasks; ++i) {
        pool.AddTask([&counter]() {
            counter++;
        });
    }

    std::cout << "Waiting for tasks to complete..." << std::endl;

    //注意:这里不会确保所有任务已经执行完毕,仅仅是等待一段时间以展示结果
    std::this_thread::sleep_for(std::chrono::seconds(5));

    std::cout << "Final Counter Value: " << counter << std::endl;

    return 0;
}

我们预期最后的结果是100000。g++编译,不要忘记加-lpthread,执行:

细心的小伙伴可能发现我的代码直接使用的counter++,这里需要注意,这只是个简单的测试代码,实际项目中要最好使用counter.fetch_add(1),因为counter++不保证++是个原子操作。我在项目中遇到了该问题,最后加出来总会比预期值少,后来换成fetch_add后就正常了。

相关内容

热门资讯

PHP新手之PHP入门 PHP是一种易于学习和使用的服务器端脚本语言。只需要很少的编程知识你就能使用PHP建立一个真正交互的...
网络中立的未来 网络中立性是什... 《牛津词典》中对“网络中立”的解释是“电信运营商应秉持的一种原则,即不考虑来源地提供所有内容和应用的...
各种千兆交换机的数据接口类型详... 千兆交换机有很多值得学习的地方,这里我们主要介绍各种千兆交换机的数据接口类型,作为局域网的主要连接设...
全面诠释网络负载均衡 负载均衡的出现大大缓解了服务器的压力,更是有效的利用了资源,提高了效率。那么我们现在来说一下网络负载...
什么是大数据安全 什么是大数据... 在《为什么需要大数据安全分析》一文中,我们已经阐述了一个重要观点,即:安全要素信息呈现出大数据的特征...
如何允许远程连接到MySQL数... [[277004]]【51CTO.com快译】默认情况下,MySQL服务器仅侦听来自localhos...
如何利用交换机和端口设置来管理... 在网络管理中,总是有些人让管理员头疼。下面我们就将介绍一下一个网管员利用交换机以及端口设置等来进行D...
P2P的自白|我不生产内容,我... 现在一提起P2P,人们就会联想到正在被有关部门“围剿”的互联网理财服务。×租宝事件使得劳...
Intel将Moblin社区控... 本周二,非营利机构Linux基金会宣布,他们将担负起Moblin社区的管理工作,而这之前,Mobli...
施耐德电气数据中心整体解决方案... 近日,全球能效管理专家施耐德电气正式启动大型体验活动“能效中国行——2012卡车巡展”,作为该活动的...