GCD
1. 队列和任务
队列(Dispatch Queue):这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用 FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。
- 串行队列(Serial Dispatch Queue):
- 每次只有一个任务被执行。让任务一个接着一个地执行。
- 只开启一个线程,一个任务执行完毕后,再执行下一个任务。
- 并发队列(Concurrent Dispatch Queue):
- 可以让多个任务并发(同时)执行。
- 可以开启多个线程,并且同时执行任务。
并发队列的并发功能只有在异步(dispatch_async)方法下才有效。
任务:就是执行操作的意思,换句话说就是你在线程中执行的那段代码。在 GCD 中是放在 block 中的。执行任务有两种方式:『同步执行』和『异步执行』。两者的主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力。
- 同步执行(sync):
- 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
- 只能在当前线程中执行任务,不具备开启新线程的能力。
- 异步执行(async):
- 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
- 可以在新的线程中执行任务,具备开启新线程的能力。
异步执行(async)虽然具有开启新线程的能力,但是并不一定开启新线程。这跟任务所指定的队列类型有关(下面会讲)。
区别 | 并发队列 | 串行队列 | 主队列 |
---|---|---|---|
同步(sync) | 没有开启新线程,串行执行任务 | 没有开启新线程,串行执行任务 | 主线程调用:死锁卡住不执行;其他线程调用:没有开启新线程,串行执行任务 |
异步(async) | 有开启新线程,并发执行任务 | 有开启新线程(1条),串行执行任务 | 没有开启新线程,串行执行任务 |
死锁的原因:队列引起的循环等待。主队列并不特殊,任何一个串行队列嵌套执行同步任务都会造成死锁。
2. 队列的创建方法 / 获取方法
可以使用dispatch_queue_create
方法来创建队列。该方法需要传入两个参数:
- 第一个参数表示队列的唯一标识符,用于DEBUG,可为空。队列的名称推荐使用应用程序ID这种逆序全程域名。
- 第二个参数用来识别是串行队列还是并发队列。
DISPATCH_QUEUE_SERIAL
表示串行队列,DISPATCH_QUEUE_CONCURRENT
表示并发队列。
// 串行队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("com.hao.testQueue", DISPATCH_QUEUE_SERIAL);
// 并发队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("com.hao.testQueue", DISPATCH_QUEUE_CONCURRENT);
复制代码
对于串行队列,GCD 默认提供了『主队列(Main Dispatch Queue)』。
- 所有放在主队列中的任务,都会放到主线程中执行。
- 可使用
dispatch_get_main_queue()
方法获得主队列。
// 主队列的获取方法
dispatch_queue_t queue = dispatch_get_main_queue();
复制代码
注意:主队列其实并不特殊。主队列的实质上就是一个普通的串行队列,只是因为默认情况下,当前代码是放在主队列中的,然后主队列中的代码,有都会放到主线程中去执行,所以才造成了主队列特殊的现象。
对于并发队列,GCD 默认提供了『全局并发队列(Global Dispatch Queue)』。
- 可以使用
dispatch_get_global_queue
方法来获取全局并发队列。 - 需要传入两个参数。第一个参数表示队列优先级,一般用
DISPATCH_QUEUE_PRIORITY_DEFAULT
。第二个参数暂时没用,用0即可。
// 全局并发队列的获取方法
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
复制代码
3. 任务的创建方法
GCD提供了同步执行任务的创建方法dispatch_sync
和异步执行任务创建方法dispatch_async
。
// 同步执行任务创建方法
dispatch_sync(queue, ^{
// 这里放同步执行任务代码
});
// 异步执行任务创建方法
dispatch_async(queue, ^{
// 这里放异步执行任务代码
});
复制代码
4. 栅栏方法:dispatch_barrier_async
我们有时需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作。这样我们就需要一个相当于 栅栏 一样的一个方法将两组异步执行的操作组给分割起来,当然这里的操作组里可以包含一个或多个任务。这就需要用到dispatch_barrier_async
方法在两个操作组间形成栅栏。
dispatch_barrier_async
方法会等待前边追加到并发队列中的任务全部执行完毕之后,再将指定的任务追加到该异步队列中。然后在dispatch_barrier_async
方法追加的任务执行完毕之后,异步队列才恢复为一般动作,接着追加任务到该异步队列并开始执行。
多读单写功能的实现
#import "UserCenter.h"
@interface UserCenter() {
dispatch_queue_t queue;
}
@property (nonatomic, strong) NSMutableDictionary *dataDic;
@end
@implementation UserCenter
- (instancetype)init
{
self = [super init];
if (self) {
// 创建并发队列
queue = dispatch_queue_create("concurrent_queues", DISPATCH_QUEUE_CONCURRENT);
// 创建数据容器
self.dataDic = [NSMutableDictionary dictionary];
}
return self;
}
- (id)objectForKey:(NSString *)key {
__block id obj;
dispatch_sync(queue, ^{
obj = [self.dataDic objectForKey:key];
});
return obj;
}
- (void)setObject:(id)obj forKey:(NSString *)key {
dispatch_barrier_async(queue, ^{
[self.dataDic setObject:obj forKey:key];
});
}
@end
复制代码
在执行完栅栏前面的操作之后,才执行栅栏操作,最后再执行栅栏后边的操作。
5. GCD 队列组:dispatch_group
有时候我们会有这样的需求:分别异步执行2个耗时任务,然后当2个耗时任务都执行完毕后再回到主线程执行任务。这时候我们可以用到GCD的队列组。
- 调用队列组的
dispatch_group_async
先把任务放到队列中,然后将队列放入队列组中。或者使用队列组的dispatch_group_enter
、dispatch_group_leave
组合来实现dispatch_group_async
。 - 调用队列组的
dispatch_group_notify
回到指定线程执行任务。或者使用dispatch_group_wait
回到当前线程继续向下执行(会阻塞当前线程)。
#import "GroupObject.h"
@interface GroupObject() {
dispatch_queue_t queue;
}
@property (nonatomic, strong) NSMutableArray <NSURL *> *arrayURLs;
@end
@implementation GroupObject
- (instancetype)init
{
self = [super init];
if (self) {
queue = dispatch_queue_create("concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
self.arrayURLs = [NSMutableArray array];
}
return self;
}
- (void)handle {
// 创建一个group
dispatch_group_t group = dispatch_group_create();
// 通过for循环,将任务异步分派到并发队列
for (NSURL *url in self.arrayURLs) {
dispatch_group_async(group, queue, ^{
// 网络请求
[NSThread sleepForTimeInterval:2];
NSLog(@"Url is %@.", url);
});
}
// 当group中的所有任务都执行完成后,回到主线程执行dispatch_group_notify中的任务
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"GroupEnd");
});
}
@end
复制代码
NSOperation
1. NSOperation实现多线程有哪些优势和特点?
- 添加任务依赖
- 任务执行状态控制
- 控制最大并发量(设为1则相当于串行)
2. NSOperation的任务都有那些执行状态?
- isReady:当前任务是否处于就绪状态
- isExecuting:当前任务是否处于正在执行中
- isFinished:当前任务是否已执行完成
- isCancelled:当前任务是否已取消
3. NSOperation的状态控制
- 如果只重写main方法,底层控制变更任务执行完成状态,以及任务退出。
- 如果重写了start方法,自行控制任务状态。
系统是怎样移除一个isFinished=YES的NSOperation的?
通过KVO
NSThread
1. NSThread启动流程
- 当我们创建一个NSThread之后,会调用它的
start()
方法来启动线程; - 在
start()
方法内部会创建一个pthread线程,然后指定pthread线程的启动函数; - 在pthread线程的启动函数当中会调用NSThread的
main()
函数; - 在
main()
函数中通过[target performSelector:selector]
的形式来执行NSThread在创建时指定的目标对应的选择器; - 最后再调用线程关闭函数
exit()
来结束线程。
2. 如何实现一个常驻线程?
我们可以在NSThread对应的入口函数中添加一个事件循环,这个事件循环往往是添加到我们在创建NSThread时,所指定的选择器方法[target performSelector:selector]
中,我们可以在选择器方法中维护一个RunLoop来实现常驻线程的目的。
锁
1. 关于锁的一些概念
- 临界区:指的是一块对公共资源进行访问的代码,并非一种机制或是算法。
- 自旋锁:是用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。
- 互斥锁(Mutex):是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成。
- 读写锁:是计算机程序的并发控制的一种同步机制,也称“共享-互斥锁”、多读者-单写者锁) 用于解决多线程对公共资源读写问题。读操作可并发重入,写操作是互斥的。读写锁通常用互斥锁、条件变量、信号量实现。
- 信号量(semaphore):是一种更高级的同步机制,互斥锁可以说是semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。
- 条件锁:就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行。
2. 时间片轮转调度算法
现代操作系统在管理普通线程时,通常采用时间片轮转算法(Round Robin,简称 RR)。每个线程会被分配一段时间片(quantum),通常在10-100毫秒左右。当线程用完属于自己的时间片以后,就会被操作系统挂起,放入等待队列中,直到下一次被分配时间片。
线程在多种情况下会退出自己的时间片。其中一种是用完了时间片的时间,被操作系统强制抢占。除此以外,当线程进行I/O操作,或进入睡眠状态时,都会主动让出时间片。
但是主动让出时间片并不总是代表效率高。让出时间片会导致操作系统切换到另一个线程,这种上下文切换通常需要10微秒左右,而且至少需要两次切换。如果等待时间很短,比如只有几个微秒,忙等就比线程睡眠更高效。
自旋锁更适用于轻量级数据访问;而互斥锁的实现中,有时也会先进行一定次数的忙等(例如1000次),再进行线程睡眠,来保证效率。
3. pthread_mutex
pthread表示POSIX thread,定义了一组跨平台的线程相关的API,pthread_mutex表示互斥锁。互斥锁的实现原理不是使用忙等,而是阻塞线程并睡眠,需要进行上下文切换。
互斥锁的常见用法
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); // 定义锁的属性
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr) // 创建锁
pthread_mutex_lock(&mutex); // 申请锁
// 临界区
pthread_mutex_unlock(&mutex); // 释放锁
复制代码
一般情况下,一个线程只能申请一次锁,也只能在获得锁的情况下才能释放锁,多次申请锁或释放未获得的锁都会导致崩溃。假设在已经获得锁的情况下再次申请锁,线程会因为等待锁的释放而进入睡眠状态,因此就不可能再释放锁,从而导致死锁。
然而这种情况经常会发生,比如某个函数申请了锁,在临界区内又递归调用了自己。辛运的是 pthread_mutex支持递归锁,也就是允许一个线程递归的申请锁,只要把attr的类型改成PTHREAD_MUTEX_RECURSIVE
即可。
4. NSLock
NSLock是Objective-C以对象的形式暴露给开发者的一种锁,它的实现非常简单,通过宏,定义了lock方法:
#define MLOCK \
- (void) lock\
{\
int err = pthread_mutex_lock(&_mutex);\
// 错误处理 ……
}
复制代码
NSLock只是在内部封装了一个pthread_mutex,属性为PTHREAD_MUTEX_ERRORCHECK
,它会损失一定性能换来错误提示。
NSLock比pthread_mutex略慢的原因在于它需要经过方法调用,同时由于缓存的存在,多次方法调用不会对性能产生太大的影响。
5. dispatch_semaphore
类似于过高速路收费站的栏杆。可以通过时,打开栏杆,不可以通过时,关闭栏杆。在 Dispatch Semaphore 中,使用计数来完成这个功能,计数小于 0 时等待,不可通过。计数为 0 或大于 0 时,计数减 1 且不等待,可通过。
dispatch_semaphore提供了三个方法:
dispatch_semaphore_create
:创建一个Semaphore并初始化信号的总量dispatch_semaphore_signal
:发送一个信号,让信号总量加1dispatch_semaphore_wait
:可以使总信号量减1,信号总量小于0时就会一直等待(阻塞所在线程),否则就可以正常执行。
dispatch_semaphore在实际开发中主要用于:
- 保持线程同步,将异步执行任务转换为同步执行任务
- 保证线程安全,为线程加锁
1) 使用信号量实现线程同步
- (void)semaphoreSync {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"semaphore---begin");
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block int number = 0;
dispatch_async(queue, ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
number = 100;
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"semaphore---end,number = %zd",number);
}
复制代码
2) 使用信号量实现线程安全
/**
* 线程安全:使用 semaphore 加锁
* 初始化火车票数量、卖票窗口(线程安全)、并开始卖票
*/
- (void)initTicketStatusSave {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"semaphore---begin");
semaphoreLock = dispatch_semaphore_create(1);
self.ticketSurplusCount = 50;
// queue1 代表北京火车票售卖窗口
dispatch_queue_t queue1 = dispatch_queue_create("net.bujige.testQueue1", DISPATCH_QUEUE_SERIAL);
// queue2 代表上海火车票售卖窗口
dispatch_queue_t queue2 = dispatch_queue_create("net.bujige.testQueue2", DISPATCH_QUEUE_SERIAL);
__weak typeof(self) weakSelf = self;
dispatch_async(queue1, ^{
[weakSelf saleTicketSafe];
});
dispatch_async(queue2, ^{
[weakSelf saleTicketSafe];
});
}
/**
* 售卖火车票(线程安全)
*/
- (void)saleTicketSafe {
while (1) {
// 相当于加锁
dispatch_semaphore_wait(semaphoreLock, DISPATCH_TIME_FOREVER);
if (self.ticketSurplusCount > 0) { // 如果还有票,继续售卖
self.ticketSurplusCount--;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
} else { // 如果已卖完,关闭售票窗口
NSLog(@"所有火车票均已售完");
// 相当于解锁
dispatch_semaphore_signal(semaphoreLock);
break;
}
// 相当于解锁
dispatch_semaphore_signal(semaphoreLock);
}
}
复制代码
6. 条件锁
1) NSCondition
NSCondition的底层是通过条件变量(condition variable)pthread_cond_t来实现的。条件变量有点像信号量,提供了线程阻塞与信号机制,因此可以用来阻塞某个线程,并等待某个数据就绪,随后唤醒线程,比如常见的生产者-消费者模式。
void consumer () { // 消费者
pthread_mutex_lock(&mutex);
while (data == NULL) {
pthread_cond_wait(&condition_variable_signal, &mutex); // 等待数据
}
// --- 有新的数据,以下代码负责处理 ↓↓↓↓↓↓
// temp = data;
// --- 有新的数据,以上代码负责处理 ↑↑↑↑↑↑
pthread_mutex_unlock(&mutex);
}
void producer () {
pthread_mutex_lock(&mutex);
// 生产数据
pthread_cond_signal(&condition_variable_signal); // 发出信号给消费者,告诉他们有了新的数据
pthread_mutex_unlock(&mutex);
}
复制代码
2) NSConditionLock
NSConditionLock
借助NSCondition
来实现,它的本质就是一个生产者-消费者模型。“条件被满足”可以理解为生产者提供了新的内容。NSConditionLock
的内部持有一个NSCondition
对象,以及_condition_value
属性,在初始化时就会对这个属性进行赋值:
// 简化版代码
- (id)initWithCondition:(NSInteger)value {
if (nil != (self = [super init])) {
_condition = [NSCondition new]
_condition_value = value;
}
return self;
}
复制代码
它的lockWhenCondition
方法其实就是消费者方法:
- (void)lockWhenCondition:(NSInteger)value {
[_condition lock];
while (value != _condition_value) {
[_condition wait];
}
}
复制代码
对应的unlockWhenCondition
方法则是生产者,使用了broadcast
方法通知了所有的消费者:
- (void) unlockWithCondition: (NSInteger)value {
_condition_value = value;
[_condition broadcast];
[_condition unlock];
}
复制代码
7. NSRecursiveLock
递归锁也是通过pthread_mutex_lock
函数来实现,在函数内部会判断锁的类型,如果类型为PTHREAD_MUTEX_RECURSIVE
,就允许递归调用,仅仅将一个计数器加一,锁的释放过程也是同理。
8. @synchronized
这其实是一个OC层面的锁, 主要是通过牺牲性能换来语法上的简洁与可读。
我们知道@synchronized后面需要紧跟一个OC对象,它实际上是把这个对象当做锁来使用。这是通过一个哈希表来实现的,OC在底层使用了一个互斥锁的数组(你可以理解为锁池),通过对对象去哈希值来得到对应的互斥锁。
9. OSSpinLock(已废弃)
OSSpinLock
是自旋锁,已被废弃,主要原因发生在低优先级线程拿到锁时,高优先级线程进入忙等(busy-wait)状态,消耗大量CPU时间,从而导致低优先级线程拿不到CPU时间,也就无法完成任务并释放锁。这种问题被称为优先级反转。
自旋锁的实现原理
bool lock = false; // 一开始没有锁上,任何线程都可以申请锁
do {
while(lock); // 如果 lock 为 true 就一直死循环,相当于申请锁
lock = true; // 挂上锁,这样别的线程就无法获得锁
Critical section // 临界区
lock = false; // 相当于释放锁,这样别的线程可以进入临界区
Reminder section // 不需要锁保护的代码
}
复制代码