iOS中几种常用的锁

多线程编程中,应该尽量避免资源在线程之间共享,以减少线程之间的相互作用,但是总有多个线程相互干扰的情况下(如多个线程访问一个资源)。在线程必须交互的情况下,就需要一些同步工具,来确保它们交互是安全的。

锁是线程编程同步工具的基础。iOS开发中常用的锁用一下几种:

  1. @synchronized
  2. NSLock 对象锁
  3. NSRecuisiveLock 递归锁
  4. NSConditionLock 条件锁
  5. pthread_mutex 互斥锁
  6. dispatch_semaphore 信号量加锁(GCD)
  • @synchoronized关键字加锁(互斥锁),性能较差不推荐使用。
1
2
3
4
5
6
7
8
  @synchoronized(self){
//这里添加要加锁的代码
}
注意点:
1. 加锁代码尽量少
2. 添加OC对象必须在多个线程中都是同一对象
3. 有点是不需要显示的创建锁对象,便可以实现锁的机制。
3. @synchronized块会隐藏的添加一个异常处理程序来保护代码,该处理层会在异常抛出是自动释放互斥锁。所以如果不想让隐试的异常处理例程带来额外开销,你可以考虑锁对象。

以面通过买票的例子展示使用

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
//设置票的张数
-(void)startSaleTickets
{
_tickets = 5;//设置票的总数
dispatch_async(_saleticket_queue, ^{
[self saleTickets];
});
dispatch_async(_saleticket_queue, ^{
[self saleTickets];
});
}

//@synchronized
-(void)saleTickets
{
while (1) {
@synchronized (self) {
[NSThread sleepForTimeInterval:1];
if (_tickets > 0) {
_tickets -- ;
NSLog(@"剩余票数:%d, Thread:%@",_tickets,[NSThread currentThread]);
}else{
NSLog(@"票已经卖完:Thread:%@",[NSThread currentThread]);
break;
}
}
}
}

以下是打印

2019-04-01 14:19:27.397588+0800 LockTest[4435:147099] 剩余票数:4, Thread:<NSThread: 0x600000c8c540>{number = 3, name = (null)}
2019-04-01 14:19:28.398071+0800 LockTest[4435:147099] 剩余票数:3, Thread:<NSThread: 0x600000c8c540>{number = 3, name = (null)}
2019-04-01 14:19:29.398409+0800 LockTest[4435:147099] 剩余票数:2, Thread:<NSThread: 0x600000c8c540>{number = 3, name = (null)}
2019-04-01 14:19:30.398952+0800 LockTest[4435:147099] 剩余票数:1, Thread:<NSThread: 0x600000c8c540>{number = 3, name = (null)}
2019-04-01 14:19:31.403696+0800 LockTest[4435:147099] 剩余票数:0, Thread:<NSThread: 0x600000c8c540>{number = 3, name = (null)}
2019-04-01 14:19:32.403928+0800 LockTest[4435:147099] 票已经卖完:Thread:<NSThread: 0x600000c8c540>{number = 3, name = (null)}
2019-04-01 14:19:33.405796+0800 LockTest[4435:147102] 票已经卖完:Thread:<NSThread: 0x600000c8d040>{number = 4, name = (null)}
  • NSLock 互斥锁,不能多次调用lock方法,会造成死锁

在Cocoa程序中NSLock实现了一个简单的互斥锁。
所有锁(包括NSLock)的接口实际上都是通过NSLocking定义的,它定义了lockunlock方法,你使用这些方法获取或者释放该锁。

NSLock类还增加了tryLocklockBeforeDate:方法。

  • tryLock试图获取一个锁,但是如果锁不可用的时候,它不会阻塞线程,相反,它只是返回NO。
  • lockBeforeDate:方法试图获取一个锁,但是如果锁没有在规定的时间内被获得,它会让线程从阻塞状态变成非阻塞状态。(或者返回NO)
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
-(void)startSaleTickets
{
_tickets = 5;//设置票的总数
dispatch_async(_saleticket_queue, ^{
[self saleTickets];
});
dispatch_async(_saleticket_queue, ^{
[self saleTickets];
});
}

//@synchronized
-(void)saleTickets
{
while (1) {
@synchronized (self) {
[NSThread sleepForTimeInterval:1];
if (_tickets > 0) {
_tickets -- ;
NSLog(@"剩余票数:%d, Thread:%@",_tickets,[NSThread currentThread]);
}else{
NSLog(@"票已经卖完:Thread:%@",[NSThread currentThread]);
break;
}
}
}
}

打印如下

