线程管理(Thread Management)

来源:互联网 发布:手机单怎么预防淘宝客 编辑:程序博客网 时间:2024/05/17 01:12

OS XiOS每个进程(应用)是由一个或多个线程组成,每个线程代表通过应用代码执行的一个路径。每个应用从一个线程开始,用来运行应用的main函数。应用可以产生额外的线程,每个线程执行特定功能的代码。

当应用生成一个新线程时,该线程成为应用进程空间内的独立实体。每个线程都有自己的执行堆栈并且由内核独立安排运行时。一个线程可以与其他线程和其他进程通信,执行 I/O 操作,和其他任何你需要做的。因为它们在相同进程空间中,然而,在一个应用中的所有线程共享相同的虚拟内存并且和进程一样有相同访问权限。

本章提供了OS XiOS中可使用的线程技术的概述,以及如何在你应用中使用这些技术的例子。

注意:查看Mac OS线程体系结构,以及额外线程背景信息,参阅技术说明TN2028,线程结构(Threading Architectures)

线程成本

在内存使用和性能方面,线程对你的程序(和系统)产生一个实际成本。每个线程都需要在内核内存空间和你程序内存空间中分配内存。需要核心结构来管理你的线程和协调调度,该结构存储在内核连续的内存中。你的线程的堆栈空间和每个线程的数据存储在应用的内存空间中。大多数结构在你第一次创建线程时就被创建和初始化,因为需要与内核交互,这个过程相当昂贵。

表2-1 量化了在你应用中创建一个用户级别线程的粗略成本。其中一些成本是可配置的,例如为次要线程分配的堆栈空间。创建一个线程的时间成本是一个粗略的近似值,应该只用于彼此相对的比较。线程创建的时间很大程度上取决于处理器负载,计算机速度以及系统和程序可用内存的数量。

表2-1 线程的创建成本

项目

大概成本

说明

内核数据结构

大约1KB

这个内存用于存储线程数据结构和属性,其中大部分分配到连接内存中,因此不能分页到磁盘上。

堆栈空间

512 KB (次要线程)

8 MB(OS X主线程)

1 MB(iOS主线程)

次要线程允许的最小堆栈大小时16KB,并且堆栈大小必须是4KB的倍数。在线程创建时间,该内存空间在你的进程空间中的预留的,但实际页面和内存并不创建直到需要它们。

创建时间

大约90微秒

这个值反应了初始调用创建线程和线程入口点程序开始执行之间的时间。该数值是分析创建线程时生成的均值和中间值决定的,线程创建时基于InterliMac,该iMac2 GHz的酷睿双核处理器,1GBRAM运行OS X v10.5。

注意:由于他们的底层支持内核,操作对象创建线程通常会更快。而不是每次从头创建线程,他们使用的线程池已经存在与内核中,可以节省分配时间。关于操作对象的更多信息,参见并发编程指南( Concurrency Programming Guide)。

当编写线程代码时需要考虑的另一个成本是生产成本。设计一个线程应用有时候需要从根本上改变你组织你应用数据结构的方式。这些变更可能需要避免使用同步,因为它们本身会强加巨大的性能损失到设计拙劣的应用上。设计这些数据结构,在线程代码中调试问题,会增加开发一个线程应用的时间。避免这些成本会在运行时创建更大的问题,然而,如果你的线程花费太多时间等待锁或什么都不做。

创建一个线程

创建一个底层线程相对简单。在所有情况下,你必须有一个函数或方法作为线程主要入口点,你必须使用一个可用的线程程序来启动你的线程。下面章节展示了基本创建过程中更常用的线程技术。线程创建使用的技术继承一套默认的属性,该属性取决于你使用的技术。关于如何配置你的线程的更多信息,参阅配置线程属性(Configuring Thread Attributes)。

使用NSThread

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

使用 detachNewThreadSelector:toTarget:withObject:类方法生成新线程。

创建一个新NSThread 对象并调用start 方法。(只支持iOSOS X v10.5后续版本)。

这两种技术在应用中创建一个分离的线程。分离的线程意味着当线程退出时线程资源由系统自动回收。这也意味着你的代码不用明确的加入线程。

因为所有OS X版本均支持detachNewThreadSelector:toTarget:withObject: 方法,在现有使用线程的Cocoa应用中经常会出现。为了分离出一个新线程,你只需提供你想用来作为线程入口点的方法的名称(指定为选择器),定义方法的对象及任何你想要在启动时传递给线程的数据。下面的例子展示了这个方法的基本调用,该方法使用当前对象自定义方法产生一个线程。

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


