http://ios.jobbole.com/87349/
工欲善其事,必先利其器。
通常我们在实现单例时候都会使用synchronized或者dispatch_once方法,初始化往往是下面的样子:
使用synchronized方法实现:
1
2
3
4
5
6
7
8
9
10
|
static
id
obj
=
nil
;
+
(
instancetype
)
shareInstance
{
@synchronized
(
self
)
{
if
(
!
obj
)
{
obj
=
[
[
SingletonObj
alloc
]
init
]
;
}
}
return
obj
;
}
|
使用dispatch_once方法实现:
1
2
3
4
5
6
7
8
9
|
static
id
obj
=
nil
;
+
(
instancetype
)
shareInstance
{
static
dispatch_once_t
onceToken
;
dispatch_once
(
&
onceToken
,
^
{
obj
=
[
[
SingletonObj
alloc
]
init
]
;
}
)
;
return
obj
;
}
|
性能差异
上面的这些写法大家应该都很熟悉,既然两种方式都能实现,我们来看看两者的性能差异,这里简单写了个测试的demo,使用两个方法分单线程跟多线程(采用dispatch_apply方式,性能相对较高)去访问一个单例对象一百万次,对比这期间的耗时,从iPod跟5s测试得到如下的结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
//ipod,主线程
SingletonTest
[
4285
:
446820
]
synchronized
time
cost
:
2.202945s
SingletonTest
[
4285
:
446820
]
dispatch_once
time
cost
:
0.761034s
//5s,主线程
SingletonTest
[
5372
:
2394430
]
synchronized
time
cost
:
0.466293s
SingletonTest
[
5372
:
2394430
]
dispatch_once
time
cost
:
0.070822s
//ipod,多线程
SingletonTest
[
4315
:
448499
]
synchronized
time
cost
:
3.385109s
SingletonTest
[
4315
:
448499
]
dispatch_once
time
cost
:
0.908009s
//5s,多线程
SingletonTest
[
5391
:
2399069
]
synchronized
time
cost
:
0.507504s
SingletonTest
[
5391
:
2399069
]
dispatch_once
time
cost
:
0.169934s
|
可以发现dispatch_once方法的性能要明显优于synchronized方法(多线程不采用dispathc_apply方式差距更明显),所以在实际的应用中我们可以多采用dispatch_once方式来实现单例。通常使用的时候了解这些就够了,不过想知道两者的具体差异就需要我们再迈进一步。
深入@synchronized(object)
翻看苹果的文档可以发现 @synchronized指令内部使用锁来实现多线程的安全访问,并且隐式添加了一个异常处理的handler,当异常发生时会自动释放锁。在stackoverflow上看到@synchronized指令其实可以转换成objc_sync_enter跟objc_sync_exit,可以在头文件中找到这两个函数:
1
2
3
4
5
|
//Allocates recursive pthread_mutex associated with 'obj' if needed
int
objc_sync_enter
(
id
obj
)
//End synchronizing on 'obj'
int
objc_sync_exit
(
id
obj
)
|
根据注释文档,objc_sync_enter会根据需要给每个传进来的对象创建一个互斥锁并lock,然后objc_sync_exit的时候unlock,这样就可以通过这个锁来实现多线程的安全访问,所以结合苹果文档可以认为
1
2
3
|
@synchronized
(
self
)
{
//thread safe code
}
|
等价于
1
2
3
4
5
6
|
<
a
href
=
'http://www.jobbole.com/members/xyz937134366'
>
@try
<
/
a
>
{
objc_sync_enter
(
self
)
;
// thread safe code
}
<
a
href
=
'http://www.jobbole.com/members/finally'
>
@finally
<
/
a
>
{
objc_sync_exit
(
self
)
;
}
|
庆幸的是苹果已经将objc-runtime这部分开源,所以我们可以更进一步了解内部的实现,源码在这里,有兴趣也可以自己去查阅,这里简单介绍一下。
让我们先来看看几个数据结构,其中有些涉及到缓存,我们就不去考虑了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
typedef
struct
SyncData
{
struct
SyncData
*
nextData
;
DisguisedPtr
object
;
int32_t
threadCount
;
// number of THREADS using this block
recursive_mutex_t
mutex
;
}
SyncData
;
struct
SyncList
{
SyncData
*data
;
spinlock_t
lock
;
}
;
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static
StripedMap
sDataLists
;
|
首先看看SyncData这个数据结构,包含一个指向object的指针,这个object对象就是我们@synchronized时传进来的对象,也包含一个跟object关联的递归互斥锁recursive_mutex_t,该锁用来互斥访问object对象;同时还包含一个指向下一个SyncData的指针nextData,可以看出SyncData是一个链表中的节点;至于threadCount,这个值标示有几个线程正在访问这个对象,当threadCount==0的时候,会重用该SyncData对象,这是为了节省内存。
接下来看看SyncList,SyncList其实就是一个链表,data指向第一个SyncData节点,lock则是为了多线程安全访问该链表。
最后看下sDataLists静态哈希表对象,它以obj的指针为key,对应的value为SyncList链表。
了解上面之后,我们就可以看看objc_sync_enter跟objc_sync_exit的具体实现(摘取部分代码)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
|
//根据object对象去查询相应的SyncData对象,如果没有则创建一个新的
static
SyncData
*
id2data
(
id
object
,
enum
usage
why
)
{
spinlock_t
*lockp
=
&
LOCK_FOR_OBJ
(
object
)
;
SyncData
*
*listp
=
&
LIST_FOR_OBJ
(
object
)
;
SyncData
*
result
=
NULL
;
//lock,多线程安全访问SyncList
lockp
->
lock
(
)
;
{
SyncData
*
p
;
SyncData
*
firstUnused
=
NULL
;
for
(
p
=
*listp
;
p
!=
NULL
;
p
=
p
->
nextData
)
{
//找到object对象对应的SyncData对象,增加其threadCount计数,然后返回
if
(
p
->
object
==
object
)
{
result
=
p
;
OSAtomicIncrement32Barrier
(
&
result
->
threadCount
)
;
goto
done
;
}
//当threadCount == 0时,设置当前SyncData为可重用
if
(
(
firstUnused
==
NULL
)
&&
(
p
->
threadCount
==
0
)
)
firstUnused
=
p
;
}
// 如果有可重用的节点,则使用当前SyncData节点,SyncData的object指针指向新的object对象
if
(
firstUnused
!=
NULL
)
{
result
=
firstUnused
;
result
->
object
=
(
objc_object
*
)
object
;
result
->
threadCount
=
1
;
goto
done
;
}
}
//如果没有可重用的节点,则创建一个新的SyncData节点
result
=
(
SyncData
*
)
calloc
(
sizeof
(
SyncData
)
,
1
)
;
//将新的SyncData节点的object指针指向传进来的object对象
result
->
object
=
(
objc_object
*
)
object
;
result
->
threadCount
=
1
;
//创建一个新的与该object关联的递归互斥锁
new
(
&
result
->
mutex
)
recursive_mutex_t
(
)
;
result
->
nextData
=
*listp
;
*listp
=
result
;
done
:
lockp
->
unlock
(
)
;
return
result
;
}
int
objc_sync_enter
(
id
obj
)
{
int
result
=
OBJC_SYNC_SUCCESS
;
if
(
obj
)
{
//根据obj指针的哈希值查找对应的SyncData,threadcount计数加一
SyncData
*
data
=
id2data
(
obj
,
ACQUIRE
)
;
//使用SyncData的互斥锁上锁
data
->
mutex
.
lock
(
)
;
}
else
{
// @synchronized(nil) 传入nil时什么也不处理
}
return
result
;
}
int
objc_sync_exit
(
id
obj
)
{
int
result
=
OBJC_SYNC_SUCCESS
;
if
(
obj
)
{
//根据obj指针的哈希值查找对应的SyncData,threadcount计数减一
SyncData
*
data
=
id2data
(
obj
,
RELEASE
)
;
//使用SyncData的互斥锁解锁
bool
okay
=
data
->
mutex
.
tryUnlock
(
)
;
if
(
!
okay
)
{
result
=
OBJC_SYNC_NOT_OWNING_THREAD_ERROR
;
}
}
else
{
// @synchronized(nil) 传入nil时什么也不处理
}
return
result
;
}
|
简单来说,调用objc_sync_enter(obj)时,会根据obj指针在哈希表sDataLists对应的链表SyncList,然后在链表中查询对应obj的SyncData对象,如果查询不到则创建一个新的SyncData对象(包含创建跟obj相关的递归互斥锁)并添加到链表中,然后使用SyncData对象上锁;调用objc_sync_exit(obj)时,使用SyncData对象解锁,因此通过这个锁便可确保@synchronized之间的代码线程安全。
深入dispatch_once
探讨了synchronized之后,我们再来说说dispatch_once。
1
|
void
dispatch_once
(
dispatch_once_t
*predicate
,
dispatch_block_t
block
)
;
|
根据官方文档,dispatch_once可以用来初始化一些全局的数据,它能够确保block代码在app的生命周期内仅被运行一次,而且还是线程安全的,不需要额外加锁;predicate必须指向一个全局或者静态的变量,不过使用predicate的话结果是未定义的,不过predicate有啥作用,如何实现block在整个生命周期执行一次?那我们只能从源码查找(源码地址:once)。
不过在这之前先简要介绍一下:
- bool __sync_bool_compare_and_swap (type ptr, type oldval type newval, …)
提供原子的比较和交换操作,如果当前值ptr == oldval,就将newval写入*ptr,当比较赋值操作成功后返回true - *__sync_synchronize (…)
调用这个函数会产生一个full memory barrier ,用于保证CPU按照我们代码编写的顺序来执行代码,比如:
1234doJob1 ( ) ;doJob2 ( ) ;__sync_synchronize ( ) ; //Job3会在Job1跟Job2完成后才执行doJob3 ( ) ; - type __sync_swap(type *ptr, type value, …)
提供原子交换操作的函数,交换第一个跟第二个参数的值,然后返回交换前第一个参数的旧值。 - _dispatch_hardware_pause()
调用这个函数主要是暗示处理器不要做额外的优化处理等,提高性能,节省CPU时间,可以查看这里了解更多- 信号量
信号量是一个非负整数,定义了两种原子操作:wait跟signal来进行访,信号量主要用于线程同步。当一个线程调用wait操作时,如果信号量的值大于0,则获得资源并将信号量值减一,如果等于0线程睡眠直到信号量值大于0或者超时;singal将信号量的值加1,如果这时候有正在等待的线程,唤醒该线程。
12345678// 创建一个信号量,其值为0dispatch_semaphore_t sema = dispatch_semaphore_create ( 0 ) ;ABAddressBookRequestAccessWithCompletion ( addressBook , ^ ( bool granted , CFErrorRef error ) {//操作完成后,调用signal信号量+1dispatch_semaphore_signal ( sema ) ;} ) ;//等待dispatch_semaphore_signal将信号量值加1后才继续运行dispatch_semaphore_wait ( sema , DISPATCH_TIME_FOREVER ) ;接下来看看具体代码,当我们调用dispatch_once时候,内部是调用dispatch_once_f函数,其中val就是外部传入的predicate值,ctxt为Block的指针,func则是Block内部具体实现的函数指针,由于源码比较短,所以我直接把源码贴出来(为了方便查看,有些不使用宏定义)。
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576struct _dispatch_once_waiter _ s {volatile struct _dispatch_once_waiter_s *volatile dow_next ;_dispatch_thread_semaphore_t dow_sema ;} ;#define DISPATCH_ONCE_DONE ((struct _dispatch_once_waiter_s *)~0l)void dispatch_once ( dispatch_once_t *val , dispatch_block_t block ){struct Block_basic *bb = ( void * ) block ;dispatch_once_f ( val , block , ( void * ) bb -> Block_invoke ) ;}void dispatch_once_f ( dispatch_once_t *val , void *ctxt , dispatch_function_t func ){//volatile,标示该变量随时可能改变,编译器不会对访问该变量的代码进行优化,每次都从内存去读取,而不使用寄存器里的值struct _dispatch_once_waiter_s * volatile *vval =( struct _dispatch_once_waiter_s * * ) val ;struct _dispatch_once_waiter_s dow = { NULL , 0 } ;struct _dispatch_once_waiter_s *tail , *tmp ;_dispatch_thread_semaphore_t sema ;//第一次执行的时候,predicate的值为0,所以vval=NULL,原子比较交换函数返回true//然后vval指向dow(dispatch_once_waiter_s,信号量的值为0,即等待中)if ( __sync_bool_compare_and_swap ( vval , NULL , & dow ) ) {//空的宏定义,啥也不做dispatch_atomic_acquire_barrier ( ) ;//执行dispatch_once传进来的block_dispatch_client_callout ( ctxt , func ) ;//后面解释dispatch_atomic_maximally_synchronizing_barrier ( ) ;//执行完block之后,将vval的值设为DISPATCH_ONCE_DONE(即predicate设为~0l)tmp = __sync_swap ( vval , DISPATCH_ONCE_DONE ) ;tail = & dow ;//1.如果在block的执行过程中,没有其线程调用该函数等待,tmp的值也为&dow,tail==tmp,循环的条件不满足,函数执行完毕//2.如果在block的执行过程中,有其线程调用该函数等待,历遍信号量链表,逐个唤醒线程继续运行while ( tail != tmp ) {//如果中途有其它线程将vval赋值&dow,这期间dow_next值为NULL,需要等待,参见else分支的__sync_bool_compare_and_swap调用while ( ! tmp -> dow_next ) {_dispatch_hardware_pause ( ) ;}sema = tmp -> dow_sema ;tmp = ( struct _dispatch_once_waiter_s * ) tmp -> dow_next ;_dispatch_thread_semaphore_signal ( sema ) ;}}else{//如果vval不等NULL,走这个分支,非第一次调用dispatch_once,其它线程调用//获取信号量,如果有信号量则返回该信号量,如果没有则在当前线程创建一个新的信号量dow . dow_sema = _dispatch_get_thread_semaphore ( ) ;for ( ; ; ) {tmp = *vval ;//vval已经被赋值为~0l,证明block已经被执行了,退出然后调用_dispatch_put_thread_semaphore销毁信号量if ( tmp == DISPATCH_ONCE_DONE ) {break ;}//空的宏定义,啥也不做dispatch_atomic_store_barrier ( ) ;//将当前信号量加入到信号链表中,然后线程等待,if ( __sync_bool_compare_and_swap ( vval , tmp , & dow ) ) {dow . dow_next = tmp ;_dispatch_thread_semaphore_wait ( dow . dow_sema ) ;}//如果vval的指向值不再是tmp,可能其它线程同时进入该分支,然后调用__sync_bool_compare_and_swap原子操作将vval指向了新的节点,//则重新开始for循环}_dispatch_put_thread_semaphore ( dow . dow_sema ) ;}}让我们来看看dispatch_once是如何确保block只执行一次。简单来说,当线程A在调用执行block并设置predicate为DISPATCH_ONCE_DONE(~0l)期间,如果有其他线程也在调用disptach_once,则这些线程会等待,各线程对应的信号量会加入到信号量链表中,等predicate设置为DISPATCH_ONCE_DONE后,也就是block执行完了,会根据信号量链表唤醒各个线程使其继续执行。
信号量链表.png不过有一种临界情况,假如线程A在执行block,但是创建单例对象obj还未完成,这时候线程B获取该obj对象,此时obj=nil,而线程B在线程A将predicate设为DISPATCH_ONCE_DONE之后读取predicate,这是线程B会认为单例对象已经初始化完成,然后使用空的obj对象,这就会导致错误发生。因此dispatch_once会在执行完block之后会执行dispatch_atomic_maximally_synchronizing_barrier()调用,这个调用会执行一些cpuid指令,确保线程A创建单例对象obj以及置predicate为DISPATCH_ONCE_DONE的时间TimeA大于线程B进入block并读取predicate值的时间TimeB。
12345#define dispatch_atomic_maximally_synchronizing_barrier() \do { unsigned long _clbr ; __asm__ __volatile__ ( \"cpuid" \: "=a" ( _clbr ) : "0" ( 0 ) : "ebx" , "ecx" , "edx" , "cc" , "memory" \) ; } while ( 0 )除此之外,每次调用dispatch_once的时候,都会先判断predicate的值是否是~0l(也就是DISPATCH_ONCE_DONE),如果是则意味着block已经执行过了,便不再执行,代码如下:
1234void dispatch_once ( dispatch_once_t *predicate , dispatch_block_t block ) ;#ifdef __GNUC__#define dispatch_once(x, ...) do { if (__builtin_expect(*(x), ~0l) != ~0l) dispatch_once((x), (__VA_ARGS__)); } while (0)#endif让我们看看这里面的__builtin_expect((x), (v)),这又是一个优化的地方。。。
1234__builtin_expect ( )目的是将“分支转移”的信息提供给编译器,这样编译器可以对代码进行优化,以减少指令跳转带来的性能下降。__builtin_expect ( ( x ) , 1 ) 表示 x 的值为真的可能性更大;__builtin_expect ( ( x ) , 0 ) 表示 x 的值为假的可能性更大。由于dispatch_once的只执行block一次,所以我们更期望的是已经block已经执行完了,也就是predict的值为~0l的可能性更大。
现在我们清楚dispatch_once是如何确保block只执行一次了,关键就在predict这个值,通过比较这个值等于0或者~0l来判断block是否执行过,这也就是为啥我们需要将这个值设为static或者全局的缘故,因为各个线程都要去访问这个predict,有兴趣的可以试试把predicate的初始值设为非0或者非静态全局变量会发生什么~~总结
通过上面的分析,我们知道
参考
objc-sync
synchronized
dispatch_once
Built-in functions for atomic memory access
__builtin_expect - 信号量
-
@synchronized采用的是递归互斥锁来实现线程安全,而dispatch_once的内部则使用了很多原子操作来替代锁,以及通过信号量来实现线程同步,而且有很多针对处理器优化的地方,甚至在if判断语句上也做了优化(逼格有点高),使得其效率有很大的提升,虽然其源码很短,但里面包含的东西却很多,所以苹果也推荐使用dispatch_once来创建单例。通过这个简短的dispatch_once,你也可以清楚为什么GCD的性能会这么高了,感兴趣可以再去看看libdispatch的其它源码。。