线程编程指南翻译第二篇(线程管理)

文档地址

示例代码下载

线程管理

在OS X和iOS系统中每个进程(应用程序)都有一个或者多个线程构成,每个线程表示着执行应用程序代码的单个路径。每个应用程序都以单线程启动,这个线程运行应用程序的main函数。应用程序能够生成一些执行特定函数代码的额外线程。

当应用程序生成一个新的线程,该线程在应用程序的进程空间里成为一个独立的实体。每个线程都有自己的执行堆栈,并由内核单独调度运行时。线程可以与其他线程和其他进程通信,执行I / O操作,并执行您可能需要执行的任何操作。由于它们位于同一进程空间内,因此单个应用程序中的所有线程共享相同的虚拟内存空间,并具有与进程本身相同的访问权限。

线程成本

线程在内存使用和性能方面对您的程序(和系统)来说是一个真正的成本。每个线程都需要在内核内存空间和程序的内存空间中分配内存。其核心结构需要使用有线内存去管理线程和派发存储在内核中的坐标。线程的堆栈空间和每个线程的数据存储在程序的内存空间中。大多数这些结构都是在您第一次创建线程时创建和初始化的 - 由于与内核的必要交互,该过程可能相对昂贵。其中一些成本是可配置的,例如为辅助线程分配的堆栈空间量。创建线程的时间成本是粗略的近似值,应该仅用于相互比较。线程创建时间可能会有很大差异,具体取决于处理器负载,计算机速度以及可用系统和程序内存的数量。

表2-1 线程创建成本

条目 近似成本 说明
内核数据结构 大约1 KB 此内存用于存储线程数据结构和属性,其中大部分被分配为有线内存,因此无法分页到磁盘。
堆栈空间 512 KB(辅助线程)8 MB(OS X主线程)1 MB(iOS主线程) 辅助线程允许的最小堆栈大小为16 KB,堆栈大小必须为4 KB的倍数。创建线程时在进程空间中会为线程留出此内存空间,但是实际的内存地址并不是在线程创建的时候关联上的,而是在需要的时候。
创建时间 大约90微秒 这个值反映的时间是从线程创建的初始调用到线程入口点的例程开始执行之间的时间。这些数据是通过分析在基于因特尔的iMac上使用2 GHz Core Duo处理器和运行于OS X v10.5上的1 GB RAM创建线程期间生成的平均值和中值来确定的。

注意:由于底层内核的支持,operation objects通常能够更快的创建线程。他们不是每次都从头开始创建线程,而是使用已驻留在内核中的线程池来节省分配时间。有关使用操作对象的更多信息,请参阅“ 并发编程指南”。

编写线程代码时要考虑的另一个成本是生产成本。设计线程化应用程序有时可能需要对组织应用程序数据结构的方式进行根本性更改。进行这些更改以避免使用同步可能是必要的,这本身可能会对设计不佳的应用程序造成巨大的性能损失。设计这些数据结构以及调试线程代码中的问题可能会增加开发多线程应用程序所需的时间。如果线程花费太多时间等待锁定或什么都不做,然而避免这些成本可能会在runtime中产生更大的问题。

创建一个线程

创建低级线程相对简单。在所有情况下,您必须具有一个函数或方法来充当线程的主入口点,并且必须使用一个可用的线程例程来启动您的线程。以下部分显示了比较常用的线程技术的基本创建过程。使用这些技术创建的线程会继承一组默认属性,这些属性由使用的技术决定。有关如何配置线程的信息,请参阅配置线程属性。

使用NSThread

使用NSThread类创建线程有两种方法:

  • 使用detachNewThreadSelector:toTarget:withObject:类方法生成新线程。
  • 创建一个新的NSThread对象并调用其start方法。(仅在iOS和OS X v10.5及更高版本中受支持。)

这两种技术都会在应用程序中创建一个分离的线程 分离线程意味着线程退出时系统会自动回收线程的资源。这也意味着您的代码以后不必明确地与该线程连接。

因为OS X的所有版本都支持detachNewThreadSelector:toTarget:withObject:该方法,所以通常能在使用线程的现有Cocoa应用程序中找到它。要分离新线程,只需提供要用作线程入口点的方法名称(指定为选择器),定义该方法的对象以及要在启动时传递给线程的任何数据。以下示例显示了此方法的基本调用,该方法使用当前对象的自定义方法生成线程。

    [NSThread detachNewThreadSelector:@selector(myThreadMainMethod:) toTarget:self withObject:nil];

