学习MJ的视频课程,整理总结知识点–队列组、自旋锁

面试题一:子线程添加timer会不执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"%@", @"1");
// 这句代码的本质是往Runloop中添加定时器
[self performSelector:@selector(log2func) withObject:nil afterDelay:.0];
NSLog(@"%@", @"3");
// [[NSRunLoop currentRunLoop] run]; 子线程默认不启动RunLoop,需要启动RunLoop,log2func才会执行
});
}

- (void)log2func {
NSLog(@"%@", @"2");
}

运行结果:打印“1 3”
一句话概括原因:performSelector:withObject:afterDelay是RunLoop中的代码,依靠RunLoop来执行,而子线程没有RunLoop或者子线程RunLoop没有启动

performSelector:withObject:afterDelay是RunLoop中的代码
底层用到了定时器、定时器添加到RunLoop里的,RunLoop被唤醒时才会处理timer任务,异步线程不执行timer是因为子线程RunLoop没有启动(需要启动RunLoop)[[NSRunLoop currentRunLoop] run];

面试题二:没有RunLoop,子线程执行完就会自动退出

1
2
3
4
5
6
7
8
9
10
11
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"1");

// 为RunLoop添加Source1
// [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
// 启动RunLoop保证线程不退出
// [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}];
[thread start];

[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];

performSelector:onThread:withObject:waitUntilDone:是通过RunLoop来处理的,子线程默认没有RunLoop,此时子线程内任务执行完就自动退出了,再执行test函数会崩溃。
除非子线程内启动了RunLoop

两个函数的对比:
performSelector:withObject: 调用的底层msgSend
performSelector:withObject:afterDelay 调用的底层是RunLoop中的API
两者调用的本质不太一样

RunLoop源码不开放,但是有组织用自己的方式实现了一遍Cocoa的源码,就是GNUstep

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
GNUstep是GNU计划的项目之一,它将Cocoa的OC库重新开源实现了一遍
源码地址:http://www.gnustep.org/resources/downloads.php
虽然GNUstep不是苹果官方源码,但还是具有一定的参考价值
```


## 队列组的使用
思考:如何用gcd实现以下功能:
异步并发执行任务:1、任务2,等任务1、任务2都执行完毕后,再回到主线程执行任务3


```objc
// 创建队列组
dispatch_group_t group = dispatch_group_create();
// 创建并发队列
dispatch_queue_t queue = dispatch_queue_create("my_queue", DISPATCH_QUEUE_CONCURRENT);

// 添加异步任务
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务1-%@", [NSThread currentThread]);
}
});

dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务2-%@", [NSThread currentThread]);
}
});

// 等前面的任务执行完毕后,会自动执行这个任务
dispatch_group_notify(group, queue, ^{
dispatch_async(dispatch_get_main_queue(), ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务3-%@", [NSThread currentThread]);
}
});
});

dispatch_group_notify(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务3-%@", [NSThread currentThread]);
}
});

dispatch_group_notify(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务4-%@", [NSThread currentThread]);
}
});

线程安全问题

卖票问题、存取钱问题

解决方案:使用线程同步技术(同步,就是协同步调,按预定的先后次序进行)
常见的线程同步技术是:加锁

ee51c443-f226-4d1a-99e2-4f81d41e9b21

bdbe50f5-99d9-4211-ad90-3b0e979f1f92

线程安全问题解决

解决方案:加锁

iOS中常用的锁有

  • OSSpinLock
  • os_unfair_lock
  • pthread_mutex
  • dispatch_semaphore
  • dispatch_queue(DISPATCH_QUEUE_SERIAL)
  • NSLock
  • NSRecursiveLock
  • NSCondition
  • NSConditionLock
  • @synchronized

OSSpinLock

OSSpinLock

1
2
3
4
5
6
7
8
9
10
11
12
static OSSpinLock moneyLock = OS_SPINLOCK_INIT;
- (void)saveMoney {
OSSpinLockLock(&moneyLock);

NSInteger oldMoney = money;
sleep(.2);
oldMoney += 50;
money = oldMoney;

NSLog(@"存50,还剩%ld元 - %@", oldMoney, [NSThread currentThread]);
OSSpinLockUnlock(&moneyLock);
}

要用同一把锁(锁被加了的标记,有标记就不进去执行了,等待这个锁被解标记后才能进去执行)

