线程的互斥和同步(1)- 原子操作与自旋锁

在进行多线程编成的时候,我们经常会遇到线程的互斥与同步问题。比如多个线程访问同一个变量,需要互斥的操作,一个线程需要等待另一个线程处理后再进行接下来的操作等等。接下来我们看一下线程的互斥,原子操作。

原子操作 ,是多线程程序中 “最小的且不可并行化的” 的操作。 通常一个资源的操作是原子操作的话,意味着多个线程访问资源时,有且仅有唯一一个线程在对这个资源进行操作。

1. 使用Windows API实现原子操作

Windows 提供了原子操作的API接口,这些API可以对整型变量进行操作,下面列出几个相关的相关的API及说明:

函数名函数说明
InterlockedIncrement将整型变量自增1
InterlockedDecrement将整型变量自减1
InterlockedExchangeAdd将整型变量增加n
InterlockedXor将整型变量异或操作
InterlockedCompareExchange将整型值与 n1 进行比较,如果相等,则替换成 n2

下面是一个简单示例,本实例中是继承自 CThread 写的多线程程序,关于 CThread 的实现可以参照
使用Windows API实现自定义线程类CThread

#include "CThread.h"
class WinAtomicThread : public CThread
{
public:
    void run(void) override;
};

源文件实现:

LONG WinAtomic = 0;
void WinAtomicThread::run(void)
{
    while (1) {
        LONG count = ::InterlockedIncrement(&WinAtomic);
        std::cout << "Run in Thread ID " << ::GetCurrentThreadId() \
                  << " , Number is " << count << std::endl;
        
        Sleep(500);
    }
}

这里在线程中,对整型变量做简单的自增操作。
函数调用如下:

// Win原子操作测试
WinAtomicThread *thread1 = new WinAtomicThread;
WinAtomicThread *thread2 = new WinAtomicThread;
thread1->start();
thread2->start();

thread1->wait();
thread2->wait();

运行结果如下
Run in Thread ID 21112 , Number is 2
Run in Thread ID 6900 , Number is 1
Run in Thread ID 21112 , Number is 4
Run in Thread ID 6900 , Number is 3
Run in Thread ID 21112 , Number is 5
Run in Thread ID 6900 , Number is 6


2. 使用C++11提供的原子对象实现原子操作

上面只是提供了Windows上提供的原子操作相关的API,无法移植到Linux或Mac等其他操作系统上。C++11为我们提供了对于原子操作标准上的支持。使用模板 std::atomic ,需要包含头文件 <atomic>
比如使用如下代码就可以创建一个原子类型的int值对象:

std::atomic<int> a;

除了可以使用模板,也可以使用内置的一些类型

原子类型名称对应的内置类型名称
atomic_boolbool
atomic_charchar
atomic_scharsigned char
atomic_ucharunsigned char
atomic_intint
atomic_uintunsigned int
atomic_shortshort
atomic_ushortunsigned short
atomic_longlong
atomic_ulongunsigned long
atomic_llonglong long
atomic_ullongunsigned long long
atomic_char16_tchar16_t
atmoic_char32_tchar32_t
atmoic_wchar_twchart_t

对于线程而言,原子类型属性资源型数据,这意味着多个线程只能访问单个原子类型的拷贝。因此在C++11中,原子类型只能从模板类型中进行构造,不允许原子类型进行拷贝构造、移动构造,以及operator=等。std::atomic的实现中有下面几句代码:

atomic(const atomic&) = delete;
atomic& operator=(const atomic&) = delete;
atomic& operator=(const atomic&) volatile = delete;

下面是一个简单的使用示例,同样也是实现了多线程自增操作:

头文件:

#include "CThread.h"
#include <atomic>

class STDAtomicThread : public CThread
{
public:
    void run(void) override;
};

源文件:

std::atomic<int> STDAtomicValue(0);
void STDAtomicThread::run(void)
{
    while (1) {
        std::cout << "Run in Thread ID " << ::GetCurrentThreadId() \
            << " , Number is " << STDAtomicValue++ << std::endl;

        Sleep(500);
    }
}

