# C++中的原子操作

C++的原子操作也是为了解决多线程编程中同步的问题,它保证在执行原子操作时不会被其他线程干扰。原子操作分为原子赋值和原子递增/递减操作。

原子赋值操作一般用于初始化一个共享资源,确保在多个线程同时访问时,不会出现多个线程同时修改同一个资源的情况。

原子递增/递减操作一般用于计数器,确保在多个线程同时访问时,不会出现多个线程同时修改同一个计数器的情况。

# atomic_flag

std::atomic_flagC++中的一个原子布尔类型,用于实现原子锁操作。默认情况下,它是清除状态(false)。可以使用ATOMIC_FLAG_INIT宏进行初始化。std::atomic_flag类型的对象必须由宏ATOMIC_FLAG_INIT初始化,它把标志初始化为置零状态:std::atomic_flag f=ATOMIC_FLAG_INITstd::atomic_flag对象永远以置零状态开始,别无他选。

std::atomic_flag对象只能执行3种操作:销毁、置零、读取原有的值并设置标志成立。这分别对应于析构函数成员函数clear()成员函数test_and_set()

clear()是存储操作,因此无法采用std::memory_order_acquirestd::memory_order_acq_rel内存次序,test_and_set()是“读-改-写”操作,因此能采用任何内存次序,对于上面两种操作,默认的内存序都是最严格的std::memory_order_seq_cst

使用atomic_flag的一个示例:

#include <iostream>
#include <atomic>
#include <thread>

std::atomic_flag flag = ATOMIC_FLAG_INIT;

void taskFunc(int tid)
{
    while(flag.test_and_set(std::memory_order_acquire)) { } // 相当于上锁
    std::cout << "Thread " << tid << " acquired the lock" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Thread " << tid << " released the lock" << std::endl;
    flag.clear(std::memory_order_release); // 相当于解锁
}

int main(int argc, char **argv)
{
    std::thread t1(taskFunc, 1);
    std::thread t2(taskFunc, 2);
    t1.join();
    t2.join();
    return 0;
}

通过上面的例子,很容易发现可以借助atomic_flag实现自旋锁:

class SpinLockMutex
{
    std::atomic_flag flag_{ATOMIC_FLAG_INIT};
public:
    void lock()
    {
        while (flag_.test_and_set(std::memory_order_acquire))
        {
            /* spin */
        }
    }

    void unlock()
    {
        flag_.clear(std::memory_order_release);
    }
};

借用实现的自旋锁,重新实现的代码为:

#include <iostream>
#include <atomic>
#include <thread>
#include <mutex>

SpinLockMutex sl_mutex;

void taskFunc(int tid)
{
    sl_mutex.lock();
    std::cout << "Thread " << tid << " acquired the lock" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Thread " << tid << " released the lock" << std::endl;
    sl_mutex.unlock();
}

int main(int argc, char **argv)
{
    std::thread t1(taskFunc, 1);
    std::thread t2(taskFunc, 2);
    t1.join();
    t2.join();
    return 0;
}

还可以结合std::lock_guard<SpinLockMutex> lock(sl_mutex);使用。

# atomic<bool>

std::atomic_flag操作严格受限,甚至不支持单纯的无修改查值操作,无法用作普通的布尔标志,因此最好还是使用std::atomic<bool>

原子类型的一个常见模式,它们所支持的赋值操作符不返回引用,而是按值返回(该值属于对应的非原子类型)。

通过store()atomic<bool>也能设定内存次序语义。

相较于std::atomic_flagstd::atomic <bool>提供了更通用的成员函数exchange()以代替test_and_set(),它获取原有的值,还让我们自行选定新值作为替换。

std::atomic<bool>还支持单纯的读取(没有伴随的修改行为):隐式做法是将实例转换为普通布尔值,显式做法则是调用load()

总结一下就是:

  • store()是存储操作,
  • load()是载入操作
  • exchange()是“读-改-写”操作

使用atomic<bool>的一个示例:

#include <iostream>
#include <atomic>
#include <thread>
#include <mutex>
#include <functional>