在OS X v10.5之前,主要使用NSThread该类来生成线程。虽然您可以获取一个NSThread对象并访问某些线程属性,但您只能在线程运行后从线程本身执行此操作。在OS X v10.5中,添加了对创建NSThread对象的支持,而不会立即生成相应的新线程。(此支持也可在iOS中使用。)此支持使得在启动线程之前获取和设置各种线程属性成为可能。它还可以使用该线程对象以后引用正在运行的线程。
NSThread在OS X v10.5及更高版本中初始化NSThread对象的简单方法是使用initWithTarget:selector:object:方法。此方法与detachNewThreadSelector:toTarget:withObject:方法获取完全相同的信息,并使用它来初始化新NSThread实例。但是,它不会启动该线程。要启动该线程,请显式调用线程对象的start方法,如以下示例所示:

    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(myThreadMainMethod:) object:nil];
    [thread start];

注意: 使用该initWithTarget:selector:object:方法的另一种方法是子类化NSThread并覆盖其main方法。您将使用此方法的重写版本来实现线程的主入口点。有关更多信息,请参阅NSThread类参考中的子类注释。

如果您有一个当前正在运行线程的NSThread对象,则应用程序中几乎任何对象可以将消息发送到该线程的一种方法是使用performSelector:onThread:withObject:waitUntilDone:方法。支持在线程(主线程之外)上执行选择器方法是在OS X v10.5中引入的,这是在线程之间进行通信的便捷方式。此支持也可在iOS中使用。)这种技术发送的消息被其他线程作为正常runloop处理的一部分直接执行。(当然这意味着目标线程已经在它的runloop中运行着,请参阅runloop。)以这种方式进行通信仍然需要某种形式的同步,但是这比在线程之间设置通信端口简单。

与其他线程通信选项列表,请参阅设置线程的分离状态。

使用POSIX线程

OS X 和 iOS使用POSIX线程API为创建线程提供基于C的支持。这种技术实际上可以用于任何类型的应用程序(包括Cocoa和Cocoa Touch应用程序),编写跨平台软件更加方便。POSIX例程用于创建线程,足够恰当地调用pthread_create函数。

清单2-1显示了两个使用POSIX调用创建线程的自定义函数。该LaunchThread函数创建一个新的线程,其主程序在PosixThreadMainRoutine函数中实现。因为默认情况下POSIX将线程创建为可连接,所以此示例更改线程的属性以创建分离线程。将线程标记为已分离使系统有机会在退出时立即回收该线程的资源。

清单2-1 在C中创建一个线程

void *PosixThreadMainOutine(void *data) {
    //在这做一些工作
    
    return NULL;
}
void LaunchThread(void *data) {
    //使用POSIX例程创建线程
    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, posixThreadMainOutine, data);
    
    returnVal = pthread_attr_destroy(&attr);
    assert(!returnVal);
    if (threadError != 0) {
        //抛出错误
        
    }
}

如果将前面列表中的代码添加到其中一个源文件并调用该LaunchThread函数,它将在您的应用程序中创建一个新的分离线程。当然,使用此代码创建的新线程不会做任何有用的事情。线程将启动并几乎立即退出。为了使事情更有趣,您需要向PosixThreadMainRoutine函数添加代码以执行一些实际工作。为了确保线程知道要做什么工作,您可以在创建时向其传递指向某些数据的指针。您将此指针作为pthread_create函数的最后一个参数传递。

要将新创建的线程中的信息传递回应用程序的主线程,您需要在目标线程之间建立通信路径。对于基于C的应用程序,有多种方法可以在线程之间进行通信,包括使用端口,条件或共享内存。对于长期存在的线程,您几乎应该始终设置某种线程间通信机制,以便为应用程序的主线程提供一种方法来检查线程的状态,或者在应用程序退出时将其关闭。

有关POSIX线程函数的更多信息,请参见pthread手册页。

使用NSObject生成线程