这里使用++操作,std::atmoic重载了++操作符,实现了原子量的自增操作。
下面列出了关于 std::atmoic 的主要操作:

操作atomic_flagatomic_boolatmoic_integral-typeatomic<T*>atomic<Class-Type>
test_and_sety
cleary
is_lock_free yyyy
load yyyy
store yyyy
exchange yyyy
compare_exchange_weak +strong yyyy
fetch_add, += yy
fetch_sub, -= yy
fetch_or, |= y y
fetch_and, &= y
fetch_xor, ^= y
++,– yyy

大部分原子类型都有读、写、交换、比较交换等操作。

这里需要指出的是,atomic_flagatomic_bool 是不同的, 相比其他的原子类型,atmoic_flag 是无锁类型,即线程访问不需要加锁。 典型的使用是使用,成员 test_and_setclear 实现自旋锁。


3. 使用atmoic_flag实现自旋锁

自旋锁(spinlock) :是当一个线程获取锁的时候,如果锁已经被其他线程获取,那么该线程会循环等待,直到锁获取成功再退出循环。

自旋锁互斥锁 都是一种实现资源保护的一种锁机制。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说, 在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。 但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁, "自旋"一词就是因此而得名。-- 摘自百度百科。百度百科-自旋锁

接下来是一个自旋锁的例子,本例子中线程1等待线程2释放锁后执行:

头文件

#include "CThread.h"
#include <atomic>
#include <iostream>
// 线程1
class SpinLockThread1 : public CThread
{
public:
    void run(void) override;
};
// 线程2
class SpinLockThread2 : public CThread
{
public:
    void run(void) override;
};

源文件:

std::atomic_flag lock{1};

void SpinLockThread1::run(void)
{
    std::cout << "Start Run Thread1" << std::endl;
    // 自旋等待
    while (lock.test_and_set(std::memory_order_acquire))
        std::cout << "Wait For UnLock" << std::endl;
    std::cout << "End Run Thread1" << std::endl;
}

void SpinLockThread2::run(void)
{
    std::cout << "Start Run Thread2" << std::endl;
    Sleep(20);
    std::cout << "Thread2 Free Lock" << std::endl;
    // 解自旋锁
    lock.clear();
}
  • 线程1中不断的判断函数 test_and_set 的返回值,如果返回值为true,则一直打印 Wait For UnLock ,即进入自旋状态。函数 test_and_set ,表示设置lock为true,并返回设置前的值。
  • 线程2中等待20ms后,调用函数 clear() 是指将lock的值设置为false,因此线程1退出自旋。

具体调用如下:

// 自旋锁
SpinLockThread1 *thread1 = new SpinLockThread1;
SpinLockThread2 *thread2 = new SpinLockThread2;

thread1->start();
thread2->start();

thread1->wait();
thread2->wait();

运行结果
Created Thread Success, Id is 17876
Created Thread Success, Id is 12556
Start Run Thread2
Start Run Thread1
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Wait For UnLock
Thread2 Free Lock
Wait For UnLock
End Run Thread1

我们也可以将封装为Lock和Unlock函数

// 加锁
void Lock(std::atomic_flag* lock) {
    while (lock->test_and_set(std::memory_order_acquire));
}
// 解锁
void Unlock(std::atomic_flag* lock){
    lock->free();
}

关于函数 test_and_set 的参数 std::memory_order_acquire , 表示在本线程中后续的读操作必须在本条原子操作完成后执行。因为不同的CPU可能实际的程序执行顺序并不是代码的顺序。

还有其他的值可以被设置,如下表所示:

枚举值说明
memory_order_relaxed不对执行顺序做任何保证
memory_order_acquire本线程中, 所有后续的读操作必须在本条原子操作完成后执行
memory_order_release本线程中,所有之前的写操作完成后才能执行本条原子操作
memory_order_acq_rel同时包含 memory_order_acquire 和 memory_order_release 标记
memory_order_consume本线程中,所有后续的有关本原子类型的操作,必须本条原子操作完成之后执行
memory_order_seq_cst全部存取都按顺序执行
不会飞的纸飞机
扫一扫二维码,了解我的更多动态。

下一篇文章:线程的互斥和同步(2)- Windows的临界区