0%

锁的原理

锁的分类

自旋锁

线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。

自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。在iOS中,声明属性,默认修饰符atomic,原子性操作自带一把自旋锁。

  • OSSpinLock (已经不安全,不在使用)
  • atomic

互斥锁

是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成

这里属于互斥锁的有:

  • NSLock
  • NSRecursiveLock
  • @synchronized
  • pthread_mutex
  • NSCondition
  • NSCondtionLock

自旋锁与互斥锁的区别

  1. 原理:
    1. 自旋锁:线程一直处于 加锁 - 解锁 - 忙等,消耗CPU资源较高。
    2. 互斥锁:线程处于 加锁 - 解锁 - 休眠(等待被唤醒)
  2. 如果共享数据已有其他线程加锁:
    • 自旋锁:死循环的方式等待,一旦被访问的资源被解锁,则立即执行。
    • 互斥锁:线程会进入休眠状态,等待解锁。
  3. 使用自旋锁应及时释放自旋锁,否则等待中的自旋锁会浪费CPU资源。

条件锁

就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行。

  • NSCondition
  • NSConditionLock

递归锁

就是同一个线程可以加锁N次而不会引发死锁。递归锁是特殊的互斥锁,一种带有递归性质的互斥锁

  • NSRecursiveLock
  • pthread_mutex(recursive)

信号量(semaphore)

是一种更高级的同步机制,互斥锁可以说是semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。它是一个互斥锁。

  • dispatch_semaphore

总结:

基本的锁包括两大类:自旋锁和互斥锁,其它锁都是在这两种的封装。

@synchronized 底层原理

我们把如下代码放在main.m文件中执行。

1
2
3
@synchronized (appDelegateClassName) {
// 进行读写操作
}

我们通过xcrun的命令生成main.cpp文件之后,才看文件可以到@synchronized的内部逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
id _sync_obj = (id)appDelegateClassName;
objc_sync_enter(_sync_obj);
try {
struct _SYNC_EXIT { _SYNC_EXIT(id arg) : sync_exit(arg) {}
~_SYNC_EXIT() {
objc_sync_exit(sync_exit);
}
id sync_exit;
}
_sync_exit(_sync_obj);

} catch (id e) {
_rethrow = e;
}

这里有两个操作,看着像是我们要找的重点内容objc_sync_enterobjc_sync_exit

为了以防万一,我们再汇编模式下debug一下代码。
在汇编模式下,同样发现了这两处代码。

1
2
3
libobjc.A.dylib`objc_sync_enter:

libobjc.A.dylib`objc_sync_exit:

并且这两处代码在libobjc.A.dylib中,这是个啥,感觉跟runtime的源码有点关系。是的,就是在runtime的源码中。接下来分析一下源码:

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

