什么是锁
锁是保证线程安全的手段,保证临界区的代码同一时间只能有一个线程执行,使得类的行为和规范一致。
使用场景
我们常用锁来避免多线程引起的资源竞争等问题
definitions wikipedia
- 临界区:一块对公共资源进行访问的代码,并非一种机制或是算法
- 自旋锁(spin lock):
spin顾名思义,do while忙等待优点是避免了上下文调度开销 缺点是耗CPU 只适合短时间的任务 - 互斥锁(mutex lock) 休眠状态等锁
- 读写锁(rwlock): 处于“写锁”时任何操作都要休眠等锁
- 递归锁(recursive lock):是互斥锁的一个特例 允许同一个线程在未释放拥有的锁时反复对该锁进行加锁操作
- 信号量(semaphore) 互斥锁可以看成是semaphore在仅取值0/1时的特例。信号量更高级,可以实现更复杂的需求,不仅限于解决多线程同步问题。
- 条件锁 顾名思义 控制了锁的变量,只有满足条件时,锁才会打开
- 死锁 一个资源被多次调用,而多次调用方都未能释放该资源就会造成一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死 锁状态或系统产生了死锁。
iOS中常见的锁类型
1. @synchronized
### 解释
关键字加锁 是互斥锁的一种
### 特点
用法简洁、可读性强:传入对象即可,没有显示的添加锁、释放锁代码
### 使用
1
2
3
@synchronized(obj){
// do work
}
[杨肖玉的博客中](http://yulingtianxia.com/blog/2015/11/01/More-than-you-want-to-know-about-synchronized/)翻译了国外大神的文章并做了详细的解释,对下面的问题有很详细的解读
### 问题
1. 锁是如何传入@synchronized的对象关联上的?
2. 如果传入@synchronized的对象在@synchronized的block里面被释放或者被赋值为nil会怎样?
下面是阅读完博客后的笔记。
首先 [apple文档](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/ThreadSafety/ThreadSafety.html#//apple_ref/doc/uid/10000057i-CH8-SW3)中有一段话:
>As a precautionary measure, the @synchronized block implicitly adds an exception handler to the protected code. This handler automatically releases the mutex in the event that an exception is thrown. This means that in order to use the @synchronized directive, you must also enable Objective-C exception handling in your code. If you do not want the additional overhead caused by the implicit exception handler, you should consider using the lock classes.
>其中`the @synchronized block implicitly adds an exception handler to the protected code` 意思是@synchronized为代码块为隐式地添加了一个异常处理。
`@synchronized` block 会变成 `objc_sync_enter` 和 `objc_sync_exit` 的成对儿调用
可以理解成上面的代码转化成了下面这样:
1
2
3
4
5
6
7
8
9
10
@synchronized(obj){
// do work
}
@try {
objc_sync_enter(obj);
// do work
} @finally {
objc_sync_exit(obj);
}
### 带着问题看源码 [最新源码地址](https://opensource.apple.com/source/objc4/objc4-750/runtime/objc-sync.mm.auto.html)
`<objc/objc-sync.h>`中:
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
typedef struct alignas(CacheLineSize) SyncData {
struct SyncData* nextData;
DisguisedPtr<objc_object> object;
int32_t threadCount; // number of THREADS using this block
recursive_mutex_t mutex;
} SyncData;
typedef struct {
SyncData *data;
unsigned int lockCount; // number of times THIS THREAD locked this block
} SyncCacheItem;
typedef struct SyncCache {
unsigned int allocated;
unsigned int used;
SyncCacheItem list[0];
} SyncCache;
/*
Fast cache: two fixed pthread keys store a single SyncCacheItem.
This avoids malloc of the SyncCache for threads that only synchronize
a single object at a time.
SYNC_DATA_DIRECT_KEY == SyncCacheItem.data
SYNC_COUNT_DIRECT_KEY == SyncCacheItem.lockCount
*/
struct SyncList {
SyncData *data;
spinlock_t lock;
constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};
// Use multiple parallel lists to decrease contention among unrelated objects.
static StripedMap<SyncList> sDataLists;
`SyncList`可以看成是一个链表(key是对象地址的hash值),`SyncData`是链表中的某个节点。
`SyncData`包括了`object`(就是我们传入的对象)、`nextData`(链表中的下一个节点)、`recursive_mutex_t`类型(递归锁)的`mutex`(与之关联的一个互斥锁)、`threadCount`(被使用的线程的数量)
`objc_sync_enter`
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, ACQUIRE);
assert(data);
data->mutex.lock();
} else {
// @synchronized(nil) does nothing
if (DebugNilSync) {
_objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
}
objc_sync_nil();
}
return result;
}
1. 首先回答如果obj为nil的情况 :`@synchronized(nil) does nothing`
`objc_sync_nil` 什么也不做
2. obj不为空: 调用`id2data`取出obj对应的SyncData,判断之后进行加锁操作
`objc_sync_exit`
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int objc_sync_exit(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, RELEASE);
if (!data) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
} else {
bool okay = data->mutex.tryUnlock();
if (!okay) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
}
}
} else {
// @synchronized(nil) does nothing
}
return result;
}
1. 惯例回答 如果代码执行完是,obj为nil: `@synchronized(nil) does nothing`
2. obj不为空: 调用`id2data`取出obj对应的SyncData,判断之后进行解锁操作
那么`id2data`在传入了`obj` 加锁解锁时,函数内部具体做了什么呢?
`id2data`代码比较长,为了方便阅读,直接将笔记写在里面
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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
static SyncData* id2data(id object, enum usage why)
{
spinlock_t *lockp = &LOCK_FOR_OBJ(object);//lockp指向SyncList对象中的自旋锁
SyncData **listp = &LIST_FOR_OBJ(object);//listp指向SyncList链表
SyncData* result = NULL;
//对于同一个线程来说,有两种缓存方式:
//第一种:快速缓存(fastCache),适用于一个线程一次只对一个对象加锁的情况,用宏SUPPORT_DIRECT_THREAD_KEYS来标识
//这种情况意味着同一时间内,线程缓存中只有一个SyncCacheItem对象,键值SYNC_DATA_DIRECT_KEY和SYNC_COUNT_DIRECT_KEY分别对应SyncCacheItem结构体中的SyncData对象和lockCount.
// Check per-thread single-entry fast cache for matching object
// 用于标识当前线程的是否已使用fastCache
bool fastCacheOccupied = NO;
SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);//获取syncdata
if (data) {
fastCacheOccupied = YES;
//标识fastcache被使用
if (data->object == object) {
//判断fastcache中SyncData中的object与当前的对象是否一致
// Found a match in fast cache.
uintptr_t lockCount;
result = data;
lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);//获取当前线程中SyncData对象已经加锁的次数
if (result->threadCount <= 0 || lockCount <= 0) {
_objc_fatal("id2data fastcache is buggy");//对象发生错误
}
switch(why) {//判断当前的操作是加锁还是解锁
case ACQUIRE: {//加锁 然后更新
lockCount++;
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
break;
}
case RELEASE://解锁
lockCount--;
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
if (lockCount == 0) {//如果lockCount为零
// remove from fast cache
//将对应的SyncData对象从线程缓存中移除
tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
// atomic because may collide with concurrent ACQUIRE
//原子操作 确保线程安全
OSAtomicDecrement32Barrier(&result->threadCount);
//threadCount是多个线程共享的变量,用于记录对一个对象加锁的线程个数,threadCount对应的SyncData对象除了线程缓存中持有之外,还存在于全局哈希表sDataLists中,sDataLists哈希表是多个线程共享的数据结构,因此存在多线程访问的可能,因此需要加锁;而lockCount则与线程一一对应且存储在线程的缓存区中,不存在多线性读写问题,因此不需要加锁
}
break;
case CHECK:
// do nothing
break;
}
return result;
}
}
// Check per-thread cache of already-owned locks for matching object
// Check per-thread cache of already-owned locks for matching object
//这是第二种缓存方式:使用SyncCache结构体来维护一个SyncCacheItem数组,这样一个线程就可以处理对多个同步对象。值得注意的是SyncCache与线程也是一对一的关系。
SyncCache *cache = fetch_cache(NO);
//获取当前线程缓存区中的SyncCache对象
if (cache) {
unsigned int i;
//遍历SyncCache对象中的SyncCacheItem数组,匹配当前同步对象object
for (i = 0; i < cache->used; i++) {
SyncCacheItem *item = &cache->list[i];
if (item->data->object != object) continue;
// Found a match. 匹配到了
result = item->data;
if (result->threadCount <= 0 || item->lockCount <= 0) {
_objc_fatal("id2data cache is buggy");
}
switch(why) {
//同上fast-cache一样
case ACQUIRE:
item->lockCount++;
break;
case RELEASE:
item->lockCount--;
if (item->lockCount == 0) {
// remove from per-thread cache
cache->list[i] = cache->list[--cache->used];
// atomic because may collide with concurrent ACQUIRE
OSAtomicDecrement32Barrier(&result->threadCount);
}
break;
case CHECK:
// do nothing
break;
}
return result;
}
}
// Thread cache didn't find anything.
// Walk in-use list looking for matching object
// Spinlock prevents multiple threads from creating multiple
// locks for the same new object.
// We could keep the nodes in some hash table if we find that there are
// more than 20 or so distinct locks active, but we don't do that now.
//如果当前线程中的缓存中没有找到当前同步对象对应的SyncData对象,则在全局哈希表中查找
//因为全局哈希表是多个线程共享的数据结构,因此需要进行加锁处理
lockp->lock();
{
SyncData* p;
SyncData* firstUnused = NULL;
//遍历当前同步对象obejct在全局哈希表中的SyncData链表。这里之所以使用链表,是因为哈希表的hash算法不能确保hash的唯一性,存在多个对象对应一个hash值的情况。
for (p = *listp; p != NULL; p = p->nextData) {
if ( p->object == object ) {//匹配到了
result = p;
// atomic because may collide with concurrent RELEASE
OSAtomicIncrement32Barrier(&result->threadCount);//原子操作
goto done;
}
if ( (firstUnused == NULL) && (p->threadCount == 0) )//标记空闲对象
firstUnused = p;
}
// no SyncData currently associated with object
//由于此时同步对象object没有对应的SyncData对象,因此RELEASE与CHECK都属于无效操作
if ( (why == RELEASE) || (why == CHECK) )
goto done;
// an unused one was found, use it
//如果没有找到匹配的SyncData对象且存在空闲的SyncData对象,则直接使用,不需要创建新的SyncData,以提高效率。
if ( firstUnused != NULL ) {
result = firstUnused;
result->object = (objc_object *)object;
result->threadCount = 1;
goto done;
}
}
// Allocate a new SyncData and add to list.
// XXX allocating memory with a global lock held is bad practice,
// might be worth releasing the lock, allocating, and searching again.
// But since we never free these guys we won't be stuck in allocation very often.
//新建一个SyncData对象
posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
result->object = (objc_object *)object;
result->threadCount = 1;
new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
result->nextData = *listp;
*listp = result;
done:
//对全局哈希表的操作结束,解锁
lockp->unlock();
if (result) {
// Only new ACQUIRE should get here.
// All RELEASE and CHECK and recursive ACQUIRE are
// handled by the per-thread caches above.
if (why == RELEASE) {
// Probably some thread is incorrectly exiting
// while the object is held by another thread.
return nil;
}
if (why != ACQUIRE) _objc_fatal("id2data is buggy");
if (result->object != object) _objc_fatal("id2data is buggy");
if (!fastCacheOccupied) {
// Save in fast thread cache
//直接缓存新建的SyncData对象
tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
} else
{
// Save in thread cache
if (!cache) cache = fetch_cache(YES);
cache->list[cache->used].data = result;
cache->list[cache->used].lockCount = 1;
cache->used++;
}
}
return result;
}
2. dispatch_once
### 解释
dispatch_once 顾名思义 只执行一次,单例常用dispatch_once来保证某个单例
### 特点
用法简洁 适用于只执行一次任务的场景
### 使用
dispatch_once的用法:
1
2
3
4
static dispatch_once_t predicate;
dispatch_once(&predicate, ^{
// one time task
});
### 问题
Q1:dispatch_once是如何保证任务只执行一次的?
Q2:使用dispatch_once后,怎么做到重新初始化?
### 带着问题看源码
* [once.h](https://opensource.apple.com/source/libdispatch/libdispatch-913.60.2/dispatch/once.h.auto.html)
* [once.c](https://opensource.apple.com/source/libdispatch/libdispatch-913.60.2/src/once.c.auto.html)
#### dispatch_once参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*!
* @function dispatch_once
*
* @abstract
* Execute a block once and only once.
* block近执行一次
* @param predicate
* A pointer to a dispatch_once_t that is used to test whether the block has
* completed or not.
* predicate是一个dispatch_once_t类型的用来判断block是否执行完毕的指针
* @param block
* The block to execute once.
* block参数是执行的一次的任务
* @discussion
* Always call dispatch_once() before using or testing any variables that are
* initialized by the block.
*/
#### dispatch_once内部调用顺序
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
_dispatch_once(dispatch_once_t *predicate,
DISPATCH_NOESCAPE dispatch_block_t block)
{
if (DISPATCH_EXPECT(*predicate, ~0l) != ~0l) {//判断predicate是否为DISPATCH_ONCE_DONE 如果没有done
dispatch_once(predicate, block);
} else {
dispatch_compiler_barrier();//barrier???
}
DISPATCH_COMPILER_CAN_ASSUME(*predicate == ~0l);
}
dispatch_once(dispatch_once_t *val, dispatch_block_t block)
{
dispatch_once_f(val, block, _dispatch_Block_invoke(block));
}
dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func)
{
if (likely(os_atomic_load(val, acquire) == DLOCK_ONCE_DONE)) {
return;
}
return dispatch_once_f_slow(val, ctxt, func);
}
dispatch_once_f_slow(dispatch_once_t *val, void *ctxt, dispatch_function_t func)
{//重点
dispatch_once_gate_t l = (dispatch_once_gate_t)val;//将传进来的参数强制转换成dispatch_once_gate_t
if (_dispatch_once_gate_tryenter(l)) {
_dispatch_client_callout(ctxt, func);//执行block
_dispatch_once_gate_broadcast(l);//发广播
} else {
_dispatch_once_gate_wait(l);//等待
}
_dispatch_once_waiter_t volatile *vval = (_dispatch_once_waiter_t*)val;
struct _dispatch_once_waiter_s dow = { };
_dispatch_once_waiter_t tail = &dow, next, tmp;
dispatch_thread_event_t event;
if (os_atomic_cmpxchg(vval, NULL, tail, acquire)) {//os_atomic_cmpxchgv:原子操作 判断vval是否为NULL
dow.dow_thread = _dispatch_tid_self();
_dispatch_client_callout(ctxt, func);//执行block
next = (_dispatch_once_waiter_t)_dispatch_once_xchg_done(val);
while (next != tail) {
tmp = (_dispatch_once_waiter_t)_dispatch_wait_until(next->dow_next);
event = &next->dow_event;
next = tmp;
_dispatch_thread_event_signal(event);
}
} else {
_dispatch_thread_event_init(&dow.dow_event);
next = *vval;
for (;;) {//遍历每一个后续的请求
if (next == DISPATCH_ONCE_DONE) {//判断是否完成 如果完成则判断下一个
break;
}
//没有完成 将相应的请求添加到链表中
if (os_atomic_cmpxchgv(vval, next, tail, &next, release)) {
//os_atomic_cmpxchgv:原子操作 比较vval与next 如果相等,返回true 并设置vval=tail 不相等 返回false 并设置vval=next 由上可知 第一次进入该循环 应该返回true
dow.dow_thread = next->dow_thread;
dow.dow_next = next;
if (dow.dow_thread) {
pthread_priority_t pp = _dispatch_get_priority();
_dispatch_thread_override_start(dow.dow_thread, pp, val);
}
_dispatch_thread_event_wait(&dow.dow_event);
if (dow.dow_thread) {
_dispatch_thread_override_end(dow.dow_thread, val);
}
break;
}
}
_dispatch_thread_event_destroy(&dow.dow_event);
}
}
3. NSLock
解释
NSLock是Foundation框架中封装好的互斥锁,
### 使用 (伪代码)
1
2
3
4
....init yourlock...
[yourlock lock];//执行前锁住
...yourtask....
[yourloxk unlock];//完成后需要解锁
特点
问题
4. NSCondition
解释
条件锁
使用
特点
问题
5. NSConditionLock
解释
条件锁
特点
顾名思义 需要满足一定的条件才能开锁
使用
问题
6. NSRecursiveLock
解释
递归锁
特点
可以被同一个线程多次请求,而不会引起死锁,
使用
主要用在循环或递归操作中
问题
带着问题看源码
7. pthread_mutex
解释 c语言下的互斥锁
特点
使用
问题
带着问题看源码
8. dispatch_semaphore 信号量
解释
特点
只要信号量的value大于0 其他线程就可以wait成功
使用
问题
带着问题看源码
9. dispatch_async_barrier 读写锁
解释
特点
使用
问题
带着问题看源码
10. OSSpinLock
解释
自旋锁 在sideTable中有用到
特点
自旋锁的优势在于不需要切换上下文 用于耗时较短的任务
使用
问题
带着问题看源码
11. pthread_rwlock
解释
特点
使用
问题
带着问题看源码
12. POSIX Conditions
解释
特点
使用
问题
带着问题看源码
13. os_unfair_lock
解释
特点
使用
问题
带着问题看源码
总结
| 锁 | 特点(优势) | 说明 |
|---|---|---|
| @synchronized | ||
| dispatch_once | ||
| NSLock | ||
| NSCondition | ||
| NSConditionLock | ||
| NSRecursiveLock | ||
| pthread_mutex | ||
| dispatch_semaphore | ||
| dispatch_async_barrier | ||
| OSSpinLock | ||
| pthread_rwlock | ||
| POSIX Conditions | ||
| os_unfair_lock |