在OS X v10.5及后续版本中初始化NSThread 对象的简单方法是使用initWithTarget:selector:object:方法。该方法采用与detachNewThreadSelector:toTarget:withObject: 方法完全相同的信息并使用它来初始化一个新的NSThread 实例。然而,它不启动线程。为了启动线程,你可以显式的调用线程对象的start 方法,如下面的例子所示:在OS X v10.5之前,你主要使用NSThread 类来创建线程。尽管你可以得到一个NSThread 对象并访问一些线程属性,在线程运行后,你可以从线程本身这样做。在OS X v10.5,支持创建NSThread 对象而没有立即产生相应的新线程。(在iOS也同样支持。)该支持使它可以获取并设置启动线程前各个线程的属性。它还能使线程对象指向正在运行的线程。

NSThread* myThread = [[NSThread alloc] initWithTarget:self                                        selector:@selector(myThreadMainMethod:)                                        object:nil];[myThread start];  // Actually create the thread


注意:另一个可替代方案是使用initWithTarget:selector:object:方法子类化NSThread 并重载其main 方法。你会使用方法的重载版本来实现你线程的主要入口点。更多信息参阅NSThread 类引用( NSThread Class Reference)的子类化说明。

如果你有一个对象中的线程正在运行,发送消息给线程的一种方式是使用你应用中几乎所有对象的performSelector:onThread:withObject:waitUntilDone:方法。支持在线程上执行选择器(除了主线程)在OS X v10.5中有介绍,是一种线程之间通信的方便方法。(在iOS中也支持。)你使用这种技术发送的消息被其他线程作为正常运行循环处理的部分直接执行。(当然,这表明目标线程必须运行在其运行循环上,参见运行循环。)当你使用这种方式通信,你可能仍然需要某种形式的同步,但是它比在线程间建立通信端口要简单。

注意:尽管适合线程间临时通信,你不应该在关键时间或频繁通信的线程中使用performSelector:onThread:withObject:waitUntilDone: 方法。

其他线程通信选项列表,参见设置线程的分离状态(Setting the Detached State of a Thread)。

使用POSIX线程

OS XiOS提供使用POSIX线程API创建线程基于C的支持。该技术可以在任何类型的应用中使用(包括CocoaCocoa Touch应用),并且当你在为多平台编写软件时会更加方便。用来创建线程的POSIX程序称为pthread_create

清单2-1展示了两个使用POSIX调用创建一个线程的自定义函数。LaunchThread 函数创建一个新线程,该线程的住程序在PosixThreadMainRoutine 函数中实现。因为POSIX默认创建可连接线程,在这个例子中改变线程的属性创建一个分离线程。将线程设为分离的,当线程退出时系统可以回收该线程的资源。

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

#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.    }}


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

为了将通信信息从你新创建的线程传递到你应用的主线程,你需要在目标线程间建立一个通信路径。对于基于C的应用,有几种线程间通信的方法,包括使用端口、条件或共享内存。对于长期线程,你应该设置一些线程间通信机制,使你应用主线程可以检查线程的状态,或者当应用退出时可以干净的关闭它。

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

使用NSObject 生成一个线程

在iOSOS X v10.5以及后续版本,所有对象可以生成一个新线程并使用该线程来执行他们的一个方法。performSelectorInBackground:withObject:方法创建一个新分离线程并制定方法作为新线程的入口点。例如,如果你有一些对象(例如变量myObj)并且对象有一个名为doSomething 方法,该方法你希望在后台线程中运行,你可以使用下面的代码来实现)。

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


调用该方法的效果是一样的,如果你调用NSThread 的detachNewThreadSelector:toTarget:withObject:方法并以当前对象、选择器和参数对象作为参数。使用默认配置立即生成新线程并开始运行。在选择器中,你必须配置线程。例如,你需要建立一个自动释放池(如果你没有使用垃圾回收),如果你打算使用它,需配置线程的运行循环。关于如何配置新线程的信息,参见配置线程属性(Configuring Thread Attributes)。

在Cocoa应用中使用POSIX线程

尽管NSThread 类是Cocoa应用中用来创建线程的主要接口,如果这样做更方便,你可以自由使用POSIX线程。例如,如果你已经有代码,并且在使用它们而不希望重写它,你可以使用POSIX线程。如果你计划在Cocoa应用中使用POSIX线程,你仍然意识到Cocoa和线程之间的交互并遵守下面章节的指导方针。

保护Cocoa框架

