文章目录
- 使用场景
- 原理
-
- 关于dispatch_compiler_barrier
- 性能测试
使用场景
dispatch_once
能够保证代码块只执行一次,即使在
多线程
使用时。
一般来说我们如果要保证代码只执行一次,就是进行加锁,通过修改一个变量值
0 -> 1
来判断这段代码是否执行过。
在iOS中
dispatch_once
经常被用来创造
单例对象
,或者进行
方法交换swizzle method
例如
CCManager
继承自
NSObject
:
+ (instancetype)shared {
static CCManager *manager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[super allocWithZone:NULL] init];
});
return manager;
}
这里有几个疑问:
- 为什么
需要用onceToken
修饰?static
-
做了什么?dispatch_once
原因如下:
-
是为了保证static
唯一,不然你想想,你每次创建一个onceToken
,然后传入,内部根据onceToken = 0
的话就执行onceToken = 0
,那么不就乱套了?block
-
其实就是将dispatch_once
值进行修改,其默认是 执行完后变为onceToken
,然后每次进行判断这个值,为-1
就返回,同时做了一些编译期的优化-1
原理
intptr_t
其实就是一个
long
,所以
dispatch_once_t
就是一个
long
那么我们打印其值
+ (instancetype)shared {
static CCManager *manager = nil;
static dispatch_once_t onceToken;
NSLog(@"A %ld",onceToken);
dispatch_once(&onceToken, ^{
manager = [[super allocWithZone:NULL] init];
NSLog(@"B %ld",onceToken);
});
NSLog(@"C %ld",onceToken);
return manager;
}
2020-04-07 19:58:06.993385+0800 DispatchOnceTest[50656:1743273] A 0
2020-04-07 19:58:06.993597+0800 DispatchOnceTest[50656:1743273] B 768
2020-04-07 19:58:06.993822+0800 DispatchOnceTest[50656:1743273] C -1
这样子就明白其过程了,
0 -> 768(并不固定) -> -1
源码中是这样的
DISPATCH_INLINE DISPATCH_ALWAYS_INLINE DISPATCH_NONNULL_ALL DISPATCH_NOTHROW
DISPATCH_SWIFT3_UNAVAILABLE("Use lazily initialized globals instead")
void
_dispatch_once(dispatch_once_t *predicate,
DISPATCH_NOESCAPE dispatch_block_t block)
{
if (DISPATCH_EXPECT(*predicate, ~0l) != ~0l) {
dispatch_once(predicate, block);
} else {
dispatch_compiler_barrier();
}
DISPATCH_COMPILER_CAN_ASSUME(*predicate == ~0l);
}
DISPATCH_EXPECT
告诉编译器,这个值大概率为
-1
即
~0l
,告诉编译器优先读取下面的指令达到优化的效果。(因为初始化一次后,后面都是直接返回了)
这里
~0l
是将一个
长整型(long)
的
按位取反。
long一般占4字节
对于16进制表示则为
0xFFFFFFFF
0x0000000 16进制2位表示一个字节 0x00,转为二进制0000 0000然后取反1111 1111 即0xFF, 0x00 -> 0xFF
关于dispatch_compiler_barrier
#define dispatch_compiler_barrier() __asm__ __volatile__("" ::: "memory")
其实就是个空嵌入式汇编语句,其中
__asm__ __volatile__("" ::: "memory")
表示,强制
"memory"
假设
gcc编译器
所有内存单元均被
RAM
修改,这样
汇编指令
中的
cpu
和
registers
中已缓存的
cache
中的
内存单元
将作废。cpu将不得不在需要的时候重新读取内存中的数据。这就阻止了
数据
又将
cpu
中的数据用于去优化指令,而
registers,cache
。
避免去访问内存
如果你想了解其实现过程,可以看这篇文章 深入浅出 GCD 之 dispatch_once
性能测试
那么了解了原理,我们测试各种锁和
dispatch_once
的性能
extern uint64_t dispatch_benchmark(size_t count, void (^block)(void));
// pthread_mutex_lock
void dispatch_once_pthread(dispatch_once_t *predicate, dispatch_block_t block) {
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mutex);
if(!*predicate) {
block();
*predicate = 1;
}
pthread_mutex_unlock(&mutex);
}
// spinlock
void dispatch_once_spinlock(dispatch_once_t *predicate, dispatch_block_t block) {
static OSSpinLock lock = OS_SPINLOCK_INIT;
OSSpinLockLock(&lock);
if(!*predicate) {
block();
*predicate = 1;
}
OSSpinLockUnlock(&lock);
}
执行代码为
size_t count = 1000000;
// pthread_mutex_lock
uint64_t time1 = dispatch_benchmark(count, ^{
static dispatch_once_t onceToken;
dispatch_once_pthread(&onceToken, ^{ });
});
NSLog(@"dispatch_once_pthread = %llu ns", time1);
// spinlock
uint64_t time2 = dispatch_benchmark(count, ^{
static dispatch_once_t onceToken;
dispatch_once_spinlock(&onceToken, ^{ });
});
NSLog(@"dispatch_once_spinlock = %llu ns", time2);
// dispatch_once
uint64_t time3 = dispatch_benchmark(count, ^{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{ });
});
NSLog(@"dispatch_once = %llu ns", time3);
结果:
2020-04-07 20:48:32.944770+0800 DispatchOnceTest[51346:1777555] dispatch_once_pthread = 101 ns
2020-04-07 20:48:33.019640+0800 DispatchOnceTest[51346:1777555] dispatch_once_spinlock = 70 ns
2020-04-07 20:48:33.048097+0800 DispatchOnceTest[51346:1777555] dispatch_once = 25 ns
最后附上demo下载地址
链接:https://pan.baidu.com/s/1_Zar_4T1X0-6SV0iHZ04Bg 密码:pd96
对于
OSSpinlock
不再安全
由于iOS线程中的优先级翻转,当低优先级线程被锁住访问共享资源时,高优先级线程如果访问访问共享资源,由于高优先级线程始终会在低优先级线程前执行,它会处于 spin lock 的忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock。 见 博文地址