【iOS基础】多线程中的锁

236 阅读7分钟

基础

分类

  • 自旋锁

    - 发现其他线程执行,当前线程忙等,耗费性能较高

    - OSSpinLock

  • 互斥锁

    - 发现其他线程执行,当前线程休眠Runnable

    - 保证锁内的代码,同一时间,只有一条线程能执行

    - 互斥锁的锁定范围应该尽量小,锁定范围越大,效率越差

    - 常见锁

        - NSLock

        - pthread_mutex

        - @synchronized

  • 读写锁

    - 一种特殊的自旋锁。

    - 相对于自旋锁而言能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源(最大可能的读者数为实际的逻辑CPU数量)。写者是排他性的

    - 同时只能有一个写者或多个读者

    - 把对共享资源的访问者分为 读者 和 写者。

    - pthread_rwlock_t

  • 上层封装实现的其他锁分类

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

        - NSCondition

        - NSConditionLock

    - 递归锁:同一个线程可以加锁N次而不会引发死锁

        - NSRecursiveLock

        - pthread_mutex(recursive)

    - 信号量semaphore:互斥锁可以说是semaphore在仅取值0/1时的特例。

  • atomic

    - 原子属性,为多线程开发准备的,是默认属性

    - iOS 10之前,属性的atomic底层实现用的是自旋锁,仅仅在属性的setter方法中,增加了自旋锁【保证同一时间,只有一条线程对属性进行赋值操作】

    - iOS 10之后,属性的atomic底层实现用的是互斥锁

    - 在objc源码中可以在reallySetProperty方法找到对atomic的处理


    spinlock_t& slotlock = PropertyLocks[slot];

    slotlock.lock();

    oldValue = *slot;

    *slot = newValue;

    slotlock.unlock();

八大锁

  • OSSpinLock:自旋锁 iOS10及以后弃用

  • os_unfair_lock :互斥锁(iOS10开始替代OSSpinLock)

  • NSLock:互斥锁。基于pthread_mutex

  • NSCondition:互斥锁

  • NSConditionLock:互斥锁

  • dispatch_semaphore_t:信号量

  • pthread_mutex:互斥锁(可设置:递归)

  • NSRecursiveLock:递归锁

  • @synchronized:递归锁

Untitled

NSLock & NSRecursiveLock

  • 底层实现:pthread

  • NSLock

  • NSRecursiveLock

NSCondition

  • NSCondition的对象实际上作为一个和一个线程检查器

    - 锁:主要为了当检测条件时保护数据源,执行条件引发的任务

    - 线程检查器:主要是根据条件决定是否继续运行线程,即线程是否被阻塞

  • 常用API


// 一般用于多线程同时访问、修改同一个数据源,保证在同一时间内数据源只被访问、修改一次,其他线程的命令需要在lock外等待,直到unlock才可以访问
[condition lock];
// 配合lock使用
[condition unlock];
// 让当前线程处于等待状态
[condition wait];
// 让cpu发信号告诉线程不用再等待,可以继续执行
[condition signal];

NSConditionLock

  • NSConditionLock是锁,一旦一个线程获得锁,其他线程一定等待;condition就是整数,内部通过整数比较条件

  • 常用API


// 表示lock期待获得锁,如果没有其他线程获得锁(不需要判断内部的condition),那它能执行以下代码;如果已经有其他线程获得锁(条件锁/无条件锁),则等待,直到其它线程解锁

[lock lock];

// 如果没有其他线程获得锁,但是该锁内部的条件不为A,它依然不能获得锁,仍然等待

// 如果内部的条件为A,并没有其他线程获得该锁,则进入代码区,同时设置它获得该锁,其它任何线程都将等待代码完成,直到它解锁

[lock lockWithCondition:A条件];

// 表示释放锁,同时把内部的condition设置为A条件。【先更改当前的value值,然后进行广播】

[lock unlockWithCondition:A条件];

// 如果被锁定(没获得锁),并超过该时间则不再阻塞线程。【返回的值是NO,它没有改变锁的状态,这个函数的目的在于可以实现两种状态下的处理】

return = [lock lockWhenCondition:A条件

                beforeDate:指定条件];

示例分析


NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    [conditionLock lockWhenCondition:1];
    NSLog(@"线程 1");
    [conditionLock unlockWithCondition:0];
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
    [conditionLock lockWhenCondition:2];
    sleep(0.1);
    NSLog(@"线程 2");
    [conditionLock unlockWithCondition:1];

});

dispatch_async(dispatch_get_global_queue(0, 0), ^{
   [conditionLock lock];
   NSLog(@"线程 3");
   [conditionLock unlock];
});

输出为:3 -> 2 -> 1

  1. 线程1调用 [conditionLock lockWhenCondition:],此时因为不满足当前条件。所以进入waiting状态,释放当前的互斥锁

  2. 此时当前的线程3调用[conditionLock lock],本质上是调用[conditionLock lockBeforeDate:],这里不需要比对条件值,所以线程3会打印

  3. 线程2执行[conditionLock lockWhenCondition:],因为满足条件值,所以线程2会打印,打印完成后调用[conditionLock unlockWhenCondition:],这个时候将value设置为1,并发送broadcast,此时线程1接收到当前的信号,唤醒执行并打印。

递归锁 NSRecursiveLock

  • 保证同一线程下重复加锁

  • **在多线程环境下,递归调用会造成死锁,多线程在加锁和解锁中,会出现互相等待解锁的情况。 **

  • 与NSLock一样都是基于pthread_mutex_init实现,只是设置type为递归类型。

