天天看点

iOS开发-dispatch_once相关使用场景原理性能测试

文章目录

  • 使用场景
  • 原理
    • 关于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;
}
           

这里有几个疑问:

  1. 为什么

    onceToken

    需要用

    static

    修饰?
  2. dispatch_once

    做了什么?

原因如下:

  1. static

    是为了保证

    onceToken

    唯一,不然你想想,你每次创建一个

    onceToken = 0

    ,然后传入,内部根据

    onceToken = 0

    的话就执行

    block

    ,那么不就乱套了?
  2. 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。 见 博文地址