OSX和iOS的每个进程(应用程序)都是由一个甚至多个线程组成,每个线程都单独代表了程序代码执行路径。main
程序可以生成额外的线程,每个线程执行特定功能的代码。
当我们程序生成一个新的线程时,该线程会成为程序进程空间(process space)内的独立实体。每个线程都有自己的运行堆栈,并由内核来调度独立的运行时间片。一个线程可以和其他线程或者进程通信,执行I/O操作,甚至执行任何你想要它完成的任务。因为它们处于相同的进程空间。所以一个独立程序里,其所有线程共享相同的虚拟内存空间,并且具有和进程相同的访问权限。
2.1 线程成本
线程会占用你应用程序(和系统的)的内存和性能方面的资源。每个线程都需要在内核的内存中和程序的内存中分配相应大小的空间。使用有线内存(Wired Memory)将“管理线程和协调调度”的核心数据存储于内核之中。每个线程的相关数据和线程所需的堆栈空间都存储于相应程序的内存中。
在线程第一次创建的时候,大部分相应的数据会一起创建并初始化。
正是因为上诉操作需要和内核进行交互,以至于这个过程(线程创建)的代价是相对高昂的。
下表量化了用户在应用程序中创建新的线程所需的大致成本。 其中一些成本是可配置的,例如为次级线程调配堆栈空间。 创建线程所需的时间成本是彼此(下表中不同的item)之间比较出来的近似值。
线程创建时间会根据处理器负载,计算机的速度以及可用的系统和程序存储器的大小会有很大的不同。
Item | 大致的成本 | Notes |
---|---|---|
内核数据结构(Kernel data structures) | 大约1KB | 消耗的这一部分内存是用于存储线程相关的数据和它的属性,大部分被分配到有线内存(Wired Memory)中(不能分配到磁盘中)。 |
堆栈空间 | 次级线程:512KB;Mac OS X主线程:8MB;iOS主线程:1MB | 次级线程的最小堆栈空间为16KB,而且必须为4的倍数。在线程创建时对应的内存空间会被保留,但是只有在线程被使用的时候与该内存相关联的实际内存才会被创建。 |
创建时间 | 大约90ms | 这个值反映了从调用初始化方法创建线程到该线程开始执行入口点(entry point)事务的时间差值。 |
由于底层内核的支持,建议使用Operation Object创建线程(更快)。通过停留在内存中线程池来分配以避免每次创建都从头开始创建新的线程。并发编程指南
2.2 线程创建
相对来说,创建一个low-level的线程是比较简单的。不管在何种情况下,我们都需要一个函数或者方法来作为线程的主入口点(main entry point),而且我们必须要有一个可用的线程来启动新创建的线程(我的理解是:在只有主线程的程序中,如果我们要创建一个新线程而且要启动它,我们就可以用主线程来启动,后面就可以此类推了)。
下面几个部分介绍了比较常用的创建线程的方法,根据不同方法创建的线程,它们会继承不同的默认属性集。有关如何配置线程的信息,请参阅配置线程属性。
2.2.1 使用NSThread创建线程
使用NSThread
类有两种方式来创建线程:
- 使用detachNewThreadSelector:toTarget:withObject:类方法来创建;
- 使用initWithTarget:selector:object:创建一个
NSThread
对象,然后调用它的start
方法(支持iOS和OS X10.5及其以后的版本);
这两种方法都是程序中创建了一个独立的线程,当线程退出时我们不需要在程序中处理后续操作,线程所占资源都是由系统自动回收。在OS X中创建多线程的办法一般都是用detachNewThreadSelector:toTarget:withObject:
方法,我们只需要为新生成的线程提供主入口方法的名称(Selector)和传递给线程的数据(这个数据是你想在线程启动时就有的数据)。
[NSThread detachNewThreadSelector:@selector(detachThread:) toTarget:self withObject:nil];
- (void)detachThread:(NSThread *)thread{
}
在OS X10.5之前,我们只能在新线程运行之后获取这个线程及其相关的属性。但是OS X10.5以后和iOS中,可以在新创建了NSThread对象之后但不立即创建新线程,因为这个改变我们可以在启动线程之前设置设置和获取相关的属性。后面我们就可以使用这个NSThread对象来启动其他线程。
第二个方法是使用initWithTarget:selector:object:方法,该方法和类方法detachNewThreadSelector:toTarget:withObject:
需要相同的开销,区别是通过该方法创建了NSThread实例,但是并没有马上启动它,我们可以显示的调用start
方法来启动该线程。
NSThread *new_thread = [[NSThread alloc] initWithTarget:self selector:@selector(detachThread:) object:nil];
[new_thread start]; /// 手动调用start方法来启动该线程
使用initWithTarget:selector:object:方法的另一个选择是子类化NSThread并重写它的
main()
方法,通过重写这个main()方法来可以实现我们线程的主入口点(重写之后可以不用调用)。
如果当前有一个正在运行的线程,我们可以为程序中的任何对象通过performSelector:onThread:withObject:waitUntilDone:
方法发送消息到该线程。在iOS和OS X10.5以后提供了在线程上执行Selector的能力,这是一种很方便的线程间通信的办法。使用这个方法发送的消息可以在其他线程上直接执行,并作为其Runloop正常处理的一部分(必须得让目标线程在它的Runloop中运行才行)。
不能在周期性或者频繁使用
performSelector:onThread:withObject:waitUntilDone:
方法来实现线程间通信
2.2.2 使用POSIX创建线程
iOS和OS X提供了基于C的创建线程的方法——POSIX,这个方法可以用于多种类型的程序上面(Cocoa和Cocoa touch),甚至是跨平台软件,调用pthread_create
来创建一个POSIX实例。
下面展示两个为了创建线程而自定义的两个函数,使用launchThread
函数创建一个线程,创建成功之后回调在posix_thread_mainroutine
函数中,因为POSIX默认将线程创建为可连接的,所以此示例更改线程的属性以创建一个独立的线程。 将线程标记为独立的,目的是为了让系统能够在线程退出时立即回收该线程的资源。(NSThread创建出来就是一个独立线程,不需要修改属性)
#include <assert.h>
#include <pthread.h>
void *posix_thread_mainroutine(void *data){
/// 线程创建成功的回调函数
return NULL;
}
void launchThread(void){
/**
/// pthread_attr_t
struct _opaque_pthread_attr_t {
long __sig;
char __opaque[__PTHREAD_ATTR_SIZE__];
};
/// pthread_t 结构指针 ///typedef struct _opaque_pthread_t *__darwin_pthread_t;
struct _opaque_pthread_t {
long __sig;
struct __darwin_pthread_handler_rec *__cleanup_stack;
char __opaque[__PTHREAD_SIZE__];
};
*/
pthread_attr_t attr;/// thread attribute
pthread_t posix_tread_id;
int returnVal;/// 结果flag
returnVal = pthread_attr_init(&attr);/// 初始化一个线程属性结构
assert(!returnVal);
returnVal = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);/// 设置线程分离状态,PTHREAD_CREATE_JOINABLE可连接线程,PTHREAD_CREATE_DETACHED独立线程
assert(!returnVal);
///这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,
///它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程id
///最简单的方法之一是可以在被创建的线程里调用pthread_cond_timewait函数
///具体可看后面的相关链接,里面有解释。
int thread_error = pthread_create(&posix_tread_id, &attr, posix_thread_mainroutine, NULL);
returnVal = pthread_attr_destroy(&attr);
assert(!returnVal);
if (thread_error != 0) {
/// 出现了问题
}
}
为了能够让新创建的线程和程序主线程进行通信,我们需要建立一条在两个目标线程之间的通信通道。基于C的程序,有很多通信方式,包括使用端口、条件、共享内存。对于长时间存活线程,我们应该创建一个线程间通信的机制来达到主线程检测其余线程的状态和程序退出时机。
2.2.3 使用NSObject生成线程
所有的对象都能够生成一个独立线程,performSelectorInBackground:withObject:
方法会创建一个独立的线程来执行特定的方法。例如:
[object performSelectorInBackground:@selector(backgroudMethod) withObject:nil];
这个方法和上面提到的NSThread中detachNewThreadSelector:toTarget:withObject:相同的。NSThread对象就是当前的对象的线程对象,selector和object参数一一对应。线程会根据默认的配置被立即创建并运行,在Selector中,我们必须要配置该线程。比如,创建自动释放池(在没有使用垃圾回收机制时),配置线程的Runloop(如果计划使用它)。
2.2.4 在Cocoa程序中使用POSIX线程
尽管NSThread类是作为Cocoa应用程序主要的接口,但是如果你使用POSIX写了相关的代码,你又不想去重写它,这种情况下就可以用POSIX线程。在使用POSIX线程的时候还是要遵守以下两个规则。
2.2.4.1 在Cocoa Framework中的保护
对于多线程程序而言,Cocoa Framework使用锁和其他内部的方式保障其正常的运行。在单线程的情况下,Cocoa不会去创建锁(因为这会降低性能,而且对于单线程而言锁存在的意义是什么。。。)直到使用NSThread。
如果你只使用POSIX方式来创建线程,Cocoa程序并不会收到任何关于当前处于多线程的通知。当发生这种情况就可能会导致崩溃。
为了能够让Cocoa知道你的程序是多线程的,具体的操作是:使用NSThread类相关的方法来生成一个线程,并让该线程立即退出。在Selector中不需要做任何事,这样做只是让Cocoa Framework知道这是多线程程序,并初始化锁需要的锁。
同时,我们可以使用isMultiThreaded来判断当前是否为多线程程序。
2.2.4.2 混合使用POSIX和Cocoa锁
在同一个程序中可以安全的混合使用POSIX和Cocoa锁,Cocoa锁和Condition对象是POSIX互斥体(mutexes)和conditions的封装。对于已经给定的锁,你必须使用相同的接口来创建和操作它。意思就是说,如果你使用pthread_mutex_init
函数创建的互斥体,我们就不能使用Cocoa NSLock对象来操作它,反之亦然。
2.3 配置线程属性
成功创建线程之后,我们可以在线程开始处理某些事情之前对它进行一些环境相关的配置。下面会列出我们可以为线程做哪些修改,以及在什么时候修改。
2.3.1 配置线程堆栈大小
每创建一个线程,系统都会在当前进程空间中分配一块指定的内存来作为目标线程的堆栈。该堆栈是用来管理在目标线程中声明的局部变量。具体的线程堆栈开销在“2.1 线程成本”中有列举出来。
我们必须要在创建线程完成之前改变指定线程的堆栈大小。每种线程相关的技术(NSThread、pthread、NSObject等等方法)都提供了一些方法来设置堆栈大小,其中NSThread设置堆栈大小的方法只在iOS和OS X 10.5及其以后可用(我们iOS反正是都可以用的)。
技术 | |
---|---|
Cocoa | 在“2.2.1 使用NSThread创建线程”中提到的两个创建线程的方法中,选择initWithTarget:selector:object:方法,在调用start方法之前,使用setStackSize: 来设置堆栈的大小。 |
POSIX | 创建一个pthread_attr_t结构,使用pthread_attr_setstacksize函数来改变默认堆栈大小。在创建线程时将pthread_attr_t传递给pthread_create函数。 |
多进程服务(Multiprocessing Services) | 在创建线程之前,将堆栈值传递给MPCreateTask函数。 |
/// 第一种
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(backgroudMethod:) object:nil];
[thread setStackSize:1024];
[thread start];
/// 第二种
pthread_attr_t attr;
pthread_t posix_tread_id;
int returnVal;
returnVal = pthread_attr_init(&attr);
assert(!returnVal);
returnVal = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
assert(!returnVal);
returnVal = pthread_attr_setstacksize(&attr, 1024);/// 在创建线程前设置堆栈大小
assert(!returnVal);
char *data;
data = "To ensure that a thread knows what work to do";
int thread_error = pthread_create(&posix_tread_id, &attr, posix_thread_mainroutine, data);
2.3.2 配置线程本地存储
每个线程都维护着一个包含健值对的字典,我们可以在该线程的任意地方访问这个字典。因此我们可以将线程执行期间的相关信息存储到这个字典中。
NSThread和POSIX使用不同的方式来存储这个线程字典,所以在使用的时候不要用混淆了。
- Cocoa中使用NSThread类的threadDictionary方法,返回值为NSMutableDictionary类型。也就是说我们可以修改相应的key-value。
- 在POSIX中,使用
pthread_setspecific
、pthread_getspecific
方法来设置和读取线程的key-values。
/// 第一种
NSMutableDictionary *thread_dictionary = [thread threadDictionary];
/// 第二种
pthread_key_t key;
pthread_key_create(&key, &pthread_key_create_completion);
char *specific_data = "pthread_setspecific";
pthread_setspecific(key, specific_data);
char *get_data = pthread_getspecific(key);
printf("specific is :%c\n",*get_data);
pthread_key_delete(key);
void pthread_key_create_completion(void *data){
printf("pthread_key_create_complerion\n");
}
2.3.3 设置线程的分离状态
我在前面的例子中使用POSIX创建线程时使用pthread_attr_setdetachstate
函数来设置线程的分离状态,其中最后一个参数的值在PTHREAD_CREATE_JOINABLE(下面会提到的可连接线程),PTHREAD_CREATE_DETACHED(独立线程)选一个。
高级线程技术默认创建的是独立线程(detached threads),但是使用POSIX默认创建可连接线程。在大多数情况下独立线程是更好的选择,因为独立线程在完成时系统会自动地释放该线程相关的数据。独立线程也不需要和程序进行显式的交互,也就是说线程返回结果的方式完全由自己决定。
对于可连接线程(joinable threads)而言系统是不会自动去回收该线程的资源,直到其他线程与该线程连接(连接过程可能会被该线程阻塞)。
我们可以将可连接线程看作子线程。它们依然是作为独立线程来运行,但是可连接线程必须要有其他线程连接之后该线程的资源才会被系统回收。
可连接线程提供一个能够让一个线程的数据传输到另一个线程的方法。在可连接线程退出之前,该线程可以将数据指针或其他返回值传递给pthread_exit
函数。另一个线程则通过调用pthread_join
函数来获得这些数据。
注意⚠️
在进程退出时,独立线程可以马上终止,但是可连接线程是不能的。每个可连接线程必须要在进程允许退出之前都被连接,因此在处理一些不能被中断的操作时应该选择可连接线程(例如保存数据到磁盘)。
POSIX是创建非独立线程的唯一方法,这里在说明一下:使用POSIX线程技术默认创建的可连接线程。但是在线程开始之后,我们可以调用pthread_detach
函数将可连接线程修改为独立线程。
2.3.4 设置线程优先级
对于大部分高级线程技术来说,默认都是创建独立的线程。内核在调度线程执行顺序时是根据线程的优先级高低而定的,处于较高优先级的线程并不是说它有特定的执行时间,而是它比低优先级线程更有可能被调度。
重要
通常来说,线程的优先级处于默认状态最好。增加某些线程的优先级同时也会增加低优先级饥饿的可能性(starvation),程序中如果必须要有相互影响的高优先级和低优先级的程,那么低优先级线程(饥饿线程)有可能会导致程序出现性能瓶颈。
解释一下线程饥饿,当一个线程因为cpu时间全部被其他线程(比如高优先级的线程)占用而得不到cpu运行时间,则说该线程当前处于饥饿状态。
Cocoa和POSIX都提供了修改线程优先级的方法。第一种是用NSThread的setThreadPriority:
类方法来为当前正在运行的线程设置优先级,第二种pthread_setschedparam方法来设置POSIX线程优先级。
/// NSThread
[NSThread setThreadPriority:...];/// 当前运行线程下运行,则设置当前线程的优先级
/// POSIX
struct sched_param param;
param.sched_priority = ...
pthread_setschedparam(pthread_self(), policy, ¶m);/// pthread_self()获取当前线程
2.4 编写线程入口点
大多数情况下,其他平台和OS X平台的线程入口点是相同的。初始化数据,然后做了一些其他事情抑或是设置一个RunLoop,最后在线程代码执行完成之后清理掉它们。
2.4.1 创建自动释放池
在Object-C程序中每个线程至少要有一个自动释放池。如果由程序来处理对象retain和release(内存管理模式),自动释放池能捕获由该线程自动释放的所有对象(将自动释放的对象push进池中),下面主要讨论垃圾回收机制和内存管理模式之间的关联。
如果程序使用垃圾回收机制(不是内存管理模式),并非一定要创建自动释放池(如果存在自动释放池那也不是坏事,但它们大部分会被忽略掉)。当然也允许垃圾回收机制和内存管理模式同时存在于某代码模块中,这种情况下,必须要有自动释放池来支持内存管理模式的代码(如果启用垃圾回收则自动忽略)。
总结:内存管理模式必须要有自动释放池,如果程序选择垃圾回收机制之后自动释放池就不是必须的,垃圾回收和内存管理模式可以共存。
如果程序是内存管理模式(不是垃圾回收机制),那么在线程的入口点首先就要创建一个自动释放池。同样的,在线程最后不要忘了销毁这个自动释放池。该自动释放池能够捕获自动释放的对象,在线程退出之后释放掉它们。(Useing Autorelease Pool Blocks)
/// 官方例子
- (void)myThreadMainRoutine
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // Top-level pool
// Do thread work here.
[pool release]; // Release the objects in the pool.
}
/// 当前版本例子
@autoreleasepool {
}
使用Runloop的线程每次该Runloop创建和释放自动释放池,频繁地释放对象会导致程序的内存占用过大。
2.4.2 设置程序异常处理
创建的线程应当具备捕获异常的能力,可以在线程入口点使用try/catch来捕获任何未知的异常并提供合适的响应。当运行工程时可以使用C++或者Objective-C来处理异常,更多关于程序异常处理见异常编程知识点。
2.4.3 设置Runloop
当在一个单独运行的线程执行事务时,我们有两个办法,第一个办法是添加一个长任务,很少甚至没有中断的情况,在任务完成时退出线程。第二办法就是将线程放入Runloop中,动态处理请求。
OS X和iOS为每个线程都提供了Runloop的支持,程序自动为主线程启动了Runloop,但是如果是自己创建的线程则需要配置Runloop并启动它,下一章来具体的讲Runloop。
2.5 终止线程
退出线程的推荐处理方式是在线程的入口点中正常退出。虽然Cocoa和POSIX等都提供了直接杀死线程的办法([NSThread exit],pthread_exit),但是非常不建议这么做,因为直接杀死线程会导致线程出现内存泄露,以及其他资源有可能无法正常清理的问题。
如果能在事先就知道在操作过程中要终止线程,就应在线程开始时就设计好相应的响应或者退出消息。当检查让线程退出的消息时,线程会执行任何相应清理并正常退出。
使用Runloop的input Source来响应线程退出消息。
- (void)threadMainRoutine{/// 这个为线程入口点函数,正常情况下该函数只会运行一次,然后线程会退出。
BOOL moreWorkToDo = YES;
BOOL exitNow = NO;
NSRunLoop* runLoop = [NSRunLoop currentRunLoop];
NSMutableDictionary* threadDict = [[NSThread currentThread] threadDictionary];/// 2.3.2中介绍的线程字典
[threadDict setValue:[NSNumber numberWithBool:exitNow] forKey:@"ThreadShouldExitNow"];
// Install an input source.
[self myInstallCustomInputSource];
while (moreWorkToDo && !exitNow){/// 这里就是为了能够让线程不会马上退出,而是根据while循环中的条件来判断是否继续运行Runloop。
///如果runloop继续运行那么thread就不会退出。
///如果不进入这个循环体,runloop就不会运行,那么马上thread就会退
// 这个意思就是让runloop马上运行,做完需要的事儿之后进入休眠之后,然后等待while循环的下一次运行。
[runLoop runUntilDate:[NSDate date]];
// Check to see if an input source handler changed the exitNow value.
exitNow = [[threadDict valueForKey:@"ThreadShouldExitNow"] boolValue];
}
}
本人总结TIPS
创建时我们需要两个操作:1.需要为创建的新线程提供一个主入口函数;2.需要一个可用线程来启动这个新线程。
在设置次级线程的堆栈空间时,大小必须是4的倍数(我看linux中pthread时说的是16的倍数,都差不多的)
使用NSThread的两个方法创建的线程是独立线程,当线程退出时我们不需要在程序中处理后续操作,线程所占资源都是由系统自动回收。
在iOS和OS X10.5以后,我们可以在线程启动之前
Set
和Get
相关的属性;我们可以使用
performSelector:onThread:withObject:waitUntilDone:
方法实现线程间通信;为了能够让Cocoa Framework生成对应的锁,我们至少需要创建一个不做任何事的线程(NSThread类);
堆栈用来管理在该线程中声明的局部变量;
如果想要改变指定线程的堆栈大小,我们就必须要在创建线程之前完成;
独立线程在完成时系统会自动地释放该线程相关的数据,但是可连接线程就不会;
在处理一些不能被中断的操作时应该选择可连接线程;
高优先级的线程比低优先级线程更容易被调度;
处于内存管理模式下,在线程入口点首先就要创建一个自动释放池;