Threads(一)线程概述

随着单个处理器的速度开始达到实际极限,芯片制造商转向了多核设计,这为计算机提供了同时执行多个任务的机会。尽管系统会尽可能利用这些核心来执行system-related tasks(系统相关的任务),但是我们的App可以通过threads利用它们。

  • 进程(process),指的是正在运行的可执行文件,它可以包含多个线程。
  • 线程(thread),表示代码的独立执行路径。
  • 任务(task),用于表示需要执行的工作的抽象概念。(task有时指正在运行的进程。)

线程

线程的概念

  • 线程(Threads)是在应用程序内部实现多个执行路径的一种相对轻量级方式。

在系统层面,程序并行运行,系统根据其需求和其它程序的需求为每个程序提供执行时间。在每个程序内部都存在一个或多个执行的thread,这些thread可以用来同时或以几乎同时的方式执行不同的任务。系统本身实际上管理这些执行thread,安排它们在可用的核心上运行,并根据需要预先中断它们以允许其他线程运行;

  • 从技术角度来看,thread是管理代码执行所需的 kernel-level(内核级) 和 application-level(应用程序级) 数据结构的组合。

kernel-level结构 协调将事件分派到thread 和 在一个可用内核上抢占式调度threadapplication-level结构包括 用于存储函数调用的调用栈,以及应用程序管理和操作thread的属性 和 状态所需的结构。

线程的优缺点

1、在应用程序中使用multiple threads(多线程)提供了两个非常重要的潜在优势:

  • multiple threads可以提高应用程序的响应能力;

  • multiple threads可以提高应用程序在多核系统上的实时性能;

2、单线程应用程序的问题

如果应用程序只有一个线程,则该线程必须执行所有操作。其问题在于,它一次只能执行一件事

那么,对于单线程应用程序,当一个计算需要很长时间才能完成时,应用程序将停止响应用户事件并更新其窗口,如果这种行为持续足够长的时间,用户可能会认为应用程序已挂起,并尝试强制退出它。但是,如果将耗时计算移动到一个单独的线程上,则应用程序的主线程将可以更及时地响应用户交互。

3、多线程的问题

使用多线程的应用程序,在执行不同任务的线程可以在不同处理器核心上同时执行操作,从而使应用程序可以在给定的时间内增加其工作量。

多线程也潜在问题,在应用程序中具有多个执行路径可能会增加代码的复杂性

每个线程必须与其他线程协调其操作,以防止损坏应用程序的状态信息。由于单个应用程序中的线程共享相同的内存空间,因此它们可以访问所有相同的数据结构。如果两个线程试图同时操作同一个数据结构,一个线程可能会以破坏结果数据结构的方式覆盖另一个线程的更改。即使有适当的保护措施,你仍然必须提防编译器优化,这些优化会在代码中引入细微的错误。

线程的替代方案

通过自己创建线程实现并发,需要考虑如下两个问题:

  • 自己创建线程会增加代码的不确定性。

线程是在应用程序中支持并发的相对低级和复杂方式。如果您不完全理解设计选项的含义,则很容易会遇到 同步或定时问题,其严重性可能 从微妙的行为变化 到应用程序崩溃 和 用户数据损坏

  • 另一个要考虑的因素是你是否需要线程或并发。

线程解决了如何在同一进程内并发执行多个代码路径的特定问题。但是,在某些情况下,你所做的工作量不能保证并发。线程在 memory consumption(内存消耗) 和 CPU time 方面都会给您的进程带来巨大开销。您可能会发现此开销对于预期的任务来说太大了,或者其他选项更易于实现。

如下列出了线程的一些替代方案,既包括线程的替换技术(OperationGCD),也包括旨在有效利用现有线程的替代方法。

1、Operation objects

Operation objects 是通常在子线程上执行任务的包装器。此包装隐藏了执行任务的线程管理方面,让您可以自由地专注于任务本身。

通常将operation objectsoperation queue object结合使用,operation queue object实际上管理一个或多个线程上operation objects的执行。在OS X v10.5中引入。

2、Grand Central Dispatch (GCD)

使用GCD,可以定义要执行的任务并将其添加到work queue中,该队列处理在适当线程上的任务调度。work queue考虑了 可用核心的数量 和 当前负载,以比使用线程更有效地执行任务。在Mac OS x v10.6中引入。

Operation objectsGCD 是使用多线程的两种方案,可以使你专注于需要执行的任务,而不是线程管理。

3、Idle-time notifications

对于相对较短且优先级很低的任务,Idle-time notifications(空闲时间通知)允许你在应用程序不那么忙的时候执行任务。Cocoa使用NSNotificationQueue对象来提供idle-time notifications。要请求idle-time notification,请使用 NSPostWhenIdle 选项将通知发布到默认的 NSNotificationQueue 对象。队列将延迟通知对象的传递,直到run loop变为空闲。

