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

文章目录

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

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

本篇文章全部代码下载:
https://github.com/douzhongqiang/threadCode/tree/master/Thread_Atmoic

 

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

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

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

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

头文件定义:

源文件实现:

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

运行结果如下:
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值对象:

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

原子类型名称 对应的内置类型名称
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的实现中有下面几句代码:

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

源文件:

这里使用++操作,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_flagatomic_bool 是不同的,相比其他的原子类型,atmoic_flag 是五锁类型,即线程访问不需要加锁。典型的使用是使用,成员 test_and_setclear 实现自旋锁。

 

3. 使用atmoic_flag实现自旋锁

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

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

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

源文件:

  • 线程1中不断的判断函数 test_and_set 的返回值,如果返回值为true,则一直打印 Wait For UnLock ,即进入自旋状态。函数 test_and_set ,表示设置lock为true,并返回设置前的值。
  • 线程2中等待20ms后,调用函数 clear() 是指将lock的值设置为false,因此线程1退出自旋。

具体调用如下:

运行结果:
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函数

关于函数 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 全部存取都按顺序执行

You May Also Like

About the Author: admin

喜欢编程、爱游戏,更爱生活。

发表评论

电子邮件地址不会被公开。