文章

线程安全中的各种锁


注意

除非开发者能保证访问锁的线程全部都处于同一优先级,否则 iOS 系统中所有类型的自旋锁都别再用。


iOS 加锁的几种方式

1、synchronized

1
2
3
4
// 性能最差,敲以下代码时 Xcode 没有提示,是否可以理解为 Apple 不建议使用这种加锁方式 ?
@synchronized (对象,一般用self) {
    // ......
}


2、NSLock

1
2
3
[self.lock lock];
    // ......
[self.lock unlock];


3、NSCondition

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
// NSCondition 实现了`NSLocking`协议,同样具有锁的功能,与`NSLock`一样可以加锁、解锁

NS_CLASS_AVAILABLE(10_5, 2_0)
@interface NSCondition : NSObject <NSLocking> {

/*
阻塞线程,直到其他线程调用该对象的 signal 方法或 broadcast 方法来唤醒
唤醒后该线程从阻塞态变为就绪态,交由系统进行线程调度
调用 wait 方法时内部会自动执行 unlock 方法释放锁,并阻塞线程
*/
- (void)wait;

// 同上,只是该方法是在 limit 到达时唤醒线程
- (BOOL)waitUntilDate:(NSDate *)limit;

// 随机唤醒一个在当前 NSCondition 对象上阻塞的一个线程,使其从阻塞态进入就绪态
- (void)signal;

// 唤醒在当前 NSCondition 对象上阻塞的所有线程
- (void)broadcast;

// 设置名称
@property (nullable, copy) NSString *name;

@end




// ===============================使用事例==================================




// 取钱
- (void)draw:(id)money {
    // 设置消费者取钱20次
    NSUInteger count = 0;
    
    while (count < 20) {
        // 首先使用condition上锁,如果其他线程已经上锁则阻塞
        [self.condition lock];
        
        // 判断是否有钱
        if (self.haveMoney) {
            // 有钱则进行取钱的操作,并设置haveMoney为NO
            self.balance -= [money doubleValue];
            self.haveMoney = NO;
            count += 1;
            NSLog(@"%@ draw money %lf %lf", [[NSThread currentThread] name], [money doubleValue], self.balance);
            
            // 取钱操作完成后唤醒其他在此condition上等待的所有线程
            [self.condition broadcast];
        } else {
            // 如果没有钱则在此condition上等待,并阻塞
            [self.condition wait];
            // 如果阻塞的线程被唤醒后会继续执行代码
            NSLog(@"%@ wake up", [[NSThread currentThread] name]);
        }
        
        // 释放锁
        [self.condition unlock];
    }
}

// 存钱
- (void)deposite:(id)money {
    // 创建了三个取钱线程,每个取钱20次,则存钱60次
    NSUInteger count = 0;
    
    while (count < 60) {
    
        // 上锁,如果其他线程上锁了则阻塞
        [self.condition lock];
        
        // 判断如果没有钱则进行存钱操作
        if (!self.haveMoney) {
            // 进行存钱操作,并设置 haveMoney 为 YES
            self.balance += [money doubleValue];
            self.haveMoney = YES;
            count += 1;
            NSLog(@"Deposite money %lf %lf", [money doubleValue], self.balance);
            
            // 唤醒其他所有在condition上等待的线程
            [self.condition broadcast];
        } else {
            // 如果有钱则等待
            [self.condition wait];
            NSLog(@"Deposite Thread wake up");
        }
        
        // 释放锁
        [self.condition unlock];
    }
}

- (void)useNSCondition {
    
    Account *account = [[Account alloc] init];
    account.accountNumber = @"1603121434";
    account.balance = 0;
    
    // 消费者线程1,每次取1000元
    NSThread *thread1 = [[NSThread alloc] initWithTarget:account selector:@selector(draw:) object:@(1000)];
    [thread1 setName:@"consumer1"];
    
    // 消费者线程2,每次取1000元
    NSThread *thread2 = [[NSThread alloc] initWithTarget:account selector:@selector(draw:) object:@(1000)];
    [thread2 setName:@"consumer2"];
    
    // 生产者线程3,每次存1000元
    NSThread *thread3 = [[NSThread alloc] initWithTarget:account selector:@selector(deposite:) object:@(1000)];
    [thread3 setName:@"productor"];
    
    [thread1 start];
    [thread2 start];
    [thread3 start];
}


4、NSConditionLock

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
// NSConditionLock 条件锁
// 其中的 condition 参数,可以理解为一个条件标识,只有传入的 condition 和锁的标识相同才会加锁成功,否则阻塞线程。

NSConditionLock *cLock = [[NSConditionLock alloc] initWithCondition:0];

// 任务1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    if ([cLock tryLockWhenCondition:0]) {
        NSLog(@"加锁成功---任务1");
       [cLock unlockWithCondition:1];
    } else {
        NSLog(@"加锁失败---任务1");
    }
});