4、Asynchronous functions

系统接口包括许多asynchronous functions(异步函数),为你提供自动并发。这些API可以使用 system daemons(系统守护进程) 和 进程,或者创建custom threads来执行任务并将结果返回给你。在设计应用程序时,请寻找提供异步行为的函数,并考虑使用它们,而不是在自定义线程上使用等效的同步函数。

5、Timers

可以在应用程序的主线程上使用Timers来执行一些周期性的任务,这些任务由于太琐碎而不需要线程,但是仍然需要定期地进行维护。

6、Separate processes(单独的进程)

如果任务需要大量内存,或者必须使用root特权执行,则可以使用进程。尽管比线程更重量级,但在任务与应用程序仅略有关联的情况下,创建一个单独的进程可能很有用。

警告:当使用fork函数启动单独的进程时,必须总是在调用fork之后调用exec或类似的函数。依赖于Core FoundationCocoaCore Data框架(显式或隐式)的应用程序必须后续调用exec函数,否则这些框架可能会行为不当。

线程技术

线程的底层实现机制是Mach threads,但是很少使用Mach级别的线程,而通常使用更方便的POSIX API或它的衍生产品之一。

Mach的实现提供了所有线程的基本功能,包括preemptive execution model(优先执行模型)和schedule threads(调度线程)的能力,使线程之间彼此独立。

如下是在 Cocoa 应用程序中创建线程的技术:

技术 描述
Cocoa threads Cocoa使用NSThread类实现线程。Cocoa还提供了NSObject上的方法,用于生成新线程并在已经运行的线程上执行代码。
POSIX threads POSIX线程为创建线程提供了一个基于c的接口pthread。如果您不编写Cocoa应用程序,这是创建线程的最佳选择。

线程三种状态

application level(应用程序层面),所有线程的行为方式基本上与其他平台上相同。启动线程后,线程以三种主要状态之一运行:运行(running)、就绪(ready)或阻塞(blocked)。

如果一个线程当前没有运行,则它要么被阻塞并等待输入,要么准备运行但尚未计划这样做。线程继续在这些状态之间来回移动,直到它最终退出并进入终止状态。

创建新线程时,必须为该线程指定一个entry-point function(入口点函数)(对于Cocoa线程,则为entry-point method)。这个entry-point function构成了要在线程上运行的代码。当函数返回时,或显式终止线程时,线程将永久停止并由系统回收。

由于创建线程在 内存消耗 和 CPU时间 方面相对昂贵,因此建议entry point function上执行大量的工作,或者设置一个run loop,以允许重复执行工作。

Run Loop 和 Threads

run loop是用于管理异步到达线程上的事件的基础架构。

run loop通过监视线程的一个或多个event sources来工作。当事件到达时,系统唤醒线程并将事件分派到run loop,然后 run loop 将事件分派给你指定的处理程序。如果没有准备处理的事件,则run loop将线程置于睡眠状态。

你不需要对创建的任何线程使用run loop,但是这样做可以为用户提供更好的体验。run loop使创建使用最少资源的long-lived threads(长寿命线程)成为可能。因为run loop在无事可做时将其线程置为睡眠状态,因此不需要轮询。轮询会浪费CPU循环,并阻止了处理器本身休眠和节省电量。

要配置run loop,您所要做的就是启动线程,获取run loop对象的引用,安装event handlers(事件处理程序),并告诉run loop运行。

系统提供的基础结构会自动处理主线程运行循环的配置。但是,如果你计划创建长寿的辅助线程,则必须自己为这些线程配置run loop

同步工具(Synchronization Tools)

线程编程的危险之一是多线程之间的resource contention(资源争用)。如果多个线程试图同时使用或修改同一资源,可能会出现问题。

解决这多线程资源争用问题的方法:

  • 其一种方法:完全消除共享资源,并确保每个线程都有自己不同的资源集来进行操作。

  • 但是,当不能维护完全独立的资源时,可能必须使用locks(锁)、conditions(条件)、atomic operations(原子操作)和其他技术来同步对资源的访问。

互斥锁

锁(Locks)为一次只能由一个线程执行的代码提供了暴力的保护形式。

最常见的lock类型是 mutual exclusion lock(互斥锁,又称为mutex),当一个线程尝试获取当前由另一个线程持有的互斥体时,它将阻塞,直到另一个线程释放锁为止。

一些系统框架提供了对互斥锁的支持,尽管它们都基于相同的底层技术。此外,Cocoa提供了互斥锁的多种变体,来支持不同类型的行为。

条件(conditions)

除了锁之外,系统还提供了对 conditions(条件) 的支持,以确保在应用程序中正确的排序任务。

