关于线程编程

什么是线程?

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

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

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

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

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

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

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

线程术语

  • 术语线程用于指代码的单独执行路径。

  • 术语进程用于指代正在运行的可执行文件,它可以包含多个线程。

  • 术语任务用于指需要执行的抽象工作概念。

线程的替代方案

线程的替代技术:

  • 操作对象

在OS X v10.5中引入的操作对象是通常在辅助线程上执行的任务的包装器。这个包装器隐藏了执行任务的线程管理方面,让您可以自由地专注于任务本身。通常将这些对象与一个操作队列对象结合使用,该对象实际上管理一个或多个线程上的操作对象的执行。

  • Grand Central Dispatch(GCD)

在Mac OS x v10.6中引入的Grand Central Dispatch是线程的另一种替代方案,可让您专注于执行所需的任务而不是线程管理。使用GCD,您可以定义要执行的任务并将其添加到工作队列中,该工作队列可以在适当的线程上处理您的任务计划。工作队列考虑可用内核的数量和当前负载,以便比使用线程更有效地执行任务。

  • 空闲时间通知(Idle-time notifications)

对于相对较短且优先级非常低的任务,空闲时间通知可让您在应用程序不太忙时执行任务。Cocoa 使用NSNotificationQueue对象为空闲时间通知提供支持。要请求空闲时间通知,请使用NSPostWhenIdle选项向默认NSNotificationQueue对象发布通知。队列会延迟通知对象的传递,直到运行循环变为空闲状态。

  • 异步函数

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

  • 计时器

您可以在您的应用程序主线程上使用定时器来执行定期任务,这些任务对于需要线程而言太重要,但仍需要定期进行维护。

  • 单独的进程

线程支持

线程包

虽然线程的底层实现机制是 Mach 线程,但您很少在 Mach 级别使用线程。相反,您通常使用更方便的 POSIX API 或其衍生工具之一。Mach实现确实提供了所有线程的基本特征,但是,包括抢先执行模型和调度线程的能力,因此它们彼此独立。

  • Cocoa 线程

Cocoa 使用 NSThread 类实现线程。Cocoa 还提供 NSObject 上的方法来生成新线程并在已经运行的线程上执行代码。

  • POSIX 线程

POSIX 线程为创建线程提供了一个基于C的接口。如果你没有编写一个 Cocoa 应用程序,这是创建线程的最佳选择。POSIX 接口使用起来相对简单,并为配置线程提供了足够的灵活性。

  • 多处理服务

多处理服务是一个传统的基于C的接口,用于从旧版 Mac OS 转换而来的应用程序。这项技术仅适用于 OS X,应该避免任何新的开发。

在应用程序级别,所有线程的行为与其他平台上的行为基本相同。启动线程后,线程将以三种主要状态之一运行:运行,准备就绪或阻塞。如果一个线程当前没有运行,它将被阻塞并等待输入,或者它已准备好运行,但尚未安排执行。线程继续在这些状态之间来回移动,直到它最终退出并移动到终止状态。

当你创建一个新线程时,你必须为该线程指定一个入口函数(或Cocoa线程的入口点方法)。这个入口函数构成了你想要在线程上运行的代码。当函数返回时,或者当您明确终止线程时,该线程会永久停止并由系统回收。由于线程在内存和时间方面创建起来相对昂贵,因此建议您的入口点函数执行大量工作或设置运行循环以允许执行重复性工作。

Run Loops

运行循环是用于管理在线程上异步到达的事件的基础设施。运行循环通过监视线程的多个事件源来工作。当事件到达时,系统唤醒线程并将事件分派给运行循环,然后运行循环将其分派给您指定的处理程序。如果没有事件存在并准备好处理,则运行循环将使线程进入休眠状态。

您不需要在创建任何线程时使用运行循环,但这样做可以为用户提供更好的体验。运行循环可以创建使用最少量资源的长效线程。由于运行循环会在无所事事时将线程置于休眠状态,因此无需轮询,浪费CPU周期并防止处理器本身进入休眠状态并节省功耗。

要配置运行循环,您只需启动线程,获取对运行循环对象的引用,安装事件处理程序并指示运行循环运行。OS X 提供的基础架构自动为您处理主线程运行循环的配置。但是,如果您打算创建长寿命的辅助线程,则必须自行为这些线程配置运行循环。

