前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >iOS底层 之 多线程原理(上)

iOS底层 之 多线程原理(上)

作者头像
CC老师
发布2021-08-25 11:53:29
4860
发布2021-08-25 11:53:29
举报

线程是什么

线程是可以在单个应用程序中同时执行多个代码路径的几种技术之一。尽管操作对象和 Grand Central Dispatch (GCD) 等新技术为实现并发提供了更现代、更高效的基础设施,但 OS X 和 iOS 也提供了用于创建和管理线程的接口。

关于线程编程

多年来,计算机的最大性能很大程度上受到作为计算机核心的单个微处理器的速度的限制。然而,随着单个处理器的速度开始达到其实际极限,芯片制造商转向多核设计,让计算机有机会同时执行多项任务。尽管 OS X 会尽可能利用这些内核来执行与系统相关的任务,但您自己的应用程序也可以通过线程来利用它们。

什么是线程?

线程是在应用程序内部实现多条执行路径的相对轻量级的方式。在系统级别,程序并行运行,系统根据每个程序的需要和其他程序的需要分配执行时间给每个程序。然而,在每个程序内部,存在一个或多个执行线程,可用于同时或以几乎同时的方式执行不同的任务。系统本身实际上管理这些执行线程,安排它们在可用内核上运行,并根据需要抢先中断它们以允许其他线程运行。

从技术角度来看,线程是管理代码执行所需的内核级和应用程序级数据结构的组合。内核级结构协调将事件分派到线程以及线程在可用内核之一上的抢占式调度。应用程序级结构包括用于存储函数调用的调用堆栈和应用程序管理和操作线程的属性和状态所需的结构。

在非并发应用程序中,只有一个执行线程。该线程以您的应用程序的main例程开始和结束,并一一分支到不同的方法或函数以实现应用程序的整体行为。相比之下,支持并发的应用程序从一个线程开始,并根据需要添加更多线程以创建额外的执行路径。每个新路径都有自己的自定义启动例程,独立于应用程序main例程中的代码运行。在应用程序中拥有多个线程提供了 两个非常重要的潜在优势:多线程可以提高应用程序的感知响应能力。多线程可以提高应用程序在多核系统上的实时性能。

如果您的应用程序只有一个线程,那么该线程必须做所有事情。它必须响应事件,更新应用程序的窗口,并执行实现应用程序行为所需的所有计算。只有一个线程的问题是它一次只能做一件事。那么当您的一项计算需要很长时间才能完成时会发生什么?当您的代码忙于计算它需要的值时,您的应用程序停止响应用户事件并更新其窗口。如果这种行为持续的时间足够长,用户可能会认为您的应用程序已挂起并试图强行退出它。但是,如果您将自定义计算移到单独的线程上,您的应用程序的主线程将可以更及时地响应用户交互。

随着多核计算机的普及,线程提供了一种提高某些类型应用程序性能的方法。执行不同任务的线程可以在不同的处理器内核上同时执行,从而使应用程序可以在给定的时间内增加它所做的工作量。

当然,线程并不是解决应用程序性能问题的灵丹妙药。伴随线程提供的好处而来的是潜在的问题。在应用程序中具有多个执行路径可能会显着增加代码的复杂性。每个线程必须与其他线程协调其操作,以防止它破坏应用程序的状态信息。由于单个应用程序中的线程共享相同的内存空间,因此它们可以访问所有相同的数据结构。如果两个线程试图同时操作相同的数据结构,一个线程可能会以破坏结果数据结构的方式覆盖另一个线程的更改。即使采取了适当的保护措施,您仍然必须注意编译器优化,这些优化将细微的(而不是那么细微的)错误引入您的代码中。

线程:用于指代代码的单独执行路径。进程:用于指代正在运行的可执行文件,它可以包含多个线程。任务:用于指代需要执行的工作的抽象概念。

线程的替代品

自己创建线程的一个问题是它们会给您的代码增加不确定性。线程是在应用程序中支持并发性的一种相对低级和复杂的方式。如果您不完全理解您的设计选择的含义,您很容易遇到同步或计时问题,其严重程度可能从细微的行为变化到应用程序崩溃和用户数据损坏。

另一个需要考虑的因素是您是否需要线程或并发。线程解决了如何在同一进程内并发执行多个代码路径的具体问题。但是,在某些情况下,您正在执行的工作量并不能保证并发性。线程会在内存消耗和 CPU 时间方面为您的进程带来大量开销。您可能会发现这种开销对于预期任务来说太大了,或者其他选项更容易实现。

图 1-1列出了线程的一些替代方案。该表包括线程的替代技术(例如操作对象和 GCD)以及旨在有效使用您已有的单线程的替代技术。

图1-1 线程的替代技术

线程管理:线程成本

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

图2-1量化了与在应用程序中创建新的用户级线程相关的近似成本。其中一些成本是可配置的,例如为辅助线程分配的堆栈空间量。创建线程的时间成本是一个粗略的近似值,应仅用于相互之间的相对比较。线程创建时间可能会因处理器负载、计算机速度以及可用系统和程序内存量的不同而有很大差异。

图2-1 线程创建成本

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

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

创建线程

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

使用 NSThread

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

detachNewThreadSelector:toTarget:withObject:类方法生成新线程。创建一个新NSThread对象并调用它的start方法。(仅在 iOS 和 OS X v10.5 及更高版本中受支持。)这两种技术都会在您的应用程序中创建一个分离的线程。分离的线程是指当线程退出时,系统会自动回收该线程的资源。这也意味着您的代码以后不必显式加入线程因为;

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

代码语言:javascript
复制
[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 及更高版本中初始化对象的简单方法是使用

initWithTarget:selector:object:方法。此方法采用与方法完全相同的信息,detachNewThreadSelector:toTarget:withObject:并使用它来初始化一个新NSThread实例。但是,它不会启动线程。要启动线程,请start显式调用线程对象的方法,如下例所示:

代码语言:javascript
复制
NSThread* myThread = [[NSThread alloc] initWithTarget:self
                                             selector:@selector(myThreadMainMethod:)
                                               object:nil];
[myThread start];  // Actually create the thread

滑动显示更多

如果您有一个NSThread对象的线程当前正在运行,则可以向该线程发送消息的performSelector:onThread:withObject:waitUntilDone:一种方法是使用应用程序中几乎所有对象的方法。

OS X v10.5 中引入了对在线程(主线程除外)上执行选择器的支持,这是一种在线程之间进行通信的便捷方式。(iOS 中也提供此支持。)您使用此技术发送的消息由另一个线程直接执行,作为其正常运行循环处理的一部分。

(当然,这确实意味着目标线程必须在其运行循环中运行;请参阅Run Loops。)当您以这种方式进行通信时,您可能仍然需要某种形式的同步,但它比在两个线程之间设置通信端口更简单线程。注意: 虽然对于线程间的偶尔通信有好处,

performSelector:onThread:withObject:waitUntilDone:对于时间紧迫或线程间频繁的通信,您不应该使用该方法。

编写线程入口例程

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

创建自动释放池

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

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

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

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

代码语言:javascript
复制
- (void)myThreadMainRoutine
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // Top-level pool

    // Do thread work here.

    [pool release];  // Release the objects in the pool.
}

滑动显示更多

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

设置异常处理程序

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

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

设置运行循环

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

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

终止线程

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

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

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

代码语言:javascript
复制
- (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];
    }
}
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-08-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 HelloCoder全栈小集 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
数据保险箱
数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档