// Begin synchronizing on 'obj'.
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
// 1. obj有值
if (obj) {
// 2. 生成SyncData类型的data,这是重点,注意参数ACQUIRE
SyncData* data = id2data(obj, ACQUIRE);
ASSERT(data);
// 3. 加互斥锁
data->mutex.lock();
} else {
// 4.
// @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就是@synchronized (self)传进来的参数。有值的情况下会执行 2. 没有值则执行 4.
  2. SyncData是一个结构体
  3. 加锁
  4. 执行objc_sync_nil();直接返回

我们先看一下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
// End synchronizing on 'obj'. 
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
int result = OBJC_SYNC_SUCCESS;
// 1. 判断是否有obj
if (obj) {
// 获取data,注意传的值的参数是RELEASE
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;
}

我们发现objc_sync_exitobjc_sync_enter的内部大同小异。主要的内容都是在id2data函数内部。

接下来,我们重点看一下这个函数:

这个函数太大了,先把一些代码逻辑隐藏掉了,接着一步一步分析:

SyncData 链表

1
2
3
4
5
6
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;
  1. nextData指向下一个值,类似与链表结构。
  2. object:hash map的关联对象,就是@synchronized (self)中self这个参数。
  3. threadCount:有多少个线程执行了这个block
  4. mutex:这是一个递归锁,递归锁是互斥锁的一种

SyncList

1
2
spinlock_t *lockp = &LOCK_FOR_OBJ(object);
SyncData **listp = &LIST_FOR_OBJ(object);

首先出现的就是spinlock_t类型的锁,这是一个自旋锁,通过LOCK_FOR_OBJ(object)来获取。
SyncData指针类型的数据,通过LIST_FOR_OBJ获取。都是通过SyncList获取的

1
2
3
4
5
6
7
8
9
10
11
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.
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;

SyncList是一个结构体类型,内部有一个链表,spinlock_t是一个自旋锁。它存放的是一个总表。

tls:是一个线程缓存的表,通过set和get方法获取对应key的值。

  • SYNC_DATA_DIRECT_KEY: 数据data对应的key
  • SYNC_COUNT_DIRECT_KEY:锁的个数对应的key
1
2
3
4
5
6
7
8
9
10
typedef struct SyncCache {
unsigned int allocated;
unsigned int used;
SyncCacheItem list[0];
} SyncCache;

typedef struct {
SyncData *data;
unsigned int lockCount; // number of times THIS THREAD locked this block
} SyncCacheItem;

SyncCache也是一个结构体,主要作用是存储线程,内部有一个list数组,存储不同的线程。list[0]存放的是当前线程的SyncData链表。

第一步 有data

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
// Check per-thread single-entry fast cache for matching object
bool fastCacheOccupied = NO;
// 1. 通过get方法,从tls缓存表中获取data
SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
if (data) {
// 快速缓存查找 为YES
fastCacheOccupied = YES;
// 如果缓存中获取的data->object等于@synchronized(object),说明之前有使用过object
if (data->object == object) {
// Found a match in fast cache.
// 有几个锁,也就是有几次执行synchronized
uintptr_t lockCount;

result = data;
// 在快速缓存中
lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
// 表中中data,但是对应的线程数和锁的个数都小于0,说明获取的data有问题
if (result->threadCount <= 0 || lockCount <= 0) {
_objc_fatal("id2data fastcache is buggy");
}
// 还记得上层方法enter、exit调用这个方法传的参数吗?
switch(why) {
case ACQUIRE: {
// enter -- 锁的个数+1
lockCount++;
// 然后通过set方法存的tls表中
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
break;
}
case RELEASE:
// exit -- 锁的个数-1
lockCount--;
// 存放到tls表中
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
if (lockCount == 0) {
// 锁的个数等于 0 ,直接从表中移除
// remove from fast cache
tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
// atomic because may collide with concurrent ACQUIRE
OSAtomicDecrement32Barrier(&result->threadCount);
}
break;
case CHECK:
// do nothing
break;
}

return result;
}
}

第二步 cache

SyncCache还有印象吗?内部list[0]存放的是当前的SyncData

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
// Check per-thread cache of already-owned locks for matching object
// 从线程的缓存池里捞数据,先看看这个函数,
SyncCache *cache = fetch_cache(NO);
if (cache) {
unsigned int i;
for (i = 0; i < cache->used; i++) {
// list存放的是SyncCacheItem的数组,判断每一个item中对应的object是不是我们使用@synchronized传的参数。
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) {
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;
}
}

fetch_cache 函数

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
static SyncCache *fetch_cache(bool create)
{
_objc_pthread_data *data;

// 这个函数有解释,而且一目了然,没有从线程缓存池中拿到数据,并且参数create==NO,然后null,如果参数是YES,则创建一个。所以这里没有找到的话返回的是null,这个函数的主要作用就是获取cache
data = _objc_fetch_pthread_data(create);
if (!data) return NULL;

if (!data->syncCache) {
if (!create) {
return NULL;
} else {
int count = 4;
data->syncCache = (SyncCache *)
calloc(1, sizeof(SyncCache) + count*sizeof(SyncCacheItem));
data->syncCache->allocated = count;
}
}

// Make sure there's at least one open slot in the list.
if (data->syncCache->allocated == data->syncCache->used) {
data->syncCache->allocated *= 2;
data->syncCache = (SyncCache *)
realloc(data->syncCache, sizeof(SyncCache)
+ data->syncCache->allocated * sizeof(SyncCacheItem));
}

return data->syncCache;
}

我们先看一下_objc_fetch_pthread_data,这个方法已经说的很清楚了,有注释,代码都不用看。

其主要目的是从tls表中获取通过key获取_objc_pthread_data(结构体),内部有一个SyncCache,我们的最终目的也就是通过这个SyncCache中的list获取我们@synchronized(object)中object对应的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/***********************************************************************
* _objc_fetch_pthread_data
* Fetch objc's pthread data for this thread.
* If the data doesn't exist yet and create is NO, return NULL.
* If the data doesn't exist yet and create is YES, allocate and return it.
**********************************************************************/
_objc_pthread_data *_objc_fetch_pthread_data(bool create)
{
_objc_pthread_data *data;

data = (_objc_pthread_data *)tls_get(_objc_pthread_key);
if (!data && create) {
data = (_objc_pthread_data *)
calloc(1, sizeof(_objc_pthread_data));
tls_set(_objc_pthread_key, data);
}

return data;
}

第三步

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
// 首先执行的就上锁,这是一个自旋锁
lockp->lock();

{
SyncData* p;
SyncData* firstUnused = NULL;
// 执行for循环,这里的目的就是一直在遍历SyncData这个链表,看其中是否有数据
for (p = *listp; p != NULL; p = p->nextData) {
// 有数据,正好是我们用的那个。直接执行done语句
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
if ( (why == RELEASE) || (why == CHECK) )
goto done;

// an unused one was found, use it
if ( firstUnused != NULL ) {
// for循环中捞到的值赋给result
result = firstUnused;
// object指向@synchronized的参数
result->object = (objc_object *)object;
// 线程数是1
result->threadCount = 1;
goto done;
}
}

// 上面的流程也没有得到数据,哪就自己创建一个,并且加到线程池中。
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的操作。

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
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) {
// 啥都没有呢,就释放,返回nil
// 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 SUPPORT_DIRECT_THREAD_KEYS
// 走了第一步就会变成YES。
if (!fastCacheOccupied) {
// Save in fast thread cache
// 把SyncData、lockCount=1锁的个数存到tls表中,快速查找的表
tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
} else
#endif
{
// 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;
}

图上,左边就是线程list,每一个线程有一个链表。 先从当前的链表中查找,如果找到直接处理。没有找到则从线程中找到对应的链表再处理。

所以性能是最差的,内部有两个锁,一个自旋锁,一个递归锁,再加上快速查找,线程池缓存查找,等一系列操作,十分消耗性能,但是还是使用非常广泛,就是因为使用简单,不用自己加锁、解锁。

再看另外一个例子:

1
2
3
4
5
for (int i = 0; i < 100000; i ++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.mArray = [NSMutableArray array];
});
}

