美文网首页
线程管理(iOS)

线程管理(iOS)

作者: 夏天的风_song | 来源:发表于2017-04-01 14:05 被阅读0次

    OS X 或iOS中的每个进程(应用程序)由一个或者多个线程组成,每个线程应用程序的代码表示单个执行路径。
    每个应用程序都从单个线程开始,它运行应用程序的main功能。 应用程序可以生成额外的线程,每个线程都执行特定功能的代码。

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

    本章概述了OS X和iOS中可用的线程技术,以及如何在应用程序中使用这些技术的示例。

    线程成本

    线程在内存使用和性能方面对您的程序(和系统)有实际的成本。 每个线程需要在内核内存空间和程序的内存空间中分配内存。 管理线程和协调其调度所需的核心结构使用有线内存存储在内核中。 您的线程的堆栈空间和每个线程数据存储在程序的内存空间中。 当您首次创建线程时,大多数这些结构都将被创建和初始化 - 由于与内核所需的交互,这个进程可能相对昂贵。

    表1量化了在应用程序中创建新的用户级线程所需的大概成本。 这些成本中的一些是可配置的,例如为次要线程分配的堆栈空间量。 创建线程的时间成本是一个粗略的近似值,只能用于相互比较。 线程创建时间可能因处理器负载,计算机速度以及可用系统和程序存储器的数量而异。

    表1

    项目 大概花费 描述
    内核数据结构 大约1 KB 该内存用于存储线程数据结构和属性,其中大部分被分配为有线内存,因此不能被分页到磁盘。
    堆栈空间 512 KB(二级线程)8 MB(OS X主线程)1 MB(iOS主线程) 辅助线程的最小允许堆栈大小为16 KB,堆栈大小必须为4 KB的倍数。 这个内存的空间在线程创建时被放在你的进程空间中,但是在需要的时候才会创建与该内存关联的实际页面。
    创建时间 大约90微秒 此值反映了初始调用创建线程与线程入口点例程开始执行的时间。 这些数字是通过分析在基于Intel的iMac上使用2 GHz Core Duo处理器和1 GB RAM运行OS X v10.5的线程创建过程中生成的平均值和中值。

    注意:由于它们的底层内核支持,操作对象通常可以更快地创建线程。 他们每次都从头开始创建线程,而是使用已经驻留在内核中的线程池来节省分配时间。

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

    创建线程

    创建低级线程比较简单。 在所有情况下,您必须具有一个函数或方法作为线程的主要入口点,并且您必须使用一个可用的线程例程来启动您的线程。 以下部分显示了更常用的线程技术的基本创建过程。 使用这些技术创建的线程将继承一组默认属性,由您使用的技术确定。

    使用NSThread

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

    • 使用 datachNewThreadSelector:toTarget:withoutObject:方法来生成新线程。

    • 创建一个新的Thread 对象并调用其start方法。
      这两种技术在您的应用程序中创建一个分离的线程。 分离的线程意味着当线程退出时,线程的资源将被系统自动回收。 这也意味着您的代码不必在以后明确加入该线程。

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

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

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

     NSThread * myThread = [[NSThread alloc] initWithTarget:self 
      selector:@selector(myThreadMainMethod :) 
     对象:nil]; 
      [myThread start]; 
    

    注意:使用initWithTarget:selector:object:方法的initWithTarget:selector:object:一种方法是将NSThread子类化并覆盖其main方法。 您将使用此方法的覆盖版本来实现线程的主入口点。

    如果您的线程当前正在运行的NSThread
    对象,您可以向该线程发送消息的一种方法是使用应用程序中几乎任何对象的`[performSelector:onThread:withObject:waitUntilDone:]
    方法。 在线程上执行的选择器支持(主线程除外)在OS X v10.5中引入,是一种方便的线程通信方式。 (此支持也可在iOS中使用。)使用此技术发送的消息由另一个线程直接执行,作为其正常运行循环处理的一部分。 (当然,这意味着目标线程必须在其运行循环中运行)当您以这种方式进行通信时,您可能仍需要某种形式的同步,但它比在线程。

    注意:尽管线程之间的偶尔通信很有用,但不应该使用performSelector:onThread:withObject:waitUntilDone:
    方法来对线程之间进行时间关键或频繁的通信。

    使用NSObject生成线程

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

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

    调用此方法的效果与调用当前对象,选择器和参数对象作为参数的[NSThread][detachNewThreadSelector:toTarget:withObject:]
    方法一样。 使用默认配置立即生成新线程,并开始运行。 在选择器中,您必须像线程一样配置线程。 例如,您将需要设置一个自动释放池(如果您没有使用垃圾回收),并且如果您计划使用它,则配置该线程的运行循环。

    配置线程属性

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

    配置线程的堆栈大小

    对于您创建的每个新线程,系统将在进程空间中分配一定量的内存,以充当该线程的堆栈。 堆栈管理堆栈帧,也是线程的任何局部变量被声明的地方。 为线程分配的内存量列在线程成本中。
    如果要更改给定线程的堆栈大小,则必须先创建线程。 所有线程技术都提供了一些设置堆栈大小的方法,尽管使用NSThread
    设置堆栈大小只能在iOS和OS X v10.5及更高版本中使用。 表2列出了各种技术的不同选项。

    表2.png
    配置线程本地存储

    每个线程都维护一个键值对的字典,可以从线程中的任何位置访问。 您可以使用此字典来存储在执行线程期间要保留的信息。 例如,您可以使用它来存储要通过线程的运行循环的多次迭代来保持的状态信息。
    Cocoa和POSIX以不同的方式存储线程字典,所以您不能混合和匹配两种技术的调用。 只要您在线程代码中使用一种技术,最终结果应该是相似的。 在Cocoa中,您可以使用NSThread对象的threadDictionary方法来检索NSMutableDictionary对象,您可以向其中添加线程所需的任何键。 在POSIX中,您可以使用pthread_setspecificpthread_getspecific函数来设置和获取线程的键值。

    设置线程的分离状态

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

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

    重要提示:在应用程序退出时,分离的线程可以立即终止,但可连接的线程不能。 在允许进程退出之前,必须连接每个可连接的线程。 因此,线程执行不应中断的关键工作(例如将数据保存到磁盘)时,可以使用可加入的线程。

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

    设置线程的优先级

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

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

    如果你想修改线程优先级,Cocoa和POSIX都提供了一种方法。 对于Cocoa线程,可以使用NSThread的` [setThreadPriority:] class方法设置当前正在运行的线程的优先级。 对于POSIX线程,可以使用pthread_setschedparam
    函数。

    写你的线程进入进程

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

    创建自动释放池

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

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

    如果您的应用程序使用托管内存模型,则创建自动释放池应该是您在线程进入例程中执行的第一件事。 同样的,摧毁这个自动释放池应该是您在线程中执行的最后一件事。 该池确保自动释放的对象被捕获,尽管它在线程本身退出之前不会释放它们。

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

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

    设置异常处理程序

    如果您的应用程序捕获并处理异常,则您的线程代码应准备好捕获可能发生的任何异常。 尽管最好在可能发生的情况下处理异常,但如果线程中抛出异常,则无法使您的应用程序退出。 在你的线程入口例程中安装最终的try / catch可以让你捕获任何未知的异常并提供适当的响应。

    在Xcode中构建项目时,可以使用C ++或Objective-C异常处理风格。

    设置运行循环

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

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

    终止线程

    退出线程的推荐方法是让它正常退出其入口点例程。 虽然Cocoa,POSIX和多处理服务提供了直接杀死线程的例程,但是强烈建议不要使用此类例程。 杀死线程会阻止线程自身清理。 线程分配的内存可能会泄漏,线程当前正在使用的任何其他资源可能无法正确清除,从而在以后创建潜在问题。
    如果您期望在操作中终止线程,您应该从一开始就设计线程以响应取消或退出消息。 对于长时间运行的操作,这可能意味着定期停止工作,并检查是否有消息到达。 如果一条消息确实要求线程退出,线程将有机会执行任何所需的清理并正常退出; 否则,它可以简单地返回工作并处理下一个数据块。
    响应取消消息的一种方法是使用运行循环输入源来接收这样的消息。 代码2-3显示了该代码在线程的主入口例程中的外观结构。 (该示例仅显示主循环部分,不包括设置自动释放池或配置实际工作的步骤。)示例在运行循环上安装自定义输入源,可能会从另一个你的线程 有关设置输入源的信息,在执行总工作量的一部分后,线程会短暂运行运行循环,以查看消息是否到达输入源。 如果没有,运行循环将立即退出,循环继续下一个工作块。 因为处理程序不能直接访问exitNow
    局部变量,所以退出条件通过线程字典中的键值对来传达。

     - (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 Boolean的值。 
      //运行运行循环,如果输入源不等待触发则立即超时。 
      [runLoop runUntilDate:[NSDate date]]; 
      //检查输入源处理程序是否更改了exitNow值。 
      exitNow = [[threadDict valueForKey:@“ThreadShouldExitNow”] boolValue]; 
      } 
      } 
    

    相关文章

      网友评论

          本文标题:线程管理(iOS)

          本文链接:https://www.haomeiwen.com/subject/ovriottx.html