iOS 多线程那点事
iOS SDK对于实现多线程提供了多种技术选项 :
技术选项 | 详细 |
---|---|
NSThread类 | SDK里定义了NSThread类创建线程,NSObject也内提供了方法实现多线程机制,其本质也是利用NSThread。 |
GCD (Grand Central Dispatch) | GCD让开发者专注于要执行的任务本身,而不是线程的管理,通过预定义的队列,可以控制任务执行的时间和目标线程,贴近系统底层的GCD会根据系统的可用资源调整合理安排任务的运行,效率比开发者自己处理更有效率。 |
Operation对象 | Operation利用对要执行任务的封装,隐藏目标线程的管理,和GCD一样,让开发者专注与任务本身。Operation常见的场景是和NSOperationQueue结合使用,实现任务的并发执行或依赖执行。 |
Timer | 一些较简单任务不必启用线程,且需要周期性运行,可以使用Timer。 |
空闲Notification | 对于一些执行时间短和优先级低的任务,可以通过结合NSNotificationQueue和NSPostWhenIdle触发,系统队列会在空闲的时间触发推送 |
NSThread
- 自定义线程可以继承NSThread,并重写主入口main方法。
- 调用类方法detachNewThreadSelector:toTarget:withObject: 创建一个新线程执行制定的方法。
// NSThread
+ (void)detachNewThreadSelector:(SEL)aSelector toTarget:(id)aTarget withObject:(id)anArgument
其中参数aSelector是发给目标对象的消息,只能带有一个参数,且没有返回值;aTarget是新线程中接收消息的对象;anArgument是唯一传递给aTarget的参数,可能为nil。
需要注意的是aSelector方法需要自己创建@autoreleasepool{},并在结束的时候释放它。
NSObject 线程
NSObject 实例中提供方法 performSelectorOnMainThread:withObject:waitUntilDone: 允许后台线程执行某些操作在主线程(如更新界面)。
//NSObject
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait
其中的wait参数表示当前线程是否被运行在主线程的指定方法提阻塞,如果是YES则阻塞当前线程,为NO该方法则立即返回。
如果当前线程本就是主线程,若wait值为YES,消息则会立即发送并得到处理。
可以看出NSThread和NSObject线程的特点:
- 使用简单,量级轻
- 不能控制线程的数量以及执行顺序
NSObject的多线程是基于NSThread的多线程技术封装的,那么也需要自己处理内存管理的问题。
taskCount = 5;
- (IBAction)runTasks:(id)sender {
for (int i = 0; i < taskCount; ++i) {
[NSThread detachNewThreadSelector:@selector(runTaskAtIndex:)
toTarget:self
withObject:[NSNumber numberWithInt:i]];
}
}
- (void)runTaskAtIndex:(NSNumber *)taskIndex {
// 自动释放池
// 在使用NSThread或者NSObject的线程方法时, 容易出现内存泄露, 可以使用自动释放池。
@autoreleasepool {
// 运行任务
...
@synchronized(self) {
--taskCount;
if (taskCount == 0) {
}
}
// 更新taskCount
}
}
[self performSelectorOnMainThread:@selector(tasksDidFinish)
NSTimer
Timer(定时器)顾名思义是在固定的时间间隔被触发,然后给指定目标发送消息。有时间可以解释为指定时间后启动一个线程。
如果只是做到在指定时间后启动一个线程,可以尝试用NSObject 中的方法performSelector 建立一个简单的“计时器”:
[self performSelector: @selector(aSelector) withObject: nil afterDelay: 1.0];
但如果要能够取消和重新使用一个计时器,就需要创建和管理NSTimer。
NSTimer *timer = [[NSTimer alloc] initWithFireDate: [NSDate date] interval: 5 target: self selector: @selector(timerFire) userInfo: nil repeats: YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
在使用Timer的时候,需要注意循环引用的问题。
打破循环引用上面提到的执行任务可以拆解为以下四个元素:
- 执行方法主体(aSelector)
- 执行主体对象 (aTarget)
- 执行的时间
- 执行的地址 (线程)
但多线程并发的场景中,更复杂的是任务之间的依赖和资源的争夺,这些问题不解决会影响业务逻辑正确性和性能,为此iOS SDK中引入了 Operation 和 GCD解决方案。
Operation 对象
NSOperation的底层实现是GCD,比GCD更加面向对象和简单易用。
NSOperation是一个抽象的基类,表示一个独立的计算单元,可以为子类提供有用且线程安全的建立状态,优先级,依赖和取消等操作。我们不能直接使用它,但可以继承NSOperation并定制自己的操作,还可以使用它的子类NSInvocationOperation或者NSBlockOperation。
类图 Operation状态机对于SDK中原生的 NSInvocationOperation或者NSBlockOperation ,一个是使用 selector 回调,一个是使用 Block ,可根据实际情况选择。
我们经常会把NSOperation放到NSOperationQueue中使用。当NSOperation添加到NSOperationQueue的时候,NSOperationQueue总会单起一个线程然后调用start方法,如果总是把定义的NSOperation放在队列内执行,没有必要把其定义成异步操作。
NSOperationQueue是线程安全的,把多个Operation放置到单个队列中的时候,不用考虑加锁的问题。
使用NSOperation时我们可以为操作分解为若干个小的任务,通过添加他们之间的依赖关系进行操作,操作之间的依赖关系是指等待前一个任务完成后,后一个任务才能启动,且依赖关系可以跨线程队列实现。
let networkingOperation: Operation = ...
let resizingOperation: Operation = ...
// resizingOperation依赖networkingOperation,只有networkingOperation执行完成之后才能执行resizingOperation。
resizingOperation.addDependency(networkingOperation)
let operationQueue = OperationQueue.main
operationQueue.addOperations([networkingOperation, resizingOperation], waitUntilFinished: false)
注意: Opertation 可以根据其优先级属性定时运行,且定时运行的任务可以取消,这是GCD做不到的。
GCD
GCD(Grand Central Dispatch) 的主要组件有调度队列(Dispatch Queue),任务组,和信号量等。
调度队列是GCD中的核心功能,它能让使用者方便的异步或同步执行任何被封装的任务,执行任务的顺序永远等同于添加任务时的顺序,也就是FIFO,GCD中已经为我们提供了三种类型的调度队列。
调度队列 | 详细 |
---|---|
主队列 | 该类队列实质上是一个全局型串行队列,在该队列中执行的任务都是在当前应用的主线程中执行的。要谨慎使用主队列。 |
串行队列 | 该类型的队列也是一个串行队列,一次只能执行一个任务,当前任务完成之后才能执行下一个任务,这类队列通常作为私有队列使用。该类型队列主要处理同步操作,队列负责将每一个操作分配到单独的线程中 |
并行队列 | 该类队列可同时执行多个任务,但是执行任务的顺序依然是遵循先进先出的原则,这类队列通常作为全局队列使用。并行执行任务的个数是可变的,基于系统资源的使用情况而调整。系统预定义了四种全局性并发队列,且每个都有不同的优先级,当然开发者可以使用DISPATCH_QUEUE_CONCURRENT自定义并发队列。 |
注意:虽然该类型的队列一次只能执行一个任务,但是可以让多个串行队列同时开始执行任务,达到并发执行的任务的目的。
//根据优先级参数,获得全局性并发队列。
dispatch_queue_t dispatch_get_global_queue(long priority, unsigned long flags);
//优先级有四种
DISPATCH_QUEUE_PRIORITY_HIGH,
DISPATCH_QUEUE_PRIORITY_DEFAULT, DISPATCH_QUEUE_PRIORITY_LOW,
DISPATCH_QUEUE_PRIORITY_BACKGROUND.
//获得全局性和主线程绑定的主队列。
dispatch_queue_t dispatch_get_main_queue(void);
//获得自定义调度队列。
dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);
其中dispatch_queue_attr_t 选项有DISPATCH_QUEUE_SERIAL 和DISPATCH_QUEUE_CONCURRENT
//将任务添加到队列
dispatch_async(queue, block);
//释放自定义队列。
void dispatch_release(dispatch_object_t object);
- (IBAction)addNumbers:(id)sender {
// 获得全局并发队列和主队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t mainQueue = dispatch_get_main_queue();
taskCount = numTasks;
finalSum = 0;
for (int taskNum = 0; taskNum < numTasks; ++taskNum) {
// 下述任务将置于并发队列从而并发执行
dispatch_async(queue, ^{
for (int i = taskNum, partialSum = 0; i < arraySize; i += numTasks)
partialSum += array[i];
// 下述任务将置于主队列,在主线程同步执行。
dispatch_async(mainQueue, ^{
--taskCount;
finalSum += partialSum;
// 任务完成,更新界面。
if (taskCount == 0) {
self.totalLabel.text = [NSString stringWithFormat:@"%d", finalSum];
}
});
});
}
}
在使用串行队列(主队列也是串行队列)的时候,和dispatch_sync使用不当会造成死锁。
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"1");
});
这段代码会发生死锁,因为:
1.主线程通过dispatch_sync把block交给主队列后,会等待block里的任务结束再往下走自身的任务。
2.而队列是先进先出的,block里的任务也在等待主队列当中排在它之前的任务都执行完了再走自己。
这种循环等待就形成了死锁。所以在主线程当中使用dispatch_sync将任务加到串行队列是不可取的。
上面描述了如何把任务放进队列,而多个队列之间发生因果关系怎么办呢?dispatch_group应孕而生
dispatch_group有 dispatch_group_notify,dispatch_group_wait等
如果想在并行队列中所有的任务执行完成后在做某种操作怎么做呢? 可以试试dispatch_group_notify。
dispatch_queue_t dispatchQueue = dispatch_get_gloable_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
dispatch_group_t dispatchGroup = dispatch_group_create();
dispatch_group_async(dispatchGroup, dispatchQueue, ^(){
NSLog(@"dispatch-1");
});
dispatch_group_async(dispatchGroup, dispatchQueue, ^(){
NSLog(@"dspatch-2");
});
dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^(){
//并行队列执行结束后,会执行以下代码。
NSLog(@"末法时代...");
});
//当前线程等待,直到dispatchGroup内任务执行结束。
dispatch_group_wait(dispatchGroup, DISPATCH_TIME_FOREVER);
dispatch_barrier_async是在执行完前面的任务后它才执行,而且它后面的任务等它执行完成之后才会执行,先后顺序是按照添加到queue的次序,这里的队列是并行属性(只能是自定义并行队列),串行是没有必要考虑这个问题的。
//假设我们有四个读取任务,在第二三个任务之间有一个写入任务
let queue = dispatch_queue_create("com.gcd.kt", DISPATCH_QUEUE_CONCURRENT)
dispatch_async(queue, block1_for_reading)
dispatch_async(queue, block2_for_reading)
//保证没有读取脏数据
dispatch_barrier_async(queue, block_for_writing)
dispatch_async(queue, block3_for_reading)
dispatch_async(queue, block4_for_reading)
dispatch_once_t 也是GCD的一部分,创建单例的时候不可或缺。
+ (id)shareInstance {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_shareInstance = [[self alloc] init];
});
}
信号量dispatch_semaphore是持有计数的信号,和NSLock,@synchronized等锁类似,保证操作的线程安全。
//dispatch_semaphore_create方法创建一个信号量.
//dispatch_semaphore_wait方法表示一直等待直到信号量的值大于等于一,当这个方法执行后,会把第一个信号量参数的值减1。
//dispatch_semaphore_signal方法将信号量的值加1
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
NSMutableArray *array = [NSMutableArrayarray];
for (int index = 0; index < 10; index++) {
dispatch_async(queue, ^(){
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[array addObject:[NSNumber numberWithInt:index]];
dispatch_semaphore_signal(semaphore);
});
}
说到多线程的竞争,来看一下NSLock,NSLock可以保证一段代码在某一时刻只能有一个线程访问,这种互斥锁也是一种信号量,
BOOL moreToDo = YES;
NSLock *theLock = [[NSLock alloc] init];
...
while (moreToDo) {
/* 自己做准备 */
//为众多线程竞争的行为加锁,隔离竞争。
if ([theLock tryLock]) {
/* 众线程竞争的啪啪啪行为 */
//释放锁
[theLock unlock];
}
}
互斥锁也可以尝试POSIX互斥锁。
pthread_mutex_lock(&mutex);
// 干活中.
pthread_mutex_unlock(&mutex);
@synchronized指令是Objective-C代码里实现互斥锁的快捷方式,但不需要明确定义锁或锁定对象。
- (void)myMethod:(id)anObj
{
@synchronized(anObj)
{
//这一块都受到@synchronized指令的保护
}
}
结合上面的代码段,在两个不同的线程中,如果是传递两个不同的参数这样是没有起到锁的目的,每一个线程都会顺利同时执行。
而如果传递的是同一个对象作为参数,就是起到加锁的目的,一个线程获得了锁,同时阻隔另一个线程执行被保护的代码段。
Volatile 变量
有时候编译器为了提供性能,会把变量值保存在寄存器。如果变量时本地变量,这是优化是合理的。但如果这变量可以被其他线程访问,这种优化则会让其他线程忽略变量值可能发生的变化。
使用关键词Volatile会强迫编译器每次都从内存加载变量值,保证使用最新的值。
由于Volatile会降低编译器优化的能力,要节制地使用它。
更多如何实现同步参见官方文档。
获取更多内容请关注微信公众号豆志昂扬:
- 直接添加公众号豆志昂扬;
- 微信扫描下图二维码;