iOS-异步编程详解

本文对iOS系统上的异步调用方式进行了深入的讲解。

异步来源

早期,计算机在一个单位时间内的最大运行数目取决于CPU的周期速度。但是,发热和其他物理因素限制了处理器的最大周期速度,因此,芯片工程师尝试通过增加处理器的核数量,从而可以同时执行多个指令。但是,如何利用这些额外的核成为一个需要解决的问题。

现在的操作系统,都可以在任意时间内运行上百甚至更多的程序,因此将这些程序分布在不同的核上运算来利用多核变成可能的。但是,大多数的程序是系统后台驻留或者后台应用,只需要占用很少的处理时间,因此,如何更高效地利用多核成为一个新的需求。

传统的处理方式是建立多个线程。然而,随着核的数量的增加,多线程方式存在很多问题。最大问题是,线程的代码不能随着核的数量变化而变化,无法实时创建跟核数量一样多的线程,且运行正确。另外,多线程之间保证正确地交互也成为一个难题。

异步方法

为了避免开发者疲于解决上述的多线程编程难题,iOS设计了一些异步方法来解决异步问题。

异步函数

异步函数通常用于处理需要长时间的task,异步函数会自动调用一个后台线程,并在运行完成通过通知的形式通知调用者。

举例:

[NSObject performSelectorInBackground:@selector(doSomething) withObject:nil];

GCD

GCD(Grand Central Dispatch)在系统级别处理线程管理,而无需开发者编写代码。开发者只需要将task加入到一个dispatch queue中即可。GCD会自动创建需要的线程去处理task。因为线程处理是系统级别的,因为GCD提供了一种更高效地处理task的方法。

GCD处理task的方式是先进先出,按照添加的顺序执行。

*Type

下面是三种类型:

Type Description
Serial(private dispatch queues) 每次只执行一个task
Concurrent(global dispatch queues) 每次执行一个或多个task,数量取决于系统条件
Main dispatch queues 主线程的queue,与主线程的RunLoop进行交互

当两个线程同时访问一个共享资源时,与其用锁来控制,不如用Serial,保证每次只有一个Operation访问到该资源,效率更高。

*Init

(1)Serial dispatch queue

Serial dispatch queue需要自己创建:

dispatch_queue_t bQueue = dispatch_queue_create("com.baidu.carlife", NULL);

第一个参数是ID,第二个参数为NULL即可。

(2)Global dispatch queue

系统预定义了四个Queue,可以直接调用:

dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

第一个参数是优先级值,有high、default、low、background四种,分别对应四个Queue,第二个参数为0即可。

(3)Main dispatch queue

Main dispatch queue已经定义好,直接调用:

dispatch_queue_t cQueue = dispatch_get_main_queue();

*Add

当前线程无需等待执行完成时,用以下方法:

dispatch_async
dispatch_async_f

如果需要block当前线程,调用以下方法:

dispatch_sync
dispatch_sync_f

举例:

dispatch_queue_t aQueue = dispatch_queue_create("com.baidu.carlife", NULL);
dispatch_async(aQueue, ^{
    NSLog(@"Block one..");
});
NSLog(@"Block one may or may not finish..");
dispatch_sync(aQueue, ^{
    NSLog(@"Block two..");
});
NSLog(@"Both finish..");

运行结果:

 Block one may or may not finish..
Block one..
Block two..
Both finish..

注意:不要在dispatch_sync当前正在执行task里面调用,dispatch_sync,否则会出现死锁。

dispatch_queue_t queue = dispatch_queue_create("com.baidu.carlife", nil);
dispatch_sync(queue, ^{
    dispatch_sync(queue, ^{
        NSLog(@"Dead lock");
    });
});

如果涉及到UI的更新,需要切回到主线程执行:

dispatch_async(dispatch_get_main_queue(), ^{
    [_tableView reloadData];
});

*Storing Custom Context

Diapatch Queue可以保存一些自定义的上下文信息,参考下面的例子。

*Clean Up Function

Serial dispatch queue可以设置一个回收函数,当它设置了Context,并被析构时,将调用该函数。没有设置Context,将不会调用该函数。

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
void myFinalizerFunction(void *context)
{
MyDataContext* theData = (MyDataContext*)context;

// Clean up the contents of the structure
myCleanUpDataContextFunction(theData);

// Now release the structure itself.
free(theData);
}