class Task {
    public:
        Task() {
            task_runing_.store(true);
            std::function<void(int)> f = std::bind(&Task::run, this, std::placeholders::_1);
            task_thread_ = std::thread(f, 1);
        }

        ~Task() {
            task_thread_.join();
            task_runing_.store(false);
        }

        void stop()
        {
            task_runing_.store(false);
        }

        void restart()
        {
            task_runing_.store(true);
        }

        void run(int id){
            std::cout << "Running " << task_runing_.load() << std::endl;
            while (task_runing_.load())
            {
                std::cout << "Task " << id << " is running..." << std::endl;
                std::this_thread::sleep_for(std::chrono::milliseconds(1000));
            }            
        }

    private:
        std::atomic<bool> task_runing_;
        std::thread task_thread_;
};

int main(int argc, char **argv)
{
    auto t11 = Task();
    std::cout << "t11 start" << std::endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(2000));
    t11.stop();
    std::cout << "t11 stop"  << std::endl;
    t11.restart();
    std::cout << "t11 restart"  << std::endl;
    return 0;
}

# atomic<T*>

指向类型T的指针的原子化形式为std::atomic<T*>,类似于原子化的布尔类型std::atomic<bool>,二者接口相同,但操作目标从布尔类型变换成相应的指针类型。

std::atomic<T*>std::atomic<bool>一样,也不能拷贝复制或拷贝赋值。

std::atomic<T*>具备成员函数:

  • is_lock_free()
  • load()
  • store()
  • exchange()
  • compare_exchange_weak()
  • compare_exchange_strong()

除此之外,std::atomic<T*>提供的新操作是算术形式的指针运算:

  • **fetch_add()**对象中存储的地址进行原子化加
  • **fetch_sub()**对象中存储的地址进行原子化减
  • 该原子类型还具有包装成重载运算符的+=−=,以及++−−的前后缀版本

实例


#include <iostream>
#include <atomic>
#include <cassert>
#include <thread>
#include <mutex>
#include <functional>

class ParalLink
{   

    public:
    ParalLink() {}
    ~ParalLink() {
        Node* current = m_head.load(std::memory_order_relaxed);
        while (current) {
            Node* next = current->next;
            delete current;
            current = next;
        }
    }

    void insert(int value) {
        Node* newNode = new Node{value, nullptr};
        newNode->next = m_head.load(std::memory_order_relaxed);
        while (!m_head.compare_exchange_weak(newNode->next, newNode, std::memory_order_release)) {
        }
    }

    void printList() {
        for (auto& t : m_threads) {
            t.join();
        }
        Node* current = m_head.load(std::memory_order_relaxed);
        while (current) {
            std::cout << current->data << " ";
            current = current->next;
        }
        std::cout << std::endl;
    }

    void taskFunc(int n)
    {
        for (int i = 0; i < n; ++i) {
            insert(i*n);
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }

    void build() {
        std::function<void(int)> f = std::bind(&ParalLink::taskFunc, this, std::placeholders::_1);
        for(int i = 1; i < 4; i++) {
            m_threads.emplace_back(f, 4);
        }
    }

    private:
        std::atomic<Node*> m_head{nullptr};
        std::vector<std::thread> m_threads;
};

int main(int argc, char** argv)
{
    auto pt = ParalLink();
    pt.build();
    pt.printList();
    return 0;
}

上面的代码使用了atomic<T*>类型的原子模板,这样保护的是Node*类型的指针,而不是Node类型的结构体。如此,对Node*类型的指针m_head的操作就是原子化的了。假如,多个线程通过对指针变量的修改,导致同时发生多线程同时写同块内存的操作,程序会发生什么样的结果呢?想必依然会发生未定义的错误。

# 总结

std::atomic<>是一个模板,除了前面介绍的bool外还支持int/unsigned short/char等多种类型特化。

原子操作在多线程编程中是非常有用的,可以帮助我们避免很多问题,但是原子操作也有一定的代价,它可能会影响程序的性能。所以,在程序中使用原子操作时,需要根据实际情况,权衡性能与并发性的关系。

# reference

1.https://en.cppreference.com/w/cpp/atomic/atomic (opens new window)