对于多线程应用,Cocoa框架使用锁和其他形式的内部同步来确保它们正确运转。为了防止这些锁在单线程中降低性能,然而,Cocoa不创建它们知道应用使用NSThread 类生成一个新线程。如果你只使用POSIX线程程序生成线程,Cocoa不接受通知,它需要知道你的应用是多线程的。当这种情况发生时,涉及Cocoa框架的操作可能破坏或使你的应用崩溃。

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

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

混合使用POSIXCocoa

在相同的应用中混合使用POSIXCocoa锁比较安全。Cocoa锁和条件对象本质上只是POSIX互斥锁和条件的包装。对于一个给定的锁,然而,你总是使用相同的接口来创建和操作锁。换句话说,你不能使用一个CocoaNSLock 对象来操作一个使用 pthread_mutex_init函数创建的互斥锁,反之亦然。

配置线程属性

在你创建一个线程后,有时在创建线程前,你可能需要配置不同部分的线程环境。以下章节描述了当你使用它们时可以做的一些更改。

配置一个线程的堆栈大小

为每个你创建的新线程,系统在你的进程空间中分配一个特定数量的内存作为该线程的堆栈。该堆栈管理堆栈帧并且也是任何线程局部变量声明的地方。分配给线程的内存数量在线程成本(Thread Costs)中列出。

如果你想改变给定线程的堆栈大小,你必须在创建线程前这样做。所有线程技术提供一些方法设置堆栈大小,尽管只能在iOSOS X v10.5以及后续版本中使用NSThread 来设置堆栈大小。表2-2列出每种技术的不同选项。

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

技术

选项

Cocoa

在iOSOS X v10.5以及后续版本中,分配和初始化一个NSThread 对象(不适用 detachNewThreadSelector:toTarget:withObject:方法)。在调用线程对象start 方法之前,使用setStackSize:方法指定新堆栈大小。

POSIX

创建一个新pthread_attr_t结构并使用pthread_attr_setstacksize函数来改变默认堆栈大小。当创建线程时,传递该属性到pthread_create函数。

多处理服务

当你创建线程时,传递适当堆栈大小值到MPCreateTask 函数

配置线程本地存储

每个线程维护一个键值对字典,该字典可以从任何地方访问线程。你可以使用该字典来存储贯穿线程执行过程的信息。例如,你可以使用它来存储线程运行循环的多次迭代的状态信息。

CocoaPOSIX以不同的方式存储线程字典,所以你不能混合调用两种技术。只要你坚持在线程代码内部使用一种技术,最终的结果是相似的。在Cocoa中,你使用NSThread 对象的threadDictionary 方法来检索一个NSMutableDictionary 对象,你可以往该对象添加任何你线程需要的键。在POSIX中,你使用pthread_setspecific函数和pthread_getspecific 函数来设置或获取你线程的键值对。

设置线程的分离状态

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

你可以认为可连接线程类似于子线程。尽管他们仍然是独立的线程,在资源被系统回收前,可连接线程必须连接到另一个线程。可连接线程也提供一个明确的方式来从一个存在的线程传递数据到另一个线程。在退出之前,一个可连接线程可以通过数据指针或其他返回值到pthread_exit函数。另一个线程可以调用 pthread_join 函数来声明这些数据。

重要:在应用退出时,分离线程可以立即终止但可连接线程不能。在进程允许退出前,每个可连接线程必须连接。因此,可连接线程更适合用于如下情况:线程在完成不能被打断的重要工作,例如保存数据到磁盘。

如果你想创建可连接线程,唯一的方法是使用POSIX线程。POSIX默认创建可连接线程。为了标记一个线程是分离的或可连接的,在创建线程前使用thread_attr_setdetachstate 函数修改线程属性。在线程开始后,你可以调用 pthread_detach函数将一个可连接线程改为分离线程。关于POSIX线程函数的更多信息,参见pthread 手册页。关于如何加入到一个线程的更多信息,参见pthread_join手册页。

设置线程优先级

任何你创建的线程都有一个与之关联的默认优先级。当决定哪些线程运行时,核调度算法考虑线程优先级,高优先级线程可能比低优先级线程更早运行。高优先级不能保证一个特定数量的线程执行时间,与低优先级线程相比,它只是更可能被调度程序选择。

重要:让你的线程优先级为默认值通常是一个好办法。增加某些线程的优先级也会增加某些低优先级线程饿死的可能性。如果你的应用包含高优先级和低优先级的线程且两者相互影响,低优先级线程的饿死可能阻塞其他线程并创建性能瓶颈。

如果你想要改变线程优先级,CocoaPOSIX提供一种方式来实现。对于Cocoa线程,你可以使用NSThread 的setThreadPriority: 类方法来设置当前运行线程的优先级。对于POSIX 线程,你可以使用pthread_setschedparam函数。更多信息,参阅NSThread 类引用(NSThread Class Reference)或pthread_setschedparam手册页。

