1.缓存一致性问题?
缓存一致性问题发生在多核处理器中,当多个处理器共享同一主内存时,为了提高数据访问速度,每个处理器都有自己的本地高速缓存。
当处理器修改了缓存中的数据后,如果缓存之间没有得到正确的同步更新,可能会导致多个处理器看到的数据不一致,从而引发错误。
现代处理器通过缓存一致性协议(MESI)解决缓存一致性问题。
2.原子操作
2.1 什么是原子操作?
原子操作(Atomic Operation)是计算机编程中的一种概念,它指的是在单个处理器执行期间,一组操作被视为不可分割的整体,要么全部完成,要么全部不执行。
这些操作不会被其他并发线程或中断所打断,因此保证了数据的一致性和完整性,特别是在多线程和并发环境下特别重要。
原子操作的特点包括:
-
封装性:操作内部的细节对外部不可见,保证了操作的可见性和顺序一致性。
-
隔离性:在同一时间点内,只有单个线程能够执行原子操作,避免了竞态条件(race condition)。
-
原子性:即使在并发环境中,整个操作也表现为不可分割的单位。
常见的原子操作包括读取/写入操作(如 CAS, compare-and-swap)、自旋锁、递增/递减计数等。
在多核处理器和现代硬件支持下,一些操作可以自然地被硬件实现为原子操作,但在没有这些支持的语言或平台上,程序员可能需要使用锁或其他同步机制来模拟原子操作。
2.2 原子操作实现原理
多个线程同时访问内存中一个变量,未使用原子操作的线程,将会并行访问同一个变量,造成缓存一致性问题,从而导致程序逻辑出现问题。
多个线程采用原子操作的方式访问内存中变量,原子操作通常是独占加载和独占存储,在一个原子操作执行期间,其他的线程无法访问内存中同一变量,这样避免了缓存不一致性问题,原子操作将并行访问变成串行访问。
原子操作的实现需要硬件的配合,实现原理也很复杂,我们一定得清楚原子操作解决的是什么问题,才能更好的使用原子操作。
3.原子操作编程
3.1C语言原子操作
C11标准中引入原子操作,实现了一整套完整的原子操作接口,定义在头文件<stdatomic.h>,C++语言是在C++11标准中引入原子操作,定义在头文件<atomic>,这里我们讨论的是C语言原子操作。
3.1.1 原子变量
原子变量是一种特殊的数据类型,原子操作访问的对象是原子变量。原子变量定义方式:atomic_数据类型 变量名。
typedef_Atomic(bool)atomic_bool; typedef_Atomic(char)atomic_char; typedef_Atomic(signedchar)atomic_schar; typedef_Atomic(unsignedchar)atomic_uchar; typedef_Atomic(short)atomic_short; typedef_Atomic(unsignedshort)atomic_ushort; typedef_Atomic(int)atomic_int; typedef_Atomic(unsignedint)atomic_uint; typedef_Atomic(long)atomic_long; typedef_Atomic(unsignedlong)atomic_ulong; typedef_Atomic(longlong)atomic_llong; typedef_Atomic(unsignedlonglong)atomic_ullong;
3.1.2 原子操作
1)原子变量初始化
voidatomic_init(obj, val);
atomic_init函数用于初始化原子变量。
-
obj:原子变量地址。
-
val:数值。
如:atomic_init(&lock, 1);
2)原子加载和存储
voidatomic_store(object, desired); voidatomic_store_explicit(object, desired, memory_order order); Tatomic_load(object); Tatomic_load_explicit(object, desired, memory_order order);
atomic_store系列函数用于设置原子变量的值。
atomic_load系列函数用于加载原子变量的值,返回原子变量的值。
-
object:原子变量地址。
-
desired:数值。
-
order:内存顺序。
3)原子交换
Tatomic_exchange(object, desired); Tatomic_exchange_explicit(object, desired, memory_order order);
atomic_exchange系列函数用于设置原子变量的值,并返回原子变量旧值。
-
object:原子变量地址。
-
desired:数值。
-
order:内存顺序。
4)原子比较交换(CAS)
boolatomic_compare_exchange_strong(object, expected, desired); boolatomic_compare_exchange_strong_explicit(object, expected, desired,memory_order suc, memory_order fail); boolatomic_compare_exchange_weak(object, expected, desired); boolatomic_compare_exchange_weak_explicit(object, expected, desired);
atomic_compare_exchange系列函数就是著名的CAS函数,该系列函数功能:
-
如果原子变量object和expected值相等,把原子变量设置成desired,返回true。
-
如果原子变量object和expected值不相等,则把expected设置成object,返回false。
weak和strong的区别在于weak系列函数存在一定的误判,需要通过while循环再次判断。
-
object:原子变量地址。
-
expected:预期值,需填变量内存地址。
-
desired:数值。
-
suc:成功时的内存顺序。
-
fail:失败时的内存顺序。
5)原子运算
Tatomic_fetch_add(object, operand); Tatomic_fetch_add_explicit(object, operand); Tatomic_fetch_sub(object, operand); Tatomic_fetch_sub_explicit(object, operand); Tatomic_fetch_or(object, operand); Tatomic_fetch_or_explicit(object, operand); Tatomic_fetch_xor(object, operand); Tatomic_fetch_xor_explicit(object, operand); Tatomic_fetch_and(object, operand); Tatomic_fetch_and_explicit(object, operand);
atomic_fetch系列函数用于执行原子变量加,减,或,异或,与操作,返回原子变量之前旧值。
-
object:原子变量地址。
-
operand:数值。
-
order:内存顺序。
3.1.3 内存顺序(Memory Order)
typedefenummemory_order { memory_order_relaxed = __ATOMIC_RELAXED, memory_order_consume = __ATOMIC_CONSUME, memory_order_acquire = __ATOMIC_ACQUIRE, memory_order_release = __ATOMIC_RELEASE, memory_order_acq_rel = __ATOMIC_ACQ_REL, memory_order_seq_cst = __ATOMIC_SEQ_CST } memory_order;
-
memory_order_relaxed:最宽松的顺序,不保证操作的顺序,可能会导致数据竞争(data races)。
-
memory_order_consume:主要用于无须保持历史状态的读操作,可以优化某些场景下的性能。
-
memory_order_acquire:确保之前的写操作已经对其他线程可见,但可能重排序。
-
memory_order_release:确保当前写操作对其他线程立即可见,但之前的读操作可以重排序。
-
memory_order_acq_rel:同时满足 acquire 和 release,适合于读-修改-写的情况。
-
memory_order_seq_cst:最严格的顺序,保证操作的顺序与单线程程序一致,包括内存顺序和程序顺序。
4.原子锁
4.1 自定义原子锁
1)原子锁初始化
atomic_boolatomic_lock;//定义原子锁 atomic_init(&atomic_lock,false);//初始化原子锁
2)原子锁加锁
boolval =false;//通过CAS指令实现原子锁加锁 while(!atomic_compare_exchange_weak_explicit(&atomic_lock, &val,true, memory_order_acquire, memory_order_relaxed)) { val =false; }
3)原子锁解锁
//通过store指令设置原子锁的值为false atomic_store_explicit(&atomic_lock,false, memory_order_release);
4.2 锁性能分析
既然今天讨论的是高性能编程,所以我们得回归到高性能编程的主题,我们来探讨一下无锁,互斥锁,自旋锁,自定义原子锁之间的性能差距。
4.2.1 测试方法
测试代码:
#include<stdio.h> #include<stdbool.h> #include<pthread.h> #include<stdatomic.h> #defineTHREAD_NUM (4)//测试线程数量 #defineTIMES (50000000LL)//每个线程自增次数 #defineLOCK_TYPE (3)//锁类型 #defineFREE_LOCK 0//无锁 #defineMUTEX_LOCK 1//互斥锁 #defineSPIN_LOCK 2//自旋锁 #defineATOMIC_LOCK 3//自定义原子锁 longlongsum =0; pthread_mutex_tmutex; pthread_spinlock_tspinlock; atomic_boolatomic_lock; voiddo_lock(){ #if(LOCK_TYPE == MUTEX_LOCK) pthread_mutex_lock(&mutex); #elif(LOCK_TYPE == SPIN_LOCK) pthread_spin_lock(&spinlock); #elif(LOCK_TYPE == ATOMIC_LOCK) boolval =false; while(!atomic_compare_exchange_weak_explicit(&atomic_lock, &val,true, memory_order_acquire, memory_order_relaxed)) { val =false; } #else #endif } voiddo_unlock(){ #if(LOCK_TYPE == MUTEX_LOCK) pthread_mutex_unlock(&mutex); #elif(LOCK_TYPE == SPIN_LOCK) pthread_spin_unlock(&spinlock); #elif(LOCK_TYPE == ATOMIC_LOCK) atomic_store_explicit(&atomic_lock,false, memory_order_release); #else #endif } void*test_proc(void*arg){ for(longlongi =0; i < TIMES; i++) { do_lock(); sum++; do_unlock(); } returnNULL; } intmain(intargc,char*argv[]){ pthread_tth[THREAD_NUM]; pthread_mutex_init(&mutex,NULL); pthread_spin_init(&spinlock,0); atomic_init(&atomic_lock,false); for(inti =0; i < THREAD_NUM; i++) { pthread_create(&th[i],NULL, test_proc,NULL); } for(inti =0; i < THREAD_NUM; i++) { pthread_join(th[i],NULL); } pthread_mutex_destroy(&mutex); pthread_spin_destroy(&spinlock); printf("sum:%u\n", sum); return0; }
测试参数:
-
THREAD_NUM:测试线程数量。
-
TIMES:每个线程自增次数。
-
LOCK_TYPE:锁类型。
通过gcc test.c -o test命令将程序编译成可执行程序,然后通过time命令执行程序。
-
实际时间:程序运行的实际时长。
-
用户时间:程序在用户态运行的时长。
-
系统时间:程序在内核态运行的时长。
注意:多核处理器存在用户时间+系统时间 > 实际时间的情况。
4.1.2 测试结果对比
测试硬件环境:树莓派4B,内存4GB。
测试参数:测试线程数量4,每个线程自增次数5千万。
1)锁类型:无锁
线程不加锁的情况,4个线程总的自增次数不是2亿次,测试结果错误,程序运行实际时间只有0.4秒,用户时间和系统时间总共1.6秒。
2)锁类型:互斥锁
线程加互斥锁,4个线程总的自增次数为2亿次,测试结果正确,程序运行实际时间为33.9秒,用户时间和系统时间总共2分06秒。
系统时间为50.3秒,说明互斥锁存在大量的系统调用。
3)锁类型:自旋锁
线程加自旋锁,4个线程总的自增次数为2亿次,测试结果正确,程序运行实际时间为22.8秒,用户时间和系统时间公共1分21秒。
系统时间为0.009秒,说明自旋锁无系统调用。
4)锁类型:自定义原子锁
线程加自定义原子锁,4个线程总的自增次数为2亿次,测试结果正确,程序运行实际时间为21.9秒,用户时间和系统时间公共1分07秒。
系统时间为0.005秒,说明自定义原子锁无系统调用。
总结:
-
对比四组测试结果,线程不加锁情况下,运行效率最高,但是测试结果不正确,所以线程编程必须要加锁。
-
互斥锁虽然能保证测试结果正确,但是存在大量系统调用,运行效率低,适用于并发量低的场景。
-
自旋锁和自定义原子锁都能保证测试结果正确,二者都不存在系统调用,且效率高,通过阅读glibc源码发现,自旋锁是基于原子操作实现。
感谢观看,欢迎点赞,收藏,转发。
本文采摘于网络,不代表本站立场,转载联系作者并注明出处:https://www.iotsj.com//kuaixun/3080.html