在iOS和OS X v10.5及更高版本中,所有对象都能够生成新线程并使用它来执行其中一个方法。performSelectorInBackground:withObject:方法创建一个新的分离线程,并使用指定的方法作为新线程的入口点。例如,如果您有一些对象(由myObj变量表示)并且该对象具有想要在后台线程中运行的doSomething方法,则可以使用以下代码执行此操作:

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

与用当前对象,选择器方法和参数对象作为参数调用NSThread的detachNewThreadSelector:toTarget:withObject:方法调用此方法的效果是一样的。使用默认配置立即生成新线程并开始运行。在选择器内部,您必须像处理任何线程一样配置线程。例如,您需要设置自动释放池(如果您没有使用垃圾收集)并配置线程的运行循环(如果您计划使用它)。有关如何配置新线程的信息,请参阅配置线程属性。

在Cocoa应用程序中使用POSIX线程

尽管NSThread该类是在Cocoa应用程序中创建线程的主要接口,如果这样做更方便的话,依然可以自由的使用POSIX线程。例如,如果已经拥有使用它们的代码并且不想重写它,则可以使用POSIX线程。如果计划在Cocoa应用程序中使用POSIX线程,仍应该了解Cocoa和线程之间的交互,并遵守以下各节中的准则。

保护Cocoa框架

对于多线程应用程序,Cocoa框架使用锁和其他形式的内部同步来确保它们的行为正确。但是,为了防止这些锁在单线程情况下降低性能,Cocoa不会创建它们,直到应用程序使用NSThread该类生成其第一个新线程。如果仅使用POSIX线程例程生成线程,Cocoa不会收到它需要知道您的应用程序现在是多线程的通知。当发生这种情况时,涉及Cocoa框架的操作可能会使您的应用程序不稳定或崩溃。

为了让Cocoa知道您打算使用多个线程,您所要做的就是使用NSThread类生成一个线程并让该线程立即退出。你的线程入口点不需要做任何事情。只是产生NSThread线程使用的行为足以确保Cocoa框架所需的锁定到位。

如果您不确定Cocoa是否认为您的应用程序是多线程的,您可以使用NSThreaddeisMultiThreaded方法来检查。

混合POSIX和Cocoa锁

在同一个应用程序中使用POSIX和Cocoa锁的混合是安全的。Cocoa锁和条件对象基本上只是POSIX互斥和条件的包装器。但是,对于给定的锁,必须始终使用相同的接口来创建和操作该锁。换句话说,您不能使用Cocoa NSLock对象来操作使用该pthread_mutex_init函数创建的互斥锁,反之亦然。

配置线程属性

在创建线程之后,有时在之前,您可能希望配置线程环境的不同部分。以下部分介绍了您可以进行的一些更改以及何时可以进行的更改。

配置线程的堆栈大小

对于您创建的每个新线程,系统会在进程空间中分配特定数量的内存,以充当该线程的堆栈。堆栈管理堆栈帧,也是声明线程的任何局部变量的地方。为线程分配的内存量列在“ 线程成本”中。

如果要更改给定线程的堆栈大小,则必须在创建线程之前执行此操作。尽管设置堆栈大小NSThread仅在iOS和OS X v10.5及更高版本中可用,但所有线程技术都提供了一些设置堆栈大小的方法。表2-2列出了每种技术的不同选项。

表2-2 设置线程的堆栈大小

技术 选项
Cocoa 在iOS和OS X v10.5及更高版本中,分配并初始化NSThread对象(不要使用该detachNewThreadSelector:toTarget:withObject:方法)。在调用start线程对象的方法之前,请使用该setStackSize:方法指定新的堆栈大小。
POSIX 创建一个新pthread_attr_t结构并使用该pthread_attr_setstacksize函数更改默认堆栈大小。pthread_create创建线程时将属性传递给函数。
多处理服务 MPCreateTask在创建线程时,将适当的堆栈大小值传递给函数。

配置线程局部存储

每个线程都维护一个键值对字典,可以从线程中的任何位置访问。您可以使用此字典存储要在整个线程执行期间保留的信息。例如,您可以使用它来存储您希望通过线程运行循环的多次迭代持久化的状态信息。