编写你的线程进入程序

在大多数情况下,线程入口点程序的结构在OS X和其他平台是相同的。你初始化数据结构,做些工作或选择建立一个运行循环,并在你线程代码完成时清理。根据你的设计,当你编写你的入口程序时,可能需要采取一些额外的步骤。

创建一个自动释放池

连接Objective-C框架的应用通常在每个线程中必须至少创建一个自动释放池。如果一个应用使用管理模式,应用处理对象的保留和释放,自动释放池捕获任何从线程中自动释放的任何对象。

如果一个应用使用来及回收而不是托管内存模型,那么创建一个自动释放池并不是必须的。在垃圾回收应用中自动释放池的出现并不是有害的,在大多数情况下只是被忽略。它允许代码模块同时支持垃圾回收和托管内存模型的情况。在这种情况下,自动释放池必须支持托管内存模型代码,并且当应用启用垃圾回收时只是被忽略。

如果你的应用使用托管内存模型,创建一个自动释放池是你在你线程入口程序中需要做的第一件事情。同样,销毁该线程池是你线程中需要做的最后一件事。该池确保自动释放的对象被捕获,尽管它不释放直到线程本身退出。清单2-2展示了使用自动释放池的基本线程入口程序的结构。

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

- (void)myThreadMainRoutine{    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // Top-level pool     // Do thread work here.     [pool release];  // Release the objects in the pool.}


因为顶级自动释放池不释放对象直到线程退出,长期线程应该创建额外的自动释放池来更频繁的释放对象。例如,一个线程使用运行循环可能在每次运行循环时创建和释放一个自动释放池。更频繁的释放对象可以防止应用的内存占用增长太大,否则会导致性能问题。与任何性能相关的行为,你应该测量代码的实际性能并适当的调整自动释放池的使用。

关于内存管理和自动释放池的更多信息,参见高级内存管理编程指南(dvanced Memory Management Programming Guide)。

设置一个异常处理程序

如果你的应用捕获和处理异常,线程代码应该准备不和任何可能出现的异常。尽管最好在异常出现的地方处理异常,在线程中未能捕获一个抛出的异常会引起你应用退出。在线程入口程序安装一个最终的try/catch使你可以捕获任何未知的异常并提供一个合适的回应。

当在Xcode中建立项目时,你可以使用C++ Objective-C异常处理风格。关于如何在Objective-C中设置如何提高和捕获异常的信息,参见异常编程主题(Exception Programming Topics)。

建立一个运行循环

编写代码时你希望在一个单独的线程上运行,你有两个选择。第一个选择是写一个线程代码作为长期执行的任务,很少或没有中断,当任务完成时线程退出。第二个选择是将你的线程放到一个循环中,当它到来时动态的处理请求。第一个选择不需要为你的代码做特殊设置,你只要开始做你想做的事。第二个选择,然而,包含设置线程的运行循环。

OS XiOS提供内置支持在每个线程上实现运行循环。应用框架自动启动应用主线程的运行循环。如果你创建任何次要线程,你必须配置运行循环和手动启动它。

关于使用和配置运行循环的信息,参见运行循环(Run Loops)。

终止一个线程

退出一个线程推荐的方法是让该线程正常的从入口点程序退出。尽管CocoaPOSIX和多处理服务提供程序直接杀死线程,不鼓励使用这些程序。杀死一个线程会阻止线程清理。该线程分配的内存可能会泄露并且线程目前使用的其他资源可能清理不感激,会在之后创造潜在的问题。

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

响应取消消息的一种方法是使用一个运行循环输入源来接收这样的消息。清单2-3展示了这段代码如何查找线程主要入口程序的结构。(这个例子展示了主要循环部分,不包括建立自动释放池或配置实际工作的步骤。)例子在运行循环上安装一个自定义输入源,可从另一线程传递消息。关于设置输入源的信息,参见配置运行循环来源( Configuring Run Loop Sources)。在执行全部工作的一部分后,线程暂时的运行运行循环来查看消息是否到达输入源。如果没有,运行循环立即退出继续下一块工作。因为处理程序没有直接访问exitNow 局部变量,退出条件通过线程字典中的键值对通信。

清单2-3 在一个长期工作中检查退出条件

- (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];    }}


官方原文地址:

https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Multithreading/CreatingThreads/CreatingThreads.html#//apple_ref/doc/uid/10000057i-CH15-SW2 



0 0
原创粉丝点击