conditions(条件) 充当 gatekeeper(看门人),阻塞给定线程,直到它表示的条件为真。当这种情况发生时,conditions将释放线程并允许其继续。POSIX层 和 Foundation框架都直接提供了对 conditions(条件) 的支持。

如果使用operation objects,则可以配置操作对象之间的依赖关系以对任务的执行进行排序,这与conditions(条件)提供的行为非常相似。

Atomic Operations(原子操作)

尽管 Locksconditions在并行设计中非常常见,但是原子操作(atomic operations)是另一种保护和同步对数据访问的方法。

在可以对标量数据类型(int、float、Bool...)执行数学或逻辑运算的情况下,atomic operations 提供了一种轻量级替代 locks 的方法。

Atomic Operations使用特殊的硬件指令来确保在其他线程有机会访问变量之前完成对变量的修改。

线程间通讯(Inter-thread Communication)

虽然一个好的设计可以将所需的通信量降至最低,但在某个时候,线程之间的通信变得必要。

线程可能 需要处理新的工作请求 或 将其进度报告给应用程序的主线程。在这些情况下,你需要一种方法从一个线程到另一个线程获取信息。由于,线程共享相同的进程空间,这意味着您有很多通信选项。

线程之间有许多通信方式,每种都有其自身的优点和缺点。如下表列出了可以在MacOS中使用的最常见的 通信机制,此表中的技术按复杂度递增的顺序排序。

如下列出了 OS X 中可以使用的最常见通信机制,下表除了 Message queuesCocoa distributed objects 外,其它技术在iOS中也可用。

1、Direct messaging(直接通讯)

Cocoa应用程序支持直接在其他线程上执行selectors的能力,即Cocoa Perform Selector Sources。这个功能意味着一个线程可以在任何其他线程上执行一个方法。由于它们是在目标线程的上下文中执行的,所以以这种方式发送的消息将在该线程上自动序列化。

2、Global variables, shared memory, and objects

两个线程之间传递信息的另一个简单方法是使用global variable(全局变量)、shared object(共享对象) 或 shared block。尽管shared variables快速而简单,但是必须使用locks或其他同步机制仔细保护,以确保代码的正确性。否则,可能会导致竞争状况、数据损坏或崩溃。

3、Conditions

Conditions是一种同步工具,可以用于控制线程何时执行代码的特定部分。只有在满足所述条件时才允许线程运行。

4、Run loop sources

自定义run loop sources是你为接收线程上特定于应用程序的消息而设置的源。由于它们是事件驱动的,因此run loop sources可以使您的线程在无事可做时自动进入睡眠状态,从而提高了线程的效率。

5、Portssockets

基于端口的通信是两个线程之间通信的一种更为复杂的方式,但它也是一种非常可靠的技术。更重要的是,Portssockets可用于与外部实体(如其他进程和服务)进行通信。为了提高效率,端口是使用run loop source实现的,因此当端口上没有数据等待时,线程就会休眠。

6、Message queues

传统的多处理服务定义了先进先出(FIFO)队列抽象,用于管理传入和传出数据。尽管消息队列既简单又方便,但是它们却不如其他一些通信技术高效。

7、Cocoa distributed objects

distributed objects(分布式对象)是一种Cocoa技术,可提供基于端口的通信的高级实现。虽然可以将此技术用于线程间通信,但由于它会产生大量的开销,因此强烈不建议这样做。Distributed Objects Programming Topics |

线程设计技巧

下面的一些技巧,可以帮助你以确保代码正确的方式实现线程,以及帮助你使用自己的线程代码获得更好的性能。

避免显式地创建线程

手动编写线程创建代码很繁琐,而且可能容易出错,应该尽可能避免这样做。MacOSiOS 通过其他API为并发提供隐式支持。与其自己创建线程,不如考虑使用 asynchronous APIsGCDoperation objects来完成这项工作。这些技术可以在幕后为你完成与线程相关的工作,并保证正确地完成这些工作。

此外,GCDoperation objects等技术旨在根据当前系统负载调整活动线程的数量,从而比你自己的代码更有效地管理线程。

保持线程合理忙碌

如果决定手动创建和管理线程,请记住线程会消耗宝贵的系统资源。你应该尽力确保分配给线程的所有任务都可以长期有效的工作。

同时,你不必担心终止大部分时间闲置的线程。线程占用的内存非常少,其中一些是连接的,因此释放空闲线程不仅有助于减少应用程序的内存占用,还可以释放更多物理内存供系统其他进程使用。

在开始终止空闲线程之前,应该始终记录一组应用程序当前性能的基线测量值。在尝试更改之后,请采取额外的度量来验证这些更改实际上是在提高性能,而不是损害性能。