同步工具

线程编程的一个危险是多线程间的资源争用。如果多个线程尝试同时使用或修改相同的资源,则可能会出现问题。缓解问题的一种方法是完全消除共享资源,并确保每个线程都有自己独特的资源集合来运行。但是,当保持完全独立的资源不是一个选项时,您可能必须使用锁,条件,原子操作和其他技术来同步对资源的访问。

锁对于一次只能由一个线程执行的代码提供蛮力保护形式。最常见的类型是互斥锁(mutual exclusion lock 或 mutex)。当一个线程试图获取另一个线程当前拥有的互斥锁时,它会阻塞,直到另一个线程释放该锁。几个系统框架为互斥锁提供支持,尽管它们都基于相同的基础技术。另外,Cocoa提供了互斥锁的几种变体来支持不同类型的行为,比如递归(recursion)。

除了锁之外,系统还为条件(condition)提供支持,以确保在应用程序中对任务进行正确排序。条件充当守门人,阻塞给定的线程,直到它所表示的条件变为真。当这种情况发生时,条件释放线程并允许它继续。POSIX 层和 Foundation 框架都为条件提供了直接支持。(如果使用操作对象,则可以配置操作对象之间的依赖关系来排序任务的执行,这与条件提供的行为非常相似。)

虽然锁和条件在并发设计中非常常见,但原子操作是保护和同步数据访问的另一种方式。在可以对标量数据类型执行数学或逻辑运算的情况下,原子操作提供了一种轻量级的替代方法。原子操作使用特殊的硬件指令来确保在其他线程有机会访问变量之前完成对变量的修改。

线程间通信

尽管一个好的设计可以最大限度地减少所需的通信量,但在某些时候,线程之间的通信变得必要。线程可能需要处理新的工作请求或将其进度报告给应用程序的主线程。在这些情况下,您需要一种从一个线程向另一个线程获取信息的方法。幸运的是,线程共享相同的进程空间的事实意味着您有很多通信选项。

线程之间有许多交流的方式,每种方式都有自己的优点和缺点。

通信机制:

  • 直接发送消息

Cocoa 应用程序支持直接在其他线程上执行选择器的功能。这个能力意味着一个线程可以基本上在任何其他线程上执行一个方法。由于它们是在目标线程的上下文中执行的,因此以这种方式发送的消息会自动在该线程上序列化。

  • 全局变量,共享内存和对象

在两个线程之间传递信息的另一种简单方法是使用全局变量,共享对象或共享内存块。虽然共享变量很快且简单,但它们比直接消息更脆弱。共享变量必须用锁或其他同步机制小心保护,以确保代码的正确性。不这样做可能会导致竞争状况,数据损坏或崩溃。

  • 条件(Conditions)

条件是一个同步工具,您可以使用它来控制线程何时执行特定代码部分。您可以将条件视为守门员(看门狗),让线程只有在符合条件时才能运行。

  • 运行循环源(Run loop sources)

自定义运行循环源是您设置为在线程上接收特定于应用程序的消息的。因为它们是事件驱动的,所以当没有任何事情可以做时,运行循环源让你的线程自动进入睡眠状态,这可以提高线程的效率。

  • 端口和套接字(Ports and sockets)

基于端口的通信是两种线程之间通信的更精细的方式,但它也是一种非常可靠的技术。更重要的是,端口和套接字可用于与外部实体(如其他进程和服务)进行通信。为了提高效率,端口是使用运行循环源实现的,所以当没有数据在端口上等待时,线程会休眠。

  • 消息队列

传统的多处理服务定义了用于管理传入和传出数据的先进先出(FIFO)队列抽象。尽管消息队列简单方便,但并不像其他通信技术那样高效。

  • Cocoa 分布式对象

分布式对象是一种 Cocoa 技术,提供基于端口通信的高级实现。虽然有可能使用这种技术进行线程间通信,但由于其发生的开销很大,因此非常不鼓励。分布式对象更适合与其他进程进行通信,其中进程之间的开销已经很高

设计技巧

避免明确地创建线程

手动编写线程创建代码非常繁琐且可能容易出错,应尽可能避免它。

保持你的线程合理繁忙
避免共享数据结构
线程和您的用户界面
在退出时注意线程行为
处理异常
干净地终止你的线程
线程安全库

results matching ""

    No results matching ""