在iOS的开发中,多线程编程显得尤为重要,把一些耗时费力的工作放在多线程中去执行有助于提高主线程的响应速度,带来更好的用户体验;随着下一代iPhone6即将推出,有传言其所采用的是四核A8处理器,多线程编程能更好的提高程序的性能。

Apple提供了不同的技术来支持多线程,除了跨平台的pthread之外,还有NSThread、NSOperationQueue、GCD等,对于pthread编程,hrchen推荐了一本不错的书Programming With POSIX Threads. Part1&2主要讲解NSThread和NSRunLoop,对这些知识的掌握将有助于我们学习后面的GCD等技术。

NSThread

线程(thread)是构成进程的子单元,操作系统调度器对线程进行单独的调度。包括GCD和NSOperationQueue在内的并发编程API都是基于线程构建的。NSThread是Objective-C对pthread的一个封装,这样对使用者而言就更面向对象一些,比如可以去创建一个NSThrea的子类。

使用NSThread管理的一个线程的生命周期大致如下所示:

  • 创建一个线程
    • 使用NSThread类方法
    • 使用NSThread实例方法
    • 使用NSObject的实例方法
    • 使用NSThread的子类
  • 创建线程入口函数
    • 创建自动释放池 -(必须
    • 设置异常处理 - (try/catch)
    • 配置线程的属性(name、stackSize、threadDictionary)和优先级
    • 设置Run Loop
  • 运行线程
  • 中断或退出线程

1. 创建一个线程

  • 使用NSThread类方法
1
[NSThread detachNewThreadSelector:@selector(threadMainRoutine:) toTarget:self withObject:@"obj"];
  • 使用NSThread实例方法
1
2
3
NSThread *aThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMainRoutine:) object:@"obj"];
self.myThread = aThread;
[aThread start];
  • 使用NSObject的实例方法
1
[self performSelectorInBackground:@selector(threadMainRoutine:) withObject:@"obj"];
  • 使用NSThread的子类
1
2
DPCustomThread *cThread = [[DPCustomThread alloc] init];
[cThread start];
1
2
3
- (void)main{
[self threadMainRoutine:@"obj"];
}

具体代码可以通过文章后面的链接下载.

  • 创建一个线程的主要成本包括构造内核空间(大约1KB,wired memory)栈空间(默认主线程1MB,多线程512KB,空间大小可以使用- (void)setStackSize:(NSUInteger)s)来进行设置,s的单位是bytes,注意栈空间必须是4KB的倍数,且最小是16KB,即s>1024*4*i(i>=4),创建线程大约需要90ms的时间(由于底层内核的支持,Operation Objects可使用内核的常驻线程池里来节省创建时间)。

  • 第一种方法和第三种方法是一样的,都会直接生成一个线程。

  • 第二种和第四种方法创建的线程好处是可以拥有线程的对象,因此可以使用performSelector:onThread:withObject:waitUntilDone:在该线程上执行方法,这是一种非常方便的线程间通讯的方法(相对于设置麻烦的NSPort用于通讯),所要执行的方法可以直接添加到目标线程的Runloop中执行。Apple建议使用这个接口运行的方法不要是耗时或者频繁的操作,以免子线程的负载过重。(第一中和第三种方法也可以通过获取当前线程来间接的来拥有线程的对象)

  • 上面四种方法生成的子线程都是detached状态,即主线程结束时这些线程都会被直接杀死;如果要生成joinable状态的子线程,只能使用pthread接口啦。

  • 如果需要,可以设置线程的优先级(-setThreadPriority:),尽量还是使用默认的优先级,这样就不会产生优先级反转的问题;如果要在线程中保存一些状态信息,还可以使用到-threadDictionary得到一个NSMutableDictionary,以key-value的方式保存信息用于线程内读写或者线程间传递参数,同时这也是Objective-C实现TLC(thread-local storage)的方式