dispatch_queue_t createMyQueue()
{
MyDataContext* data = (MyDataContext*) malloc(sizeof(MyDataContext));
myInitializeDataContextFunction(data);

// Create the queue and set the context data.
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.CriticalTaskQueue", NULL);
if (serialQueue)
{
dispatch_set_context(serialQueue, data);
dispatch_set_finalizer_f(serialQueue, &myFinalizerFunction);
}

return serialQueue;
}

*Loop Iterations Concurrently

对于for循环,如果每次的循环结果是独立的,不互相依赖,可以使用dispatch queue的以下方法使其同时运行,提高效率。

dispatch_apply
dispatch_apply_f

注意,上述两个方法是同步的,会阻塞当前线程,但是其执行block代码块是异步的。
举例:

NSLog(@"Begin");
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(5, queue, ^(size_t i) {
    NSLog(@"%ld", i);
});
NSLog(@"End");

运行结果为:

Begin
1
2
0
3
4
End

*Suspending & Resuming

挂起Queue,挂起并不能中断一个已经在运行的task,只会阻止新的Operation开始运行,可以调用方法进行挂起或恢复:

dispatch_suspend(queue);
dispatch_resume(queue);

*Dispatch Semaphores(信号量)

Dispatch Semaphores可以用来控制对于有限资源的访问,例如每个应用只能访问有限数量的文件修饰符,但是很多线程要访问,例如一个停车场有三个车位,而有五辆车要停。

举例:

1
2
3
4
5
6
7
dispatch_semaphore_t fd_sema = dispatch_semaphore_create(getdtablesize() / 2);

dispatch_semaphore_wait(fd_sema, DISPATCH_TIME_FOREVER);
fd = open("/etc/services", O_RDONLY);
close(fd);

dispatch_semaphore_signal(fd_sema);

创建Semaphores时,必须设置一个数量变量。每次wait,将减一,而signal,则加一。如果值为负,将挂起当前线程。

*Wait on Groups of Tasks

有时候,需要等待一组异步task完成后,才能进行下一步操作,可以使用dispatch group来完成。

举例:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
    NSLog(@"Group work..");
});

NSLog(@"Before group..");
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"After group..");

运行结果:

Before group..
Group work..
After group..

*Timer

除了主线程自动设置了RunLoop外,其他线程是没有设置的,这时如果直接调用

- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;

或者新建NSTimer,都会发现,该方法无效的情况。解决办法是:

dispatch_time_t time=dispatch_time(DISPATCH_TIME_NOW, 5ull *NSEC_PER_SEC);
dispatch_after(time, queue, ^{
    // Do something..
    }
});

Operation Queues

Operation Queues很像dispatch queue,它会自动处理进行线程管理,开发者只需要定义task,并加入到Operation Queues中即可。

Operation Queues是一种处理NSOperation的队列。

*Init & Add

Operation Queue创建并addOperation后,即开始异步运行Operation。

注意,尽管可以创建任意多个Queue,但并不代表可以同时运行,取决于可获取的CPU核和系统的负载。

举例:

_op = [CustomOperation new];

NSOperationQueue *queue = [NSOperationQueue new];
[queue addOperation:_op];
[queue addOperations:[NSArray arrayWithObject:[CustomOperation new]] waitUntilFinished:NO];
[queue addOperationWithBlock:^{
    // Do work..
}];

如果要限制Operation Queue每次只执行一个Operation,可以在add前设置:

[queue setMaxConcurrentOperationCount:1];

*Cancel

当Operation添加到Queue后,不能进行移除操作,要取消,只能调用以下方法。

Operation:cancel
Queue:cancelAllOperations

*WaitForFinished

当需要waitForFinished时,调用以下方法:

Operation:waitUntilFinished
Queue:waitUntilAllOperationsAreFinished

注意,不要在主线程中调用,否则会阻塞主线程。

*Suspending & Resuming

挂起Queue,挂起并不能中断一个已经在运行的task,只会阻止新的Operation开始运行,可以调用方法进行挂起或恢复:

setSuspended:

性能Tips

  • 通过计算直接得到值,而不要从内存取:计算值直接使用寄存器,访问速度比内存快;
  • 同步改为异步:如果同步任务是因为依赖共享资源,调整资源架构,可能可以通过拷贝资源来实现异步;
  • 避免使用锁:使用dispatch queues和operation queues,而尽量避免使用锁;
  • 使用系统framework:系统frameworks大多数有异步方法,内部是使用线程和其他技术实现的,而不需要自己编码。