// 任务2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [cLock lockWhenCondition:3];
    NSLog(@"加锁成功---任务2");
    [cLock unlockWithCondition:2];
});

// 任务3
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [cLock lockWhenCondition:1];
    NSLog(@"加锁成功---任务3");
    [cLock unlockWithCondition:3];
});

打印顺序: 任务1  任务3  任务2


我们在初始化 NSConditionLock 对象时,给了他的标示为 0
执行 tryLockWhenCondition: 时,我们传入的条件标示也是 0,所以 任务1 加锁成功
执行 unlockWithCondition: 时,这时候会把 condition  0 修改为 1
接着会走到 任务3,然后 任务3 又将 condition 修改为 3,最后走 任务2 的流程
从上面的结果我们可以发现,NSConditionLock 还可以实现任务之间的依赖。


5、NSRecursiveLock 递归锁

递归锁可以被同一个线程多次获取而不会导致死锁。但是所有其他线程都无法访问由锁保护的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 此处如果用 NSLock 会死锁
// NSLock *rLock = [NSLock new];

NSRecursiveLock *rLock = [NSRecursiveLock new];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    static void (^RecursiveBlock)(NSInteger);
    RecursiveBlock = ^(NSInteger value) {
        [rLock lock];
        if (value > 0) {
            NSLog(@"线程%d", value);
            RecursiveBlock(value - 1);
        }
        [rLock unlock];
    };
    RecursiveBlock(4);
});


死锁情况:
如果用 NSLock,在线程中 RecursiveMethod 是递归调用,每次进入 block 时,都会加一次锁,
而从第二次开始,由于锁已经被使用,所以它需要等待锁被解除,这就导致死锁,线程被阻塞。
需要将 NSLock 替换为 NSRecursiveLock


6、pthread_mutex 互斥锁

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
// ==================================================================

pthread_mutex_t _mutex;
pthread_cond_t _cond;

// 用法类似 NSCondition
pthread_cond_broadcast(&_cond);
pthread_cond_signal(&_cond);
pthread_cond_wait(&_cond, &_mutex);
    
    
// 普通锁
pthread_mutex_init(&_mutex, NULL));

// 递归锁
pthread_mutexattr_t attr;
pthread_mutexattr_init (&attr);
pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init (&_mutex, &attr);
pthread_mutexattr_destroy (&attr);
   
   
   
#define PTHREAD_MUTEX_NORMAL		0
#define PTHREAD_MUTEX_ERRORCHECK	1
#define PTHREAD_MUTEX_RECURSIVE		2
#define PTHREAD_MUTEX_DEFAULT		PTHREAD_MUTEX_NORMAL
    
    
    
    
// 在 dealloc 方法中需要销毁
pthread_mutex_destroy(&_mutex)




// ==================================================================




static pthread_mutex_t pLock;
pthread_mutex_init(&pLock, NULL);

// 任务1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"任务1 准备上锁");
    pthread_mutex_lock(&pLock);
    sleep(3);
    NSLog(@"任务1");
    pthread_mutex_unlock(&pLock);
});

// 任务2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"任务2 准备上锁");
    pthread_mutex_lock(&pLock);
    NSLog(@"任务2");
    pthread_mutex_unlock(&pLock);
});


// pthread_mutex 中有个 pthread_mutex_trylock(&pLock) 和 OSSpinLockTry(&oslock) 的
// 区别在于,前者可以加锁时返回的是 0,否则返回一个错误提示码;后者返回的 YES 和 NO


