在进行多线程编成的时候,我们经常会遇到线程的互斥与同步问题。比如多个线程访问同一个变量,需要互斥的操作,一个线程需要等待另一个线程处理后再进行接下来的操作等等。接下来我们看一下线程的互斥,原子操作。
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
上面只是提供了Windows上提供的原子操作相关的API,无法移植到Linux或Mac等其他操作系统上。C++11为我们提供了对于原子操作标准上的支持。使用模板 std::atomic ,需要包含头文件 <atomic> 。
比如使用如下代码就可以创建一个原子类型的int值对象:
std::atomic<int> a;
除了可以使用模板,也可以使用内置的一些类型
原子类型名称 | 对应的内置类型名称 |
---|---|
atomic_bool | bool |
atomic_char | char |
atomic_schar | signed char |
atomic_uchar | unsigned char |
atomic_int | int |
atomic_uint | unsigned int |
atomic_short | short |
atomic_ushort | unsigned short |
atomic_long | long |
atomic_ulong | unsigned long |
atomic_llong | long long |
atomic_ullong | unsigned long long |
atomic_char16_t | char16_t |
atmoic_char32_t | char32_t |
atmoic_wchar_t | wchart_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_flag | atomic_bool | atmoic_integral-type | atomic<T*> | atomic<Class-Type> |
---|---|---|---|---|---|
test_and_set | y | ||||
clear | y | ||||
is_lock_free | y | y | y | y | |
load | y | y | y | y | |
store | y | y | y | y | |
exchange | y | y | y | y | |
compare_exchange_weak +strong | y | y | y | y | |
fetch_add, += | y | y | |||
fetch_sub, -= | y | y | |||
fetch_or, |= | y | y | |||
fetch_and, &= | y | ||||
fetch_xor, ^= | y | ||||
++,– | y | y | y |
大部分原子类型都有读、写、交换、比较交换等操作。
这里需要指出的是,atomic_flag 和 atomic_bool 是不同的, 相比其他的原子类型,atmoic_flag 是无锁类型,即线程访问不需要加锁。 典型的使用是使用,成员 test_and_set 和 clear 实现自旋锁。
自旋锁 和 互斥锁 都是一种实现资源保护的一种锁机制。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说, 在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。 但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁, "自旋"一词就是因此而得名。-- 摘自百度百科。百度百科-自旋锁
接下来是一个自旋锁的例子,本例子中线程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();
}
具体调用如下:
// 自旋锁
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 | 全部存取都按顺序执行 |