2019-04-01 14:53:04.285814+0800 LockTest[4552:172571] 剩余票数:4, Thread:<NSThread: 0x60000320d940>{number = 3, name = (null)}
2019-04-01 14:53:06.584844+0800 LockTest[4552:172572] 剩余票数:3, Thread:<NSThread: 0x60000320d7c0>{number = 4, name = (null)}
2019-04-01 14:53:07.585406+0800 LockTest[4552:172571] 剩余票数:2, Thread:<NSThread: 0x60000320d940>{number = 3, name = (null)}
2019-04-01 14:53:08.443847+0800 LockTest[4552:172572] 剩余票数:1, Thread:<NSThread: 0x60000320d7c0>{number = 4, name = (null)}
2019-04-01 14:53:10.613102+0800 LockTest[4552:172571] 剩余票数:0, Thread:<NSThread: 0x60000320d940>{number = 3, name = (null)}
2019-04-01 14:53:12.610755+0800 LockTest[4552:172572] 票已卖完,Thread:<NSThread: 0x60000320d7c0>{number = 4, name = (null)}
2019-04-01 14:53:13.614076+0800 LockTest[4552:172571] 票已卖完,Thread:<NSThread: 0x60000320d940>{number = 3, name = (null)}
  • NSRecursiveLock 递归锁
    使用递归锁最容易犯的一个错误就是在递归或循环中造成死锁
    如下代码中,因为在线程1中的递归block中,锁会被多次lock,所以自己也被阻塞了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-(void)deadLockTest
{
dispatch_async(_saleticket_queue, ^{
static void(^LockTest)(int);
LockTest = ^(int value){
[self->_lock lock];
if (value > 0) {
[NSThread sleepForTimeInterval:1];
NSLog(@"deadLockTest Value=%d",value);
LockTest(value--);
}
[self->_lock unlock];
};
LockTest(5);
});
}

此处,将NSLock换成NSRecursizeLock便解决问题.
NSRecursiveLock类定义的锁可以在同一线程多次lock,而不会造成死锁。递归锁会跟踪它被多次次lock。每次成功的lock都必须平衡调用unlock操作。只有所有的锁住和解锁操作都平衡的时候,锁才真正被释放给其他线程获得。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-(void)recursizeLockTest
{
_recursizeLock = [[NSRecursiveLock alloc] init];
dispatch_async(_saleticket_queue, ^{
static void(^TestLock)(int);
TestLock = ^(int value){
[self->_recursizeLock lock];
if (value > 0) {
[NSThread sleepForTimeInterval:1];
NSLog(@"recursizeLockTest Value=%d",value);
value--;
TestLock(value);
}
[self->_recursizeLock unlock];
};
TestLock(5);
});
}
  • NSConditionLock 条件锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-(void)condictionLockTest
{
NSConditionLock *theLock = [[NSConditionLock alloc] init];
//线程1
dispatch_async(_saleticket_queue, ^{
for (int i=0; i<= 3; i++){
[theLock lock];
NSLog(@"thread:%d",i);
sleep(1);
[theLock unlockWithCondition:i];
}
});
//线程2
dispatch_async(_saleticket_queue, ^{
[theLock lockWhenCondition:2];
NSLog(@"thread2111111");
[theLock unlock];
});
}

在线程1中加锁使用了lock,是不需要条件的,所以顺利的就锁住了,unlockWithConditin:在开锁的同时设置了一个整形条件2.线程2則需要一把被标识为2的钥匙,所以当线程1循环到i=2时,线程2的任务才执行。
NSConditionLock也跟其他的锁一样,是需要lock与unlock对应的,只是lock,lockWhenCondition:与unlock,unlockWithCondition:是可以随意组合的。当然这是与你需求相关的。

  • pthread_mutex 互斥锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)pthread_mutexTest
{
__block pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
//线程1
dispatch_async(_saleticket_queue, ^{
pthread_mutex_lock(&mutex);
NSLog(@"任务1");
sleep(2);
pthread_mutex_unlock(&mutex);
});
//线程2
dispatch_async(_saleticket_queue, ^{
sleep(1);
pthread_mutex_lock(&mutex);
NSLog(@"任务2");
pthread_mutex_unlock(&mutex);
});
}

打印如下

2019-04-01 17:28:03.606464+0800 LockTest[5075:272170] 任务1
2019-04-01 17:28:05.611833+0800 LockTest[5075:272169] 任务2
  • dispatch_semaphore 信号量实现加锁

GCD也提供了一种信号机制。使用它我们也可以来构建一把“锁”(从本质意义上来讲,信号量与锁是有区别的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)dispatch_semaphoreTest
{
//创建信号量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
//线程1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"任务1");
sleep(10);
dispatch_semaphore_signal(semaphore);
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(1);
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"任务2");
dispatch_semaphore_signal(semaphore);
});
}

打印如下:

2019-04-01 17:33:14.204755+0800 LockTest[5090:275909] 任务1
2019-04-01 17:33:24.206795+0800 LockTest[5090:275911] 任务2

信号量和互斥锁的区别

信号量用在多线程任务同步的,一个线程完了某一个动作就通过信号量来告诉被的线程,别的线程再进行某些动作(大家都在semtake的时候,就阻塞在哪里)。而互斥锁是用在多线程多任务互斥的,一个线程占用了某一个资源,那么别的线程就无法访问,直到这个线程unlock,其他的线程才能开始利用这个资源。比如全局变量的访问,有时要加锁,操作完了再解锁。有时候锁和信号量会同时使用的。
也就是说,信号量不一定是锁定某一个资源,而是流程上的概念,比如有A、B两个线程,B线程要等A线程完成某一个任务以后再进行自己下面的步骤,这个任务不一定是锁定某一资源,还是可以进行一些计算或者数据处理之类。而线程互斥则是“锁住某一资源的概念”,在锁定期间内,其他线程无法对被保护的数据进行操作。有些情况下两者可以互换。

0%