Cocoa和POSIX以不同的方式存储线程字典,因此您无法混合和匹配对这两种技术的调用。但是,只要您在线程代码中坚持使用一种技术,最终结果应该是相似的。在Cocoa中,您使用NSThread对象的threadDictionary方法来检索NSMutableDictionary对象,您可以向其添加线程所需的任何键。在POSIX中,您使用pthread_setspecific和pthread_getspecific函数来设置和获取线程的键和值。

设置线程的分离状态

大多数高级线程技术默认创建分离线程。在大多数情况下,首选分离线程是因为它们允许系统在完成线程后立即释放线程的数据结构。分离的线程也不需要与您的程序进行明确的交互。意味着从线程中检索结果的方法由您自行决定。相比之下,系统不会回收可连接线程的资源,直到另一个线程显式加入该线程,这个进程可能会阻塞执行连接的线程。

您可以将可连接线程视为类似于子线程。尽管它们仍然作为独立线程运行,但是在系统可以回收其资源之前,必须由另一个线程连接可连接线程。可连接线程还提供了一种将数据从现有线程传递到另一个线程的显式方法。在它退出之前,可连接的线程可以将数据指针或其他返回值传递给pthread_exit函数。然后另一个线程可以通过调用该pthread_join函数来声明该数据。

重要说明: 在应用程序退出时,分离的线程可以立即终止,但可连接的线程不能。必须先连接每个可连接线程,然后才允许进程退出。因此,在线程正在执行不应被中断的关键工作(例如将数据保存到磁盘)的情况下,可连接线程可能是优选的。

如果您确实想要创建可连接的线程,唯一的方法是使用POSIX线程。默认情况下,POSIX将线程创建为可连接。要将线程标记为已分离或可连接,请pthread_attr_setdetachstate在创建线程之前使用该函数修改线程属性。线程开始后,您可以通过调用该pthread_detach函数将可连接线程更改为分离线程。有关这些POSIX线程函数的更多信息,请参见pthread手册页。有关如何加入线程的信息,请参见pthread_join手册页。

设置线程优先级

您创建的任何新线程都具有与之关联的默认优先级。内核的调度算法在确定要运行哪些线程时考虑线程优先级,优先级较高的线程比具有较低优先级的线程更可能运行。较高优先级并不能保证线程的特定执行时间,只是与较低优先级的线程相比,调度程序更有可能选择它。

重要提示: 将线程的优先级保留为默认值通常是个好主意。增加某些线程的优先级也会增加低优先级线程之间饥饿的可能性。如果您的应用程序包含必须相互交互的高优先级和低优先级线程,则较低优先级线程的饥饿可能会阻塞其他线程并产生性能瓶颈。

如果你想修改线程优先级,Cocoa和POSIX都提供了一种方法。对于Cocoa线程,您可以使用NSThread的setThreadPriority:class方法设置当前运行的线程的优先级。对于POSIX线程,您可以使用该pthread_setschedparam函数。有关更多信息,请参见NSThread类参考或pthread_setschedparam手册页。

编写线程入口例程

在大多数情况下,线程的入口点例程的结构在OS X中与在其他平台上的结构相同。初始化数据结构,执行某些操作或者可选地设置运行循环,并在线程代码完成时进行清理。根据您的设计,在编写入口例程时可能需要执行一些额外的步骤。

创建自动释放池

在Objective-C框架中链接的应用程序通常必须在每个线程中创建至少一个自动释放池。如果应用程序使用托管模型 - 应用程序处理保留和释放对象的位置 - 自动释放池将捕获从该线程自动释放的所有对象。

如果应用程序使用垃圾收集而不是托管内存模型,则不一定要创建自动释放池。在垃圾收集的应用程序中存在自动释放池是无害的,并且在很大程度上被忽略。允许代码模块必须同时支持垃圾收集和托管内存模型的情况。在这种情况下,必须存在自动释放池以支持托管内存模型代码,如果应用程序在启用垃圾收集的情况下运行,则会被忽略。

如果您的应用程序使用托管内存模型,那么创建自动释放池应该是您在线程入口例程中首先要做的事情。同样,销毁这个自动释放池应该是你在线程中做的最后一件事。此池确保捕获自动释放的对象,但在线程本身退出之前不会释放它们。清单2-2显示了使用自动释放池的基本线程入口例程的结构。

清单2-2 定义线程入口点例程

