iOS底层原理探索 之 多线程原理|8月更文挑战

1,174 阅读33分钟
写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。

目录如下:

  1. iOS 底层原理探索 之 alloc
  2. iOS 底层原理探索 之 结构体内存对齐
  3. iOS 底层原理探索 之 对象的本质 & isa的底层实现
  4. iOS 底层原理探索 之 isa - 类的底层原理结构(上)
  5. iOS 底层原理探索 之 isa - 类的底层原理结构(中)
  6. iOS 底层原理探索 之 isa - 类的底层原理结构(下)
  7. iOS 底层原理探索 之 Runtime运行时&方法的本质
  8. iOS 底层原理探索 之 objc_msgSend
  9. iOS 底层原理探索 之 Runtime运行时慢速查找流程
  10. iOS 底层原理探索 之 动态方法决议
  11. iOS 底层原理探索 之 消息转发流程
  12. iOS 底层原理探索 之 应用程序加载原理dyld (上)
  13. iOS 底层原理探索 之 应用程序加载原理dyld (下)
  14. iOS 底层原理探索 之 类的加载
  15. iOS 底层原理探索 之 分类的加载
  16. iOS 底层原理探索 之 关联对象
  17. iOS底层原理探索 之 魔法师KVC
  18. iOS底层原理探索 之 KVO原理|8月更文挑战
  19. iOS底层原理探索 之 重写KVO|8月更文挑战

以上内容的总结专栏


细枝末节整理


前言

在我们实际的开发过程中,会有各种各样的业务逻辑需要完成,随着业务的开展和内容的复杂度越来越高,多线程技术的应用显得越来越重要。本篇内容开始,我们就展开探索在iOS开发中,对于多线程的原理、应用以及面试中的一些坑点的内容整理总结。好的,内容一点一点展开,大家,加油!!

线程是什么

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

关于线程编程

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

什么是线程?

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

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

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

  • 多线程可以提高应用程序的感知响应能力。
  • 多线程可以提高应用程序在多核系统上的实时性能。

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

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

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

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

线程的替代品

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

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

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

表1-1  线程的替代技术

技术描述
Operation objectsOS X v10.5 中引入的操作对象是通常在辅助线程上执行的任务的包装器。这个包装器隐藏了执行任务的线程管理方面,让您可以自由地专注于任务本身。您通常将这些对象与操作队列对象结合使用,该对象实际上管理一个或多个线程上的操作对象的执行。有关如何使用操作对象的更多信息,请参阅*并发编程指南*。
Grand Central Dispatch (GCD)在 Mac OS x v10.6 中引入的 Grand Central Dispatch 是另一种线程替代方案,它让您可以专注于需要执行的任务而不是线程管理。使用 GCD,您可以定义要执行的任务并将其添加到工作队列中,该队列在适当的线程上处理任务的调度。工作队列会考虑可用内核的数量和当前负载,以比使用线程更有效地执行任务。有关如何使用 GCD 和工作队列的信息,请参阅*并发编程指南*
Idle-time notifications对于相对较短且优先级非常低的任务,空闲时间通知可让您在应用程序不那么忙的时候执行任务。Cocoa 使用NSNotificationQueue对象提供对空闲时间通知的支持。要请求空闲时间通知,请NSNotificationQueue使用NSPostWhenIdle选项向默认对象发布通知。队列会延迟通知对象的传递,直到运行循环空闲。有关更多信息,请参阅*通知编程主题*。
Asynchronous functions系统接口包括许多为您提供自动并发的异步函数。这些 API 可能使用系统守护进程和进程或创建自定义线程来执行它们的任务并将结果返回给您。(实际实现无关紧要,因为它与您的代码分离。)在设计应用程序时,寻找提供异步行为的函数并考虑使用它们,而不是在自定义线程上使用等效的同步函数。
Timers您可以在应用程序的主线程上使用计时器来执行周期性任务,这些任务太琐碎而不需要线程,但仍需要定期进行服务。有关计时器的信息,请参阅计时器源
Separate processes尽管比线程更重要,但在任务仅与您的应用程序相关的情况下,创建单独的进程可能很有用。如果任务需要大量内存或必须使用 root 权限执行,您可以使用进程。例如,您可能使用 64 位服务器进程来计算大型数据集,而您的 32 位应用程序向用户显示结果。