运行上面的代码,是否会发生crash?

会发生crash,就是因为点属性是一个set方法,在set方法内部,会对旧值relase,新值retain,在某一个节点,就有可能release的次数过多,造成重复释放,发生了野指针,就发生了crash。

最简单的做法就是使用@synchronized(self),那能不能锁@synchronized(_mArray)呢?

1
2
3
@synchronized (self) {
self.mArray = [NSMutableArray array];
}

答案是不能,因为在运行的过程中,_mArray会有等于nil的情况,那还能锁住啥?

@synchronized (self) 锁的对象一定要有声明周期,是可以释放的,并且block中执行的变量最好与锁住的对象有关系,两人存在声明周期上的关联。

但是也不要乱用self,当在不同线程执行不同的block时,@synchronized的查找会更加耗时,因为链表的查找需要从头开始找。这个时候就需要使用其他的锁来处理了。

总结

  • 锁的分类

    1. 自旋锁 :atomic
    2. 互斥锁 :@synchronized,NSLock,pthread
    3. 条件锁 :NSCondition,NSConditionLock
    4. 递归锁 :NSRecursiveLock
    5. 信号量 :dispatch_semaphore
    6. 主要分为两大类:自旋锁、互斥锁
  • @synchronized底层原理

    1. 通过objc_sync_enter加锁
    2. 通过objc_sync_exit解锁
    3. 内部有一个自旋锁、一个递归锁。自旋锁用于hash表中的查找,查找线程池。递归锁用于锁住的对象的操作。
    4. 查找分为3个步奏:
      1. 通过当前链表查找,找到了直接返回
      2. 当前链表没有则通过线程池缓存查找,
      3. 在hash表中查找
      4. 锁住的对象最好与block执行的变量有关联关系,不要乱用self

引用

objc源码