iOS-多线程详解

本文对iOS系统上的多线程进行了深入的讲解。

多任务的线程处理方式

对于不同的task任务,处理的方式和策略可以参考以下表格。

Technology Description
Operation objects 用队列管理一组task任务,内部使用一或者多个线程
GCD(Grand Central Dispatch) 用队列管理一组task任务,内部使用一个线程
Idle-time notifications 对于短且低优先级的task,可以通过向NSNotificationQueue发送一个包含NSPostWhenIdle选项的通知,当RunLoop处于Idle状态时,系统会处理该task
Asynchronous functions 一些带有异步功能的方法
Timer 在一个线程中利用RunLoop的TimerSources进行执行,一般在主线程中使用
Separate processes 用多进程来处理事务

线程库

iOS和OS X系统提供了以下的线程库:

Technology Description
Cocoa threads OC编写的库,包含NSThread类
POSIX threads C编写的库,包含POSIX Threads
Multiprocessing Services 遗留的库,尽量避免使用

线程的成本

空间成本

每个线程需要占用内核和应用两边的内存空间。当线程被创建时,这两部分的内存空间即被占用。

Item Approximate cost Notes
Kernel data structures 约1KB 用于存储线程本身的结构和属性,不能被分页到硬盘
Stack space 1MB (iOS主线程) 8MB(OSX 主线程)512KB(创建的线程) 最小的创建线程的栈空间大小是16KB,且必须是4KB的倍数,内存空间在创建时即被分配,但是只有需要用时,真实的内存分页才会被创建)
Creation time 约90ms 从开始创建线程到线程入口开始执行的时间

时间成本

而线程创建的时间,取决于处理器的负载、计算机的速度以及程序的内存等,只能进行粗略的估算,参考上表。

产品成本

多线程可能增加产品的不稳定性、开发的复杂性,应谨慎考虑。

线程创建

NSThread

NSThread有两种创建方式。

1
2
3
4
5
//第一种
[NSThread detachNewThreadSelector:@selector(myThreadMainMethod:) toTarget:self withObject:nil];
//第二种
NSThread* myThread = [[NSThread alloc] initWithTarget:self selector:@selector(myThreadMainMethod:) object:nil];
[myThread start]; // Actually create the thread

POSIX Threads

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
#include <assert.h>
#include <pthread.h>

void* PosixThreadMainRoutine(void* data)
{
// Do some work here.
return NULL;
}

void LaunchThread()
{
// Create the thread using POSIX routines.
pthread_attr_t attr;
pthread_t posixThreadID;
int returnVal;

returnVal = pthread_attr_init(&attr);
assert(!returnVal);
returnVal = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
assert(!returnVal);

int threadError = pthread_create(&posixThreadID, &attr, &PosixThreadMainRoutine, NULL);
returnVal = pthread_attr_destroy(&attr);
assert(!returnVal);
if (threadError != 0)
{
// Report an error.
}
}

通过NSObject派生线程

NSObject可以直接创建一个线程,并将对应的方法作为线程的执行入口。

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

线程属性配置

设置线程的栈空间

Technology Description
Cocoa 在start前用setStackSize:方法设置
POSIX 创建pthread_attr_t结构体和使用pthread_attr_setstacksize方法设置栈空间大小,在创建时传递给pthread_create方法
Multiprocessing Services MPCreateTask参数设置

设置线程属性

1
2
NSThread:threadDictionary
POSIX:pthread_setspecific pthread_getspecific

设置线程独立状态

一般情况下,线程都是detached(独立)的,当task完成时可立即被释放。必要时,也可以使用joinable线程,joinable意味着该线程只有被另一个线程join的时候,才会被释放。

1
2
joinable线程可以通过pthread_exit传递数据给调用pthread_join的线程。
POSIX提供了设置线程独立状态的方法:pthread_attr_setdetachstate

设置线程的优先级

可以通过以下函数来设置线程优先级。

1
2
NSThread:setThreadPriority
POSIX:pthread_setschedparam

注意:即使设置了线程的优先级,也不能保证该线程一定会先被执行,只是有更大的可能性。

编写线程入口方法

使用ARC

ARC可以自动对线程的资源进行释放。

异常处理

线程的异常如果没有通过try/catch进行捕获,将会导致程序crash。

建立一个RunLoop

RunLoop可以帮你处理多任务,参考iOS-RunLoop详解;

终止线程

尽管每种线程库都提供了终止线程的方法,但是最好让线程自己执行到结束,自然终止,有利于线程自己管理空间释放。

NSThread可以使用RunLoop来检测自身是否可以终止。

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
- (void)threadMainRoutine
{
BOOL moreWorkToDo = YES;
BOOL exitNow = NO;
NSRunLoop* runLoop = [NSRunLoop currentRunLoop];

// Add the exitNow BOOL to the thread dictionary.
NSMutableDictionary* threadDict = [[NSThread currentThread] threadDictionary];
[threadDict setValue:[NSNumber numberWithBool:exitNow] forKey:@"ThreadShouldExitNow"];

// Install an input source.
[self myInstallCustomInputSource];

while (moreWorkToDo && !exitNow)
{
// Do one chunk of a larger body of work here.
// Change the value of the moreWorkToDo Boolean when done.

// Run the run loop but timeout immediately if the input source isn't waiting to fire.
[runLoop runUntilDate:[NSDate date]];

// Check to see if an input source handler changed the exitNow value.
exitNow = [[threadDict valueForKey:@"ThreadShouldExitNow"] boolValue];
}
}