线程管理

线程成本

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

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

表 2-1 线程创建成本

物品大概费用笔记
内核数据结构约 1 KB该内存用于存储线程数据结构和属性,其中大部分被分配为有线内存,因此无法分页到磁盘。
堆栈空间512 KB(辅助线程)8 MB(OS X 主线程)1 MB(iOS 主线程)辅助线程的最小允许堆栈大小为 16 KB,并且堆栈大小必须是 4 KB 的倍数。该内存的空间在线程创建时在您的进程空间中留出,但与该内存关联的实际页面直到需要它们时才会创建。
创建时间大约 90 微秒该值反映了创建线程的初始调用与线程的入口点例程开始执行之间的时间。这些数字是通过分析在基于 Intel 的 iMac 上创建线程期间生成的平均值和中值确定的,该 iMac 配备 2 GHz Core Duo 处理器和 1 GB RAM,运行 OS X v10.5。

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

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

创建线程

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

使用 NSThread

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

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

这两种技术都会在您的应用程序中创建一个分离的线程。分离的线程是指当线程退出时,系统会自动回收该线程的资源。这也意味着您的代码以后不必显式加入线程。

因为detachNewThreadSelector:toTarget:withObject:所有版本的 OS X 都支持该方法,所以在使用线程的现有 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 及更高版本中初始化对象的简单方法是使用initWithTarget:selector:object:方法。此方法采用与方法完全相同的信息,detachNewThreadSelector:toTarget:withObject:并使用它来初始化一个新NSThread实例。但是,它不会启动线程。要启动线程,请start显式调用线程对象的方法,如下例所示:

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  定义线程入口点例程