2. NSThread的入口方法

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
- (void)threadMainRoutine:(id)object{
@autoreleasepool {
NSLog(@"thread start");
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)
{
// 线程要执行的任务
// 如果完成了可设置moreWorkToDo = NO
[self threadTask];

// 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];
}
}
NSLog(@"thread exit");
}
  • 必须创建一个autoreleasepool,因为多线程不会自动创建,如果线程中需要创建大量的对象,可以在Run Loop的每次迭代处创建再创建一个autoreleasepool

  • 如果要捕获异常,可以在线程入口处使用try { } catch { }

  • 如果多线程处理一次即结束,则无需设置Run Loop;如果要循环处理事件,则使用Run Loop是最好的一种方式;如果线程中使用了定时期,则必须要启动Run Loop。

  • 多线程的正常退出应该使用标志位来进行判断,让线程自然退出。

3. NSThread 的接口

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// nitializing an NSThread Object
– init
– initWithTarget:selector:object:

// Starting a Thread
+ detachNewThreadSelector:toTarget:withObject:
//1.运行 -main 2.如果是第一个多线程,发出通知:NSWillBecomeMultiThreadedNotification
– start
– main

// Stopping a Thread
+ sleepUntilDate:
+ sleepForTimeInterval:
//类方法,强制退出当前线程,发出NSThreadWillExitNotification通知,不建议使用,因为没有机会做清理工作
+ exit
//与 isCanceled配合使用,可用于启动RunLoop的外层循环判断标志
– cancel

// Determining the Thread’s Execution State
– isExecuting
– isFinished
– isCancelled

// Working with the Main Thread
+ isMainThread
– isMainThread
+ mainThread

// Querying the Environment
+ isMultiThreaded
+ currentThread
+ callStackReturnAddresses
+ callStackSymbols

// Working with Thread Properties
– threadDictionary
– name
– setName:
– stackSize
– setStackSize:

// Working with Thread Priorities
+ threadPriority
– threadPriority
+ setThreadPriority:
– setThreadPriority:

4. NSThread 的使用注意事项

  • 1.)避免直接使用NSThread,Apple推荐使用性能更好的并发编程:GCD和Operation Queue

  • 2.)线程消耗了系统的宝贵资源,有些可能是wired memeory,应该合理分配任务到每个线程,保证长时间运行和运行成效(不能让一个线程长时间的处于锁的的等待什么的这类操作)。可以去打断一个长时间处于空闲状态的线程,这样有助于减少内存的占用

  • 3.)避免使用不是线程安全的共享资源,如果非使用不可,避免冲突最简单容易的方法是每个线程都使用一个副本!加锁/原子操作同样有性能损耗,所以把避免资源争夺放在首位可以同样得到高性能的程序。

  • 4.)对UI进行的所有操作都放在主线程中运行,这样就避免了处理窗口绘图和用户事件的同步问题。

  • 5.)如果想在程序退出的时候在多线程中保存数据,使用POSIX API创建非独立线程(可连接线程)及在applicationShouldTerminate中延迟退出。

  • 6.)关于异常处理,每一个线程都有自己的堆栈,且一个线程是捕捉不到其他线程的异常的,所以每一个线程都负责捕捉自己的异常,对异常的处理一般是告诉其他线程当前线程发生了什么,然后可以选择继续执行,等待指示或者直接退出,如果捕捉异常失败,则可能引起进程的Crash。

  • 7.)不到万不得已,不要使用函数去中断一个线程,让线程自然结束,强行中断可能会妨碍线程的自清理,发生内存泄漏或其他问题。

  • 8.)对于类库开发者而言,由于不知道使用者是不是在多线程中使用,只能假设应用程序的调用是在多线程中的,或者是在多线程中切换的,所以你应该总是在临界区进行加锁的处理。

其实这个也是不建议直接使用NSThread的原因,比如你创建了4个线程,而使用的框架也做了同样的事情,最终导致线程数量急剧增加。

参考资料

下载代码