-
iOS开发常用的多线程方式
- pthread: C语言实现, 可以跨平台, 线程生命周期需要手动管理
- NSThread: OC实现, 线程生命周期需要手动管理
- GCD: 苹果对多核性能优化, C语言, 线程生命周期自动管理
- NSOperation: 对GCD封装, 线程生命周期自动管理
-
NSThread简单介绍
-
创建线程对象: 显示创建(alloc init)和隐式创建(performSelector…)
-
线程状态:新建, 就绪(start), 阻塞(sleep), 运行, 死亡(exit)
-
常用属性: name(当前线程名字), threadPriority(线程优先级), isMainThread等
-
GCD和NSOperation对比
- GCD执行效率更高,而且由于队列中执行的是由block构成的任务,这是一个轻量级的数据结构,写起来更方便
- GCD只支持FIFO的队列,而NSOperationQueue可以通过设置最大并发数,设置优先级,添加依赖关系等调整执行顺序
- NSOperationQueue甚至可以跨队列设置依赖关系,但是GCD只能通过设置串行队列,或者在队列内添加barrier(dispatch_barrier_async)任务,才能控制执行顺序,较为复杂
- NSOperationQueue因为面向对象,所以支持KVO,可以监测operation是否正在执行(isExecuted)、是否结束(isFinished)、是否取消(isCanceled)
-
什么情况下会出现死锁
-
在主线程中将同步任务添加到主队列会导致死锁
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"deallock");
});
// Do any additional setup after loading the view, typically from a nib.
}
-
在一个串行队列任务中将 同步任务添加到当前的队列中
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
dispatch_sync(serialQueue, ^{
NSLog(@"deadlock");
});
});
-
GCD中的线程栅栏
dispatch_barrier_async/dispatch_barrier_sync 分别表示同步栅栏和异步栅栏
栅栏的作用是可以将任务分块执行, 条件是任务必须在同一个队列上, 否则无效
dispatch_queue_t concurrentQueue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
for (NSInteger i = 0; i < 10; i++) {
dispatch_async(concurrentQueue, ^{
NSLog(@"%zd",i);
});
}
dispatch_barrier_async(concurrentQueue, ^{
NSLog(@"barrier");
});
NSLog(@"哈哈哈");
for (NSInteger i = 10; i < 20; i++) {
dispatch_async(concurrentQueue, ^{
NSLog(@"%zd",i);
});
}
执行结果:
哈哈哈
0-9 乱序打印
barrier
10-19乱序打印
由于采用的是异步栅栏, 所以栅栏后的任务会"哈哈哈"会提起执行, 如果将栅栏换成同步栅栏则 "哈哈哈"一定是在"barrier"之后执行
通过线程栅栏可以实现多度单写, 即允许多个地方同时读取数据, 但是在写入数据时只允许一个地方写入
- (id)readDataForKey:(NSString *)key {
__block id result;
dispatch_sync(_concurrentQueue, ^{
result = [self valueForKey:key];
});
return result;
}
- (void)writeData:(id)data forKey:(NSString *)key {
dispatch_barrier_async(_concurrentQueue, ^{
[self setValue:data forKey:key];
});
}
-
GCD线程组的使用
需求多个网络请求完成之后一并刷新UI
- (void)testGCDGroup {
__block NSInteger number = 0;
dispatch_group_t group = dispatch_group_create();
//A耗时操作
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(3);
number += 2222;
NSLog(@"A:%zd", number);
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self sendRequestWithCompletion:^(id response) {
number += [response integerValue];
NSLog(@"B:%zd", number);
}];
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self sendRequestWithCompletion:^(id response) {
number += [response integerValue];
NSLog(@"C:%zd", number);
}];
});
// //B网络请求
// dispatch_group_enter(group);
// [self sendRequestWithCompletion:^(id response) {
// number += [response integerValue];
// NSLog(@"B:%zd", number);
// dispatch_group_leave(group);
//
// }];
//
// //C网络请求
// dispatch_group_enter(group);
// [self sendRequestWithCompletion:^(id response) {
// number += [response integerValue];
// NSLog(@"C:%zd", number);
// dispatch_group_leave(group);
// }];
//
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"finish: %zd", number);
});
}
- (void)sendRequestWithCompletion:(void (^)(id response))completion {
//模拟一个网络请求
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
sleep(2);
dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(@1111);
});
});
}
调用结果:
B:1111
C:2222
A:4444
finish: 4444
使用方式如代码所示, 先创建线程组, 然后使用dispatch_group_async, 将任务绑定到线程组, 最后使用dispatch_group_notify接受线程组任务完成的回调. 其中也可以使用dispatch_group_enter(group)这种方式将任务添加到线程组, 不过任务结束之后要配合使用 dispatch_group_leave(group);
-
Dispatch Semaphore 信号量
Dispatch Semaphore 提供了三个函数
- dispatch_semaphore_create: 创建一个Semaphore并初始化总量
- dispatch_semaphore_signal: 发送信号, 让信号总量加1
- dispatch_semaphore_wait: 可以是信号总量减1, 信号总量为0是进入等待
Dispatch Semaphore 在实际开发中主要用于:
-
保持线程同步,将异步执行任务转换为同步执行任务
- (void)testSemaphore {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block NSInteger number = 0;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
number = 100;
NSLog(@"执行异步任务");
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"semaphore---end,number = %zd",number);
}
//打印结果如下:
//执行异步任务
//semaphore---end,number = 100
创建信号量初始化总量为0, 由于是异步执行,所以直接执行到dispatch_semaphore_wait, 如果信号量为0则一直等待阻断线程, 当异步任务执行完之后将信号量加1, 此时信号量等待函数终结, 继续执行后面的任务
-
保证线程安全,为线程加锁
- (void)testSemaphore2 {
_semaphore = dispatch_semaphore_create(1);
for (NSInteger i = 0; i < 100; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self asyncTask];
});
}
}
- (void)asyncTask {
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
_count++;
sleep(1);
NSLog(@"执行任务:%zd",_count);
dispatch_semaphore_signal(_semaphore);
}
///执行结果发现, 是顺序从1-100执行
原因: 在子线程执行并发任务时, 由于第一次执行任务将信号量减1, 信号总量变为0, 当第二个任务进来时需要由于信号总量为0所以进入等待状态, 任务一执行完之后将信号量加1, 任务二开始执行, 并立即将信号总量减1变为0, 任务三继续等待, 依次类推, 实现了线程加锁目的
-
使用dispatch_once实现单例
//手写单例
- (id)sharedInstance {
static id instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
})
return instance;
}
-
NSOperationQueue/NSOperation简介
-
优势
-
可以添加任务依赖,方便控制执行顺序
-
可以设定操作执行的优先级
-
可以设置最大并发量
-
任务执行状态控制:isReady,isExecuting,isFinished,isCancelled
如果只是重写NSOperation的main方法,由底层控制变更任务执行及完成状态,以及任务退出
如果重写了NSOperation的start方法,自行控制任务状态
系统通过KVO的方式移除isFinished==YES的NSOperation
-
基本操作
NSOperation是抽象类, 主要使用两个子类: NSBlockOperation和NSInvocationOperation
可以直接添加Block任务NSOperation到队列中执行, 默认在当前线程执行, NSBlockOperation添加多任务时会自动开启新线程执行
也可以将NSOperation添加到NSOperationQueue中执行操作, 当maxConcurrentOperationCount = 1时为串行队列, 大于1时为并发队列
-
示例代码
/**
* 设置 MaxConcurrentOperationCount(最大并发操作数)
*/
- (void)setMaxConcurrentOperationCount {
// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.设置最大并发操作数
queue.maxConcurrentOperationCount = 1; // 串行队列
// queue.maxConcurrentOperationCount = 2; // 并发队列
// queue.maxConcurrentOperationCount = 8; // 并发队列
// 3.添加操作
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程
}
}];
}
执行结果为顺序执行1,1,2,2,3,3,4,4
如果将maxConcurrentOperationCount改为2, 则并并发执行, 这里就不再验证
-
NSOperation的操作依赖设置
/**
* 操作依赖, 操作2依赖操作1
* 使用方法:addDependency:
*/
- (void)addDependency {
// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.创建操作
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];
// 3.添加依赖
[op2 addDependency:op1]; // 让op2 依赖于 op1,则先执行op1,在执行op2
// 4.添加操作到队列中
[queue addOperation:op1];
[queue addOperation:op2];
}
-
NSOperation 提供了queuePriority
属性, 新建操作的默认优先级是Normal
// 优先级的取值
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};
-
线程间通信示例
/**
* 线程间通信
*/
- (void)communication {
// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
// 2.添加操作
[queue addOperationWithBlock:^{
// 异步进行耗时操作
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
// 回到主线程
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// 进行一些 UI 刷新等操作
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];
}];
}
-
RunLoop
-
什么是RunLoop
RunLoop是通过内部维护的事件循环来对事件进行管理的一个对象
-
为什么main函数不会退出
UIApplicationMain函数内部默认开启了主线程的RunLoop,并执行了一段无限循环的代码
-
RunLoop的数据结构
NSRunLoop(Foundation)
是CFRunLoop(CoreFoundation)
的封装,提供了面向对象的API
RunLoop 相关的主要涉及五个类:
-
CFRunLoop
:RunLoop对象
-
CFRunLoopMode
:运行模式
kCFRunLoopDefaultMode
:默认模式,主线程是在这个运行模式下运行UITrackingRunLoopMode
:跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)UIInitializationRunLoopMode
:在刚启动App时第进入的第一个 Mode,启动完成后就不再使用GSEventReceiveRunLoopMode
:接受系统内部事件,通常用不到kCFRunLoopCommonModes
:伪模式,不是一种真正的运行模式,是同步Source/Timer/Observer到多个Mode中的一种解决方案
苹果对外开放的主要有kCFRunLoopDefaultMode
和kCFRunLoopCommonModes
一个比较常见的问题:滑动tableView时,定时器还会生效吗?
默认情况下RunLoop运行在kCFRunLoopDefaultMode
下,而当滑动tableView时,RunLoop切换到UITrackingRunLoopMode
,而Timer是在kCFRunLoopDefaultMode
下的,就无法接受处理Timer的事件。 怎么去解决这个问题呢?把Timer添加到UITrackingRunLoopMode
上并不能解决问题,因为这样在默认情况下就无法接受定时器事件了。
所以我们需要把Timer同时添加到UITrackingRunLoopMode
和kCFRunLoopDefaultMode
上。
那么如何把timer同时添加到多个mode上呢?就要用到NSRunLoopCommonModes
了
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
-
CFRunLoopSource
:输入源/事件源
-
CFRunLoopTimer
:定时源
-
CFRunLoopObserver
:观察者
-
RunLoop常见问题
- (void)test {
NSLog(@"1");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"2");
[[NSRunLoop currentRunLoop] run];
[self performSelector:@selector(test) withObject:nil afterDelay:10];
NSLog(@"3");
});
NSLog(@"4");
}
- (void)test {
NSLog(@"5");
}
输出顺序? 答案是: 1423, 5不会执行, 原因是runloop开启时应该有任务执行mode中应该有item才行, 否则会退出, 所以应调整成如下代码
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"2");
//先添加任务
[self performSelector:@selector(test) withObject:nil afterDelay:10];
//再开启循环
[[NSRunLoop currentRunLoop] run];
NSLog(@"3");
});
-
如何创建一个常驻线程
@autoreleasepool {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
-
怎样保证子线程数据更新回到主线程更新UI时不影响滑动操作
答: 将更新UI的任务添加到主线程的NSDefaultRunLoopMode 上执行即可, 这样主线程将会在用户停止滑动之后由UITrackingRunLoopMode切换到NSDefaultRunLoopMode之后更新UI
[self performSelectorOnMainThread:@selector(reloadData)
withObject:nil waitUntilDone:NO
modes:@[NSDefaultRunLoopMode]];
-
自旋锁与互斥锁
-
自旋锁
当任务被另一个线程锁定是, 尝试执行的线程会进入等待(不会休眠), 等上一个线程解除锁定时, 立即执行下一个线程任务,
优点: 因为自旋锁不会引起调用者睡眠,所以不会进行线程调度,CPU时间片轮转等耗时操作。所有如果能在很短的时间内获得锁,自旋锁的效率远高于互斥锁
缺点: 自旋锁一直占用CPU,他在未获得锁的情况下,一直运行--自旋,所以占用着CPU,如果不能在很短的时 间内获得锁,这无疑会使CPU效率降低。自旋锁不能实现递归调用
常用自旋锁: atomic, dispatch_semaphore_t
-
互斥锁
当上一个线程锁定时, 下一个尝试执行的线程会进入休眠, 等上一个线程解除锁定, 下一个线程自动唤醒然后执行任务
常用互斥锁: NSLock, NSCondition, @ synchronized