- (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  在长时间作业期间检查退出条件

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

同步

应用程序中存在多个线程会带来有关从多个执行线程安全访问资源的潜在问题。修改同一资源的两个线程可能会以意想不到的方式相互干扰。例如,一个线程可能会覆盖另一个线程的更改或将应用程序置于未知且可能无效的状态。如果幸运的话,损坏的资源可能会导致明显的性能问题或崩溃,这些问题相对容易追踪和修复。但是,如果您不走运,损坏可能会导致微妙的错误,这些错误直到很久以后才会显现出来,或者这些错误可能需要对您的基本编码假设进行重大检查。

谈到线程安全,好的设计就是最好的保护。避免共享资源并最小化线程之间的交互可以降低这些线程相互干扰的可能性。然而,完全无干扰的设计并不总是可能的。在您的线程必须交互的情况下,您需要使用同步工具来确保它们在交互时安全地进行。

OS X 和 iOS 提供了许多同步工具供您使用,从提供互斥访问的工具到在应用程序中正确排序事件的工具。

同步工具

为防止不同线程意外更改数据,您可以将应用程序设计为没有同步问题,也可以使用同步工具。尽管完全避免同步问题是可取的,但这并不总是可能的。

原子操作

原子操作是一种简单的同步形式,适用于简单的数据类型。原子操作的优点是它们不会阻塞竞争线程。对于简单的操作,比如增加一个计数器变量,这可以带来比获取锁更好的性能。

OS X 和 iOS 包括许多操作来对 32 位和 64 位值执行基本的数学和逻辑运算。这些操作包括比较和交换、测试和设置和测试和清除操作的原子版本。有关支持的原子操作列表,请参见/usr/include/libkern/OSAtomic.h头文件或atomic手册页。

内存障碍和易失性变量

为了获得最佳性能,编译器经常对汇编级指令进行重新排序,以保持处理器的指令管道尽可能满。作为此优化的一部分,编译器可能会重新排序访问主内存的指令,因为它认为这样做不会生成不正确的数据。不幸的是,编译器并不总是能够检测到所有与内存相关的操作。如果看似独立的变量实际上相互影响,编译器优化可能会以错误的顺序更新这些变量,从而产生潜在的错误结果。

内存屏障是一种非阻塞同步工具,用于确保内存操作以正确的顺序发生。内存屏障就像一道栅栏,强制处理器完成位于屏障前面的任何加载和存储操作,然后才允许执行位于屏障之后的加载和存储操作。内存屏障通常用于确保一个线程(但对另一个线程可见)的内存操作总是以预期的顺序发生。在这种情况下缺少内存屏障可能会让其他线程看到看似不可能的结果。(例如,请参阅 Wikipedia 中的内存屏障条目。)要使用内存屏障,您只需OSMemoryBarrier在代码中的适当位置调用该函数即可。

易失性变量对单个变量应用另一种类型的内存约束。编译器通常通过将变量的值加载到寄存器中来优化代码。对于局部变量,这通常不是问题。但是,如果该变量对另一个线程可见,则这种优化可能会阻止另一个线程注意到它的任何更改。将volatile关键字应用于变量会强制编译器在每次使用该变量时从内存中加载该变量。您可以声明一个变量,就volatile好像它的值可以随时被编译器可能无法检测到的外部源更改一样。

由于内存屏障和 volatile 变量都会减少编译器可以执行的优化次数,因此应谨慎使用它们,并且仅在需要确保正确性的情况下使用它们。有关使用内存屏障的信息,请参阅 OSMemoryBarrier手册页。

锁是最常用的同步工具之一。您可以使用锁来保护代码的关键部分,这是一次仅允许一个线程访问的一段代码。例如,临界区可能会操作特定的数据结构或使用某种资源,一次最多支持一个客户端。通过在此部分周围放置一个锁,您可以排除其他线程进行可能影响您代码正确性的更改。

表4-1列出了一些程序员常用的锁。OS X 和 iOS 为大多数这些锁类型提供了实现,但不是全部。对于不受支持的锁类型,描述列解释了为什么这些锁没有直接在平台上实现的原因。

表 4-1 锁类型

描述
互斥锁 Mutex互斥(或互斥)锁充当资源周围的保护屏障。互斥体是一种信号量,一次只允许访问一个线程。如果互斥量正在使用中,而另一个线程试图获取它,则该线程会阻塞,直到互斥量被其原始持有者释放为止。如果多个线程竞争同一个互斥锁,则一次只允许一个线程访问它。
递归锁 Recursive lock递归锁是互斥锁的变体。递归锁允许单个线程在释放之前多次获取锁。其他线程保持阻塞状态,直到锁的所有者释放锁的次数与它获取锁的次数相同。递归锁主要在递归迭代期间使用,但也可用于多个方法各自需要单独获取锁的情况。
读写锁 Read-write lock读写锁也称为共享排他锁。这种类型的锁通常用于更大规模的操作,如果频繁读取受保护的数据结构并偶尔修改,则可以显着提高性能。在正常操作期间,多个阅读器可以同时访问数据结构。但是,当线程想要写入结构时,它会阻塞,直到所有读取器都释放锁,此时它获取锁并可以更新结构。当一个写线程正在等待锁时,新的读线程阻塞直到写线程完成。系统仅支持使用 POSIX 线程的读写锁。有关如何使用这些锁的更多信息,请参见pthread手册页。
分布式锁 Distributed lock分布式锁提供进程级别的互斥访问。与真正的互斥锁不同,分布式锁不会阻塞进程或阻止其运行。它只是报告锁何时忙,并让进程决定如何继续。
自旋锁 Spin lock自旋锁反复轮询其锁定条件,直到该条件变为真。自旋锁最常用于锁的预期等待时间较短的多处理器系统。在这些情况下,轮询通常比阻塞线程更有效,这涉及上下文切换和线程数据结构的更新。由于自旋锁的轮询性质,系统不提供任何自旋锁的实现,但您可以在特定情况下轻松实现它们。有关在内核中实现自旋锁的信息,请参阅*内核编程指南*。
双重检查锁 Double-checked lock双重检查锁试图通过在获取锁之前测试锁定标准来减少获取锁的开销。由于双重检查锁可能不安全,因此系统没有为它们提供明确的支持,因此不鼓励使用它们。

注意: 大多数类型的锁还包含内存屏障,以确保在进入临界区之前完成任何前面的加载和存储指令。

有关如何使用锁的信息,请参阅使用锁

线程安全和信号

当谈到线程应用程序时,没有什么比处理信号的问题更令人恐惧或困惑了。信号是一种低级 BSD 机制,可用于向进程传递信息或以某种方式对其进行操作。一些程序使用信号来检测某些事件,例如子进程的死亡。该系统使用信号来终止失控的进程并传达其他类型的信息。

信号的问题不在于它们做什么,而在于当您的应用程序有多个线程时它们的行为。在单线程应用程序中,所有信号处理程序都在主线程上运行。在多线程应用程序中,与特定硬件错误(例如非法指令)无关的信号被传递给当时正在运行的线程。如果多个线程同时运行,则信号被传递给系统碰巧选择的任何一个。换句话说,信号可以传递到应用程序的任何线程。

在应用程序中实现信号处理程序的第一条规则是避免假设哪个线程正在处理信号。如果一个特定的线程想要处理一个给定的信号,你需要想办法在信号到达时通知该线程。您不能仅仅假设从该线程安装信号处理程序将导致信号被传递到同一个线程。

有关信号和安装信号处理程序,看到更多的信息signalsigaction手册页。

总结

进程

  • 进程是指在系统中正在运行的一个应用程序
  • 每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存空间内
  • 通过活动监视器可以查看 Mac 系统中所开启的进程

线程

  • 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行
  • 进程要想执行任务,必须得有线程,进程至少要有一条线程
  • 程序启动会默认开启一条线程,这条线程被称为主线程或 UI 线程

进程与线程的关系

  • 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
  • 资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的 资源是独立的。

  1. 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进 程都死掉。所以多进程要比多线程健壮。
  2. 进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进 程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程 。
  3. 执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是 线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
  4. 线程是处理器调度的基本单位,但是进程不是。
  5. 线程没有地址空间,线程包含在进程地址空间中

多线程的意义

优点

  • 能适当提高程序的执行效率
  • 能适当提高资源的利用率(CPU、内存)
  • 线程中的任务执行完毕后,线程可以自动销毁

缺点

  • 开启线程需要占用一定的内存空间(默认情况下占512K)
  • 如果开启大量的线程,会占用大量的内存空间,降低程序的性能
  • 线程越多,CPU 在调用线程上的开销就越大
  • 程序设计更加复杂,比如线程间的通信、多线程的数据共享

多线程原理

时间片

CPU在多个任务之间进行快速的切换,这个时间间隔就是时间片


(单核CPU)同一时间,CPU 只能处理 1 个线程

  • 换言之,同一时间只有 1 个线程在执行

多线程同时执行

  • 是 CPU 快速的在多个线程之间的切换
  • CPU 调度线程的时间足够快,就造成了多线程的“同时”执行的效果


如果线程数非常多

  • CPU 会在 N 个线程之间切换,消耗大量的 CPU 资源
  • 每个线程被调度的次数会降低,线程的执行效率降低

模板2 - new_副本.gif

线程生命周期

线程被创建出来之后,我们就可使用它来完成我们指定给他的任务。我们作为开发这,自然是希望其能按照我们的业务逻辑,快速而且高质量的完成。

但是,具体的任务内容是交给系统去做处理,再往底层是交给CPU来执行代码。那么,一个线程从创建到最后被回收,会经历什么呢?我简单整理了下图:

线程生命周期.001.jpeg

线程大致有 就绪、运行、阻塞、死亡这几种状态。其生米周期也是和可调度线程池以及CPU的调度有直接的关系,线程创建出来之后,就会等待被CPU调度,此时是就绪状态,CPU调度到我们的线程之后,线程进入运行状态来执行任务,过程中,如果我们有调用sleep或者在等候同步锁或者从可调度线程池中移出后,则进入阻塞状态,带sleep结束,获取到同步锁,被添加到可调度线程池之后,我们的线程又回到就绪状态,等待被调度,直到最后被系统回收掉,也就是死亡状态。