7、信号量机制

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
// 信号量机制
// 初始化信号量,值要 >= 0,如果 < 0,则返回 NULL
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

// 可以是具体的等待时间,或者 DISPATCH_TIME_FOREVER
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 3.0f * NSEC_PER_SEC);

// 线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"线程1 等待ing");
    
    // 信号等待: 如果 semaphore = 0,则阻塞当前线程;在 > 0 时 semaphore - 1 并返回
    dispatch_semaphore_wait(semaphore, timeout);
    
    NSLog(@"线程1 正在执行");
    
    // 信号通知: semaphore + 1 并返回
    dispatch_semaphore_signal(semaphore);
    NSLog(@"线程1 发送信号");
});

// 线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"线程2 等待ing");
    dispatch_semaphore_wait(semaphore, timeout);
    NSLog(@"线程2 正在执行 ");
    dispatch_semaphore_signal(semaphore);
    NSLog(@"线程2 发送信号");
});


8、pthread_rwlock_t 读写锁

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
- (void)useRWLock
{
    // pthread_rwlock_t 读写锁
    // 当读写锁被一个线程以读模式占用时,写操作的其他线程会被阻塞,读操作的其他线程还可继续进行。
    // 当读写锁被一个线程以写模式占用时,写操作的其他线程会被阻塞,读操作的其他线程也被阻塞。
    
    // 初始化
    pthread_rwlock_t rwLock = PTHREAD_RWLOCK_INITIALIZER;
    // 读模式
    pthread_rwlock_wrlock(&rwLock);
    // 写模式
    pthread_rwlock_rdlock(&rwLock);
    // 读模式或者写模式的解锁
    pthread_rwlock_unlock(&rwLock);
    
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self readBookWithTag:1];
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self readBookWithTag:2];
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self writeBook:3];
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self writeBook:4];
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self readBookWithTag:5];
    });
}

- (void)readBookWithTag:(NSInteger )tag {
    pthread_rwlock_rdlock(&rwLock);
    NSLog(@"start read ---- %ld",tag);
    
    // 读...
    
    NSLog(@"end   read ---- %ld",tag);
    pthread_rwlock_unlock(&rwLock);
}

- (void)writeBook:(NSInteger)tag {
    pthread_rwlock_wrlock(&rwLock);
    NSLog(@"start wirte ---- %ld",tag);
    
    // 写...
    
    NSLog(@"end   wirte ---- %ld",tag);
    pthread_rwlock_unlock(&rwLock);
}


8、os_unfair_lock 苹果官方建议的用来替代 OSSpinLock 的锁

1
2
3
4
5
6
7
8
9
// 创建锁,并初始化
os_unfair_lock_t unfairLock;
unfairLock = &(OS_UNFAIR_LOCK_INIT);

// 加锁
os_unfair_lock_lock(unfairLock);

// 解锁
os_unfair_lock_unlock(unfairLock);


9、OSSpinLock 自旋锁(性能最好,但某些场景会有问题,已经被苹果废弃)

  • 自旋锁的特点是当有其他线程加锁后,当前线程会循环等待,并不会进入睡眠,所以性能最好。
  • 若几个线程的优先级不同,则可能出现优先级翻转的问题。所以 OSSpinLock 已经被苹果废弃。
  • 除非开发者能保证访问锁的线程优先级都相同,否则 iOS 系统中所有类型的自旋锁都别再用。
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
- (void)testOSSpinLock
{
    // 主线程中
    __block OSSpinLock spinlock = OS_SPINLOCK_INIT;
    
    // 任务1
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        OSSpinLockLock(&spinlock);
        [self threadMethod1];
        sleep(3);
        OSSpinLockUnlock(&spinlock);
    });
    
    for (NSUInteger i = 0; i < 10; i++) {
        // 任务2
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            // 睡一秒,保证 任务1 先加锁
            sleep(1);
            
            // 如果 任务1 加锁成功,则 任务2 的线程会循环等待,并不会进入睡眠,这是自旋锁的特点
            OSSpinLockLock(&spinlock);
            [self threadMethod2];
            OSSpinLockUnlock(&spinlock);
        });
    }
}
本文由作者按照 CC BY 4.0 进行授权