避免共享数据结构

避免与线程相关的资源冲突,最简单的方法是给程序中的每个线程提供自己所需数据的副本。当你最小化线程之间的通信和资源争用时,并行代码效果最好。

创建多线程应用程序很困难。即使你非常谨慎并在代码的所有正确位置锁定共享数据结构,您的代码在语义上仍可能是不安全的。例如,如果您的代码希望共享数据结构按特定顺序进行修改,则可能会遇到问题。将您的代码更改为基于事务的模型(transaction-based model)以进行补偿,可能会导致多线程的性能优势被抵消。

消除资源争用通常会导致设计简单,性能出色。

线程和用户界面

如果应用程序具有图形用户界面,建议您从应用程序的主线程接收与用户相关的事件并启动界面更新。这种方法有助于避免与 处理用户事件 和 绘制窗口 内容相关的同步问题。

有一些值得注意的例外,从其他线程执行图形操作是有利的。例如,您可以使用辅助线程来创建和处理图像以及执行其他与图像有关的计算。对这些操作使用辅助线程可以大大提高性能。

Cocoa Drawing Guide

注意退出时的线程行为

进程将一直运行,直到所有non-detached(未分离)的线程退出为止。默认情况下,只有应用程序的主线程被创建为non-detached,但你也可以通过这种方式创建其他线程。

当用户退出应用程序时,通常认为应该立即终止所有detached threads(分离线程)是适当的行为,因为detached threads所做的工作被认为是可选的。但是,如果您的应用程序正在使用后台线程将数据保存到磁盘或执行其他关键工作,则可能需要将这些线程创建为non-detached,以防止在应用程序退出时丢失数据。

将线程创建为 non-detached(也称为joinable(可连接)) 需要你进行额外的工作。由于大多数高级线程技术在默认情况下不会创建joinable threads,因此您可能必须使用POSIX API来创建线程。此外,必须向应用程序的主线程添加代码,以便在non-detached threads最终退出时加入。

如果你正在写一个macOS应用程序,也可以使用applicationShouldTerminate: 代理方法来延迟应用程序的终止,直到以后或者干脆取消它。当延迟终止时,您的应用程序需要等到任何关键线程完成任务,然后调用replyToApplicationShouldTerminate:方法。

处理异常(Handle Exceptions)

异常处理机制 依赖于当前调用堆栈在抛出异常时执行任何必要的清理。因为每个线程都有自己的调用堆栈,所以每个线程负责捕获自己的异常。在辅助线程中未能捕获到异常与在主线程中未能捕获到异常相同:拥有异常的进程被终止。不能抛给其他线程处理未捕获的异常。

如果你需要将当前线程中的异常情况通知另一个线程(如主线程),你应该捕获异常并简单地向另一个线程发送一条消息,指示发生了什么。根据您的模型和您试图做的事情,捕获异常的线程可以继续处理(如果可能的话)、等待指令、或者干脆退出。

Cocoa中,NSException对象是一个独立的对象,一旦被捕获,即可在线程之间传递。

在某些情况下,可能会自动为您创建一个异常处理程序。 例如,Objective-C中的@synchronized指令包含一个隐式异常处理程序。

干净地终止线程

线程退出的最佳方法是自然的让它到达其主入口点例程的末尾。

虽然有一些函数可以立即终止线程,但这些函数只能作为最后的手段使用。在线程到达其自然终点之前终止它会阻止线程在自身之后清理干净。

如果线程已分配内存、打开了文件或获取了其他类型的资源,则代码可能无法回收这些资源,从而导致内存泄漏或其他潜在问题。

库中的线程安全(Thread Safety in Libraries)

虽然应用程序开发人员可以控制应用程序是否使用多个线程执行,但是库开发人员不能。开发库时,必须假定调用的应用程序是多线程的,或者可以随时切换为多线程的。因此,你应该始终对代码的关键部分使用锁。

对于库开发人员来说,只在应用程序变为多线程时才创建锁是不明智的。如果需要在某个时刻锁定代码,请在使用库的早期创建lock对象,最好是通过某种显式调用来初始化库。尽管您也可以使用静态库初始化函数来创建这样的锁,但是只有在没有其他方法的情况下才尝试这样做。函数的初始化可能会对函数的执行造成不利影响。

始终记得在库中平衡对互斥锁的锁定和解锁调用。您还应该记住锁定库数据结构,而不是依赖调用代码来提供线程安全的环境。

学些博客

Threading Programming Guide

Concurrency Programming Guide

读 Threading Programming Guide 笔记(一)

文章作者: Czm
文章链接: http://yoursite.com/2020/10/15/Threads-%E4%B8%80-%E7%BA%BF%E7%A8%8B%E6%A6%82%E8%BF%B0/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Czm