并发编程一个主要问题就是如何同步数据。同步数据的方式有很多种,这里我们介绍一下libkern/OSAtomic.h
。这个头文件包含是大量关于原子操作和同步操作的函数,如果要对数据进行同步操作,这里面的函数可以作为我们的首选项。不同平台这些函数的实现是自定义的。另外,它们是线程安全的。
需要注意的是,传递给这些函数的所有地址都必须是“自然对齐”的,例如int32_t *
指针必须是32位对齐的(地址的低位2个bit为0),int64_t *
指针必须是64位对齐的(低3位为0)。
这些原子函数的一些版本整合了内存屏障(memory barriers),而另一些则没有。在诸如PPC这样的弱有序(weakly-ordered)架构中,Barriers严格限制了内存访问顺序。所有出现在barriers之前的加载和存储操作完成后,才会运行barriers之后的加载和存储操作。
在单处理器系统中,barriers操作通常是一个空操作。在多处理器系统中,barriers在某些平台上可能是相当昂贵的操作,如PPC。
大多数代码都应该使用barrier函数来确保在线程间共享的内存是正确同步的。例如,如果我们想要初始化一个共享的数据结构,然后自动增加某个变量值来标识初始化操作完成,则我们必须使用OSAtomicIncrement32Barrier
来确保数据结构的存储操作在变量自动增加前完成。
同样的,该数据结构的消费者也必须使用OSAtomicIncrement32Barrier
,以确保在自动递增变量值之后再去加载这些数据。另一方面,如果我们只是简单地递增一个全局计数器,那么使用OSAtomicIncrement32
会更安全且可能更快。
如果不能确保我们使用的是哪个版本,则使用barrier变量以保证是安全的。
另外,自旋锁和队列操作总是包含一个barrier。
这个头文件中的函数主要可以分为以下几类
内存屏障(Memory barriers)
内存屏障的概念如上所述,它是一种屏障和指令类,可以让CPU或编译器强制将barrier之前和之后的内存操作分开。CPU采用了一些可能导致乱序执行的性能优化。在单个线程的执行中,内存操作的顺序一般是悄无声息的,但是在并发编程和设备驱动程序中就可能出现一些不可预知的行为,除非我们小心地去控制。排序约束的特性是依赖于硬件的,并由架构的内存顺序模型来定义。一些架构定义了多种barrier来执行不同的顺序约束。
OSMemoryBarrier()
函数就是用来设置内存屏障,它即可以用于读操作,也可以用于写操作。
示例代码:
|
|
自旋锁(Spinlocks)
自旋锁是在多处理器系统(SMP)上为保护一段关键代码的执行或者关键数据的一种保护机制,是实现synchronization的一种手段。
libkern/OSAtomic.h
中包含了三个关于自旋锁的函数:OSSpinLockLock
, OSSpinLockTry
, OSSpinLockUnlock
。
示例代码:
|
|
原子队列操作
队列操作主要包含两类:
- 不加锁的FIFO入队和出队原子操作,包含
OSAtomicFifoDequeue
和OSAtomicFifoEnqueue
两个函数 - 不加锁的LIFO入队和出队原子操作,包含
OSAtomicDequeue
和OSAtomicEnqueue
两个函数。这两个函数是线程安全的,对有潜在精确要求的代码来说,这会是强大的构建方式。
比较和交换
这组函数可以细分为三组函数:
OSAtomicCompareAndSwap**[Barrier](type __oldValue, type __newValue, volatile type *__theValue)
:这组函数用于比较__oldValue
是否与__theValue
指针指向的内存位置的值匹配,如果匹配,则将__newValue
的值存储到__theValue
指向的内存位置。可以根据需要使用barrier版本。OSAtomicTestAndClear/OSAtomicTestAndClearBarrier(uint32_t __n, volatile void * __theAddress)
:这组函数用于测试__theAddress
指向的值中由__n
指定的bit位,如果该位未被清除,则清除它。需要注意的是最低bit位应该是1,而不是0。对于一个64-bit的值来说,如果要清除最高位的值,则__n
应该是64。OSAtomicTestAndSet/OSAtomicTestAndSetBarrier(uint32_t __n, volatile void * __theAddress)
:与OSAtomicTestAndClear
相反,这组函数测试值后,如果指定位没有设置,则设置它。
示例代码:
|
|
上述代码的作用是如果没有缓冲区,我们将创建一个newBuffer,然后将其写到buffer中。
布尔操作(AND, OR, XOR)
这组函数可根据以下两个规则来分类:
- 是否使用Barrier
- 返回值是原始值还是操作完成后的值
以And为例,有4个函数:OSAtomicAnd32
, OSAtomicAnd32Barrier
, OSAtomicAnd32Orig
, OSAtomicAnd32OrigBarrier
。每个函数均带有两个参数:__theMask(uint32_t)
和__theValue(volatile uint32_t *)
。函数将__theMask
与__theValue
指向的值做AND操作。
类似,还有OR操作和XOR操作。
数学操作
这组函数主要包括:
- 加操作:
OSAtomicAdd**
,OSAtomicAdd**Barrier
- 递减操作:
OSAtomicDecrement**
,OSAtomicDecrement**Barrier
- 递增操作:
OSAtomicIncrement**
,OSAtomicIncrement**Barrier
示例代码:
|
|
小结
相较于@synchronized
,OSAtomic原子操作更趋于数据的底层,从更深层次来对单例进行保护。同时,它没有阻断其它线程对函数的访问。