等待锁的线程会处于忙等,一致占用cpu资源,while等待

thread1
thread2
thread3
线程的调度
时间片轮训
线程优先级高的话,会分配的时间片更多

导致OSSpinLock的问题问题:优先级反转
如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁
(因为它是占用cpu忙等,所以执行效率是比较高的)

底层实现

通过汇编查看
stepinstruction、stepi、si

os_unfair_lock

iOS10 API_AVAILABLE
和OSSpinLock的使用方式非常像
Low-level lock 等待过程中会休眠,自旋锁不是,等待时是一直占用cpu等
os_unfair_lock

通过汇编查看
stepinstruction、stepi、si

pthread_mutex

mutex叫做”互斥锁”,等待锁的线程会处于休眠状态
pthread_mutex

递归锁

一个函数内加锁,内部调用另一个函数,这个函数内也加同样的锁,结果会出现死锁
递归函数也会出现同样的问题
解决:使用递归锁
递归锁:允许同一个线程对一把锁进行重复加锁

1
2
3
4
5
6
7
8
// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
// 初始化锁
pthread_mutex_init(mutex, &attr);
// 销毁属性
pthread_mutexattr_destroy(&attr);

递归锁可以对同一把锁重复加锁,但是重复加锁仅限同一个线程,重复加锁最终也都会做同样次数的释放锁
当第二个线程进来发现已经加锁时,就等待之前的线程释放锁

条件锁

条件
// 等待
pthread_cond_wait(&_cond, &_mutex);
// 信号
pthread_cond_signal(&_cond);

执行流程:
pthread_cond_wait睡眠等待、释放锁,等signal条件唤醒
signal条件唤醒,解锁完成,等待的线程重新加锁,继续往下执行,完成后解锁

使用场景:
线程依赖的实现

NSLock、NSRecursiveLock

NSLock是对mutex普通锁的封装
NSRecursiveLock也是对mutex递归锁的封装,API跟NSLock基本一致
NSLock、NSRecursiveLock

NSCondition

条件锁
NSCondition是对mutex和cond的封装
屏幕快照 2020-06-06 上午9.19.35

NSConditionLock

NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值
可以设置只有满足条件才加锁,真正使用的场景不多
NSConditionLock

dispatch_queue(DISPATCH_QUEUE_SERIAL)

串行队列,让队列中的任务串行执行,防止并发访问的安全问题。也就是多线程中的任务串行执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (instancetype)init {
if (self = [super init]) {
self.ticketQueue = dispatch_queue_create("ticketQueue", DISPATCH_QUEUE_SERIAL);
self.moneyQueue = dispatch_queue_create("moneyQueue", DISPATCH_QUEUE_SERIAL);
}
return self;
}

- (void)__drawMoney {
dispatch_sync(self.moneyQueue, ^{
[super __drawMoney];
});
}


- (void)__saveMoney {
dispatch_sync(self.moneyQueue, ^{
[super __saveMoney];
});
}

dispatch_queue

semaphore

semaphore叫做”信号量”
信号量的初始值,可以用来控制线程并发访问的最大数量
信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步
semaphore

1
2
3
4
5
6
7
8
static dispatch_semaphore_t semaphore;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
semaphore = dispatch_semaphore_create(1);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// ...
dispatch_semaphore_signal(semaphore);

@synchronized

@synchronized是对mutex递归锁的封装
源码查看:objc4中的objc-sync.mm文件
@synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作

@synchronized

选择对象
self、类对象、oncelock对象

iOS线程同步方案性能比较

性能从高到低排序
os_unfair_lock
OSSpinLock
dispatch_semaphore
pthread_mutex
dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock
NSCondition
pthread_mutex(recursive)
NSRecursiveLock
NSConditionLock
@synchronized

自旋锁、互斥锁比较

什么情况使用自旋锁比较划算?
预计线程等待锁的时间很短
加锁的代码(临界区)经常被调用,但竞争情况很少发生
CPU资源不紧张
多核处理器

什么情况使用互斥锁比较划算?
预计线程等待锁的时间较长
单核处理器
临界区有IO操作
临界区代码复杂或者循环量大
临界区竞争非常激烈

总结

os_unfair_lock iOS10
dispatch_semaphore 多样性
pthread_mutex 跨平台 多样性

参考和源码

源码:

评论