- (void)myThreadMainRoutine
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // 顶级池
 
    //线程在这里工作
 
    [pool release];  // 释放池中的对象
}

由于顶级自动释放池在线程退出之前不释放其对象,因此长期存在的线程应创建其他自动释放池以更频繁地释放对象。例如,使用运行循环的线程可能每次通过该循环创建并释放自动释放池。更频繁地释放对象可以防止应用程序的内存占用过大,从而导致性能问题。与任何与性能相关的行为一样,您应该测量代码的实际性能并适当调整自动释放池的使用。

有关内存管理和自动释放池的详细信息,请参阅“ 高级内存管理编程指南”。

设置异常处理

如果您的应用程序捕获并处理异常,则应准备好线程代码以捕获可能发生的任何异常。虽然最好在异常产生时处理异常,但是如果未能在线程中捕获抛出异常会导致应用程序退出。在线程入口例程中安装最终的try / catch允许您捕获任何未知异常并提供适当的响应。

在Xcode中构建项目时,可以使用C ++或Objective-C异常处理样式。有关设置如何在Objective-C中引发和捕获异常的信息,请参阅异常编程主题。

设置运行循环

编写要在单独的线程上运行的代码时,您有两个选择。第一种选择是将线程的代码编写为一个很长的任务,在很少或没有中断的情况下执行,并在完成时让线程退出。第二个选项是将您的线程放入循环中,并在它们到达时动态处理请求。第一个选项不需要为您的代码进行特殊设置; 做你想做的工作就行了。但是,第二个选项涉及设置线程的运行循环。

OS X和iOS提供了在每个线程中实现运行循环的内置支持。应用程序框架自动启动应用程序主线程的运行循环。如果创建任何辅助线程,则必须配置运行循环并手动启动它。

有关使用和配置运行循环的信息,请参阅运行循环。

终止线程

退出线程的推荐方法是让它正常退出其入口点例程。尽管Cocoa,POSIX和Multiprocessing Services提供了直接杀死线程的例程,但强烈建议不要使用此类例程。杀死一个线程可以防止该线程自行清理。线程分配的内存可能会被泄露,并且线程当前正在使用的任何其他资源可能无法正确清理,从而在以后产生潜在问题。

如果您预计需要在操作过程中终止线程,则应该从一开始就设计线程以响应取消或退出消息。对于长时间运行的操作,这可能意味着定期停止工作并检查是否有这样的消息到达。如果确实有消息要求线程退出,则线程将有机会执行任何所需的清理并正常退出; 否则,它可以简单地返回工作并处理下一个数据块。

响应取消消息的一种方法是使用运行循环输入源来接收此类消息。清单2-3显示了此代码在线程主入口例程中的外观结构。(该示例仅显示主循环部分,不包括设置自动释放池或配置要执行的实际工作的步骤。)该示例在运行循环上安装自定义输入源可能被另一个线程通知到; 有关设置输入源的信息,请参阅配置运行循环源。执行总工作量的一部分后,线程会短暂运行运行循环,以查看是否有消息到达输入源。如果没有,则运行循环立即退出,循环继续下一个工作块。因为处理程序不能直接访问exitNow局部变量,所以退出条件通过线程字典中的键值对进行传递。

清单2-3 长时间工作检查退出条件

- (void)threadMainRoutine {
    BOOL moreWorkToDo = YES;
    BOOL exitNow = NO;
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    
    //将exitNow BOOL添加到线程字典中
    NSMutableDictionary *threadDict = [[NSThread currentThread] threadDictionary];
    [threadDict setValue:[NSNumber numberWithBool:exitNow] forKey:@"ThreadShouldExitNow"];
    
    //安装输入源
    [self myInstallCustomInputSource];
    
    while (moreWorkToDo && (!exitNow)) {
        //在这里做一大部分工作
        //完成后更改moreWorkToDo布尔值。
        
        //如果输入源没有等待触发,则runLoop立即超时
        [runLoop runUntilDate:[NSDate date]];
        
        //检查输入源处理程序是否更改了exitNow值
        exitNow = [[threadDict valueForKey:@"ThreadShouldExitNow"] boolValue];
    }
}

你可能感兴趣的:(线程编程指南翻译第二篇(线程管理))