递归锁 @synchronized

使用

OC与Swift使用方式


/** objc */

@synchronized({obj}) {

    // action

}
/** swift */

// 注意enter和leave的obj必须是同一个

objc_sync_enter({obj})

// action

objc_sync_exit({obj})

加锁时,在缓存获取,不会重复创建。可以在多线程下递归调用。如性能方面要求不是非常高的话,使用该锁还更简便。

数据结构


// SyncData数据结构如下,可以看到是一个链表结构

typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;  // 链表,下一个结点
    DisguisedPtr<objc_object> object; // 关联到锁的对象
    int32_t threadCount;  // 使用该block的线程数量
    recursive_mutex_t mutex; // 互斥递归锁
} SyncData;


// SyncCache
typedef struct SyncCache {
    unsigned int allocated;
    unsigned int used;
    SyncCacheItem list[0];
} SyncCache;

typedef struct {
    SyncData *data;
    unsigned int lockCount;  // 当前线程锁定这个block的次数
} SyncCacheItem;

原理

recursive_mutex


#import <Foundation/Foundation.h>


@interface YKDemo : NSObject
@end


@implementation YKDemo
- (void)testFunc {
    @synchronized (self) {
        NSLog(@"Yakamoz");
    }
}
@end

将该文件通过clang -x objective-c -rewrite-objc main.m 转成c++代码后查看testFunc的实现

static void _I_YKDemo_testFunc(YKDemo * self, SEL _cmd) {
    { id _rethrow = 0; id _sync_obj = (id)self; 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);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders__v_wkpkp22d6ns5gwlzdyykl4040000gn_T_main_b95c39_mi_0);
        } catch (id e) {
            _rethrow = e;
        }
        ···
    }
}

可以看到有两部分

  1. objc_sync_enter(_sync_obj) + objc_sync_exit(sync_exit)

  2. _fin_force_rethow异常捕获和处理

在objc4源码中找到1的两个方法实现,在方法中可以看到若obj传空则无效,下面只截取重点代码探究

流程


// objc_sync_enter(id obj)

SyncData* data = id2data(obj, ACQUIRE);

ASSERT(data);

data->mutex.lock();

  1. 构建SyncData对象

  2. 对象内部的recursive_mutex_t加锁


// objc_sync_exit(id 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;

    }

}

  1. 构建SyncData对象

  2. 若对象为空,即代表没有该锁没有线程在使用,返回错误

  3. 若对象不为空,对象内部的recursive_mutex_t 解锁

id2data 获取同步信息

那么这两个方法是如何通过调用id2data()函数取得同步信息的呢?


// 1. 快速缓存。检查每条线程的单项快速缓存是否匹配对象,若匹配则返回该对象

switch(why) {

    case ACQUIRE: {

        // objc_sync_enter时,调用锁的数量+1

        ++syncLockCount;

        break;

    }

    case RELEASE:

        // objc_sync_exit时,调用锁的数量-1

        if (--syncLockCount == 0) {

            // 若-1后,没有线程再使用该锁,从快速缓存中移除

            syncData = nullptr;

            // 使获取到的结果的threadCount-1.

            // 使用atomic因为可能与并发ACQUIRE发生冲突

            AtomicDecrement(&result->threadCount);

        }

        break;

        ···

}

// 2. pthread线程缓存。检查已拥有锁的每条线程的缓存是否匹配对象,若匹配则返回该对象

// 2.1 获取pthread线程缓存SyncCache

// 2.2 遍历使用的线程,匹配object

SyncCacheItem *item = &cache->list[i];

// 2.3 若匹配到

switch(why) {

    case ACQUIRE:

// objc_sync_enter时,调用锁的数量+1

        item->lockCount++;

        break;

    case RELEASE:

// objc_sync_exit时,调用锁的数量-1

        item->lockCount--;

 

        if (item->lockCount == 0) {

            // 若-1后,没有线程再使用该锁,从pthread缓存中移除

            cache->list[i] = cache->list[--cache->used];

            // 同第1步中的操作

            AtomicDecrement(&result->threadCount);

        }

        break;

    ···

}

  


// 若线程缓存中都没找到,执行下述第3步

// spinlock_t对下述流程加锁,防止多个线程为同一个新对象创建多个锁。

  


SyncData* p;

SyncData* firstUnused = NULL;

// 3. 遍历全局列表static StripedMap<SyncList> sDataLists查找匹配的对象

// 3.1 遍历列表,若object匹配,使threadCount+1,跳出循环

  


// 3.3 若不匹配,使firstUnused指向p

// 3.4 若为objc_sync_exit,跳出循环

// 3.5 否则,将object传给该对象,并初始化threadCount为1,跳出循环

result = firstUnused;

result->object = (objc_object *)object;

result->threadCount = 1;

  


// 3.6 全局列表存储

···

// 4. 若快速缓存没有被占用,存储至快速缓存,否则存储至pthread缓存,便于下次查找

···

核心处理总结

  • 存储结构

    - 全局静态哈希表sDataLists

    - 元素为SyncList>SyncData

  • 存储方案

    - TLS

    - Cache

  • 加锁、解锁实现

    - OSAtomicIncrement32Barrier(&result->threadCount);

    - OSAtomicDecrement32Barrier(&result->threadCount);

  • 可重入、递归、多线程实现原因

    - TLS保障 threadCount标记多少条线程对这个锁对象加锁

    - lockCount标记进来了多少次