关于线程编程(About Threaded Programming)

来源:互联网 发布:电脑编程语言 编辑:程序博客网 时间:2024/05/23 14:20

多年来,大部分电脑性能主要受到位于电脑中心的单个微处理器的速度限制。由于单个处理器的速度开始达到他们实际限制,然而,芯片制造商转向多核设计,使计算机可以同时执行多个任务。尽管OS X利用这些核心可以随时执行系统相关任务,你自己的应用也可以通过线程利用他们。

什么是线程?

线程是一个相对轻量级的方法在应用内部实现多路径执行。在系统层面上,程序一起运行,系统为每个程序发放执行时间,根据每个项目的需要和其他项目的需要。然而,在每个项目中存在一个或多个执行的线程,这些线程可以同时或几乎同时用来执行不同的任务。事实上系统本身管理这些线程的执行,安排线程在可用内核上运行并根据需要预先中断线程让其他线程运行。

从技术角度来看,线程是一个内核级和应用级数据结构的结合,用来管理执行代码。内核级结构协调线程的事件调度并预先安排线程到可用内核上。应用级结构包括存储函数调用的调用栈和应用需要管理和操作线程的属性和状态。

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

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

如果你的应用只有一个线程,该线程必须完成一切。它必须响应事件,更新应用程序的窗口并执行所需的所有计算来实现应用的行为。只有一个线程的问题在于,它一次只能做一件事。所以当你的一个技术需要很长时间完成,会发生什么?当你的代码忙于计算值,你的应用停止响应用户事件及窗口更新。如果这种行为持续很长时间,用户可能认为你的应用挂了并试图强行退出。如果你将自定义计算转移到一个单独的线程时,你的主线程可以自由的响应用户交互,更加及时。

现在多核计算机比较常见,线程提供一种方法来提高某些类型应用的性能。执行不同人物的线程可以同时在不同的处理器核心上,使应用在给定的时间内增加工作量。

当然,线程不是修复应用性能问题的灵丹妙药。随着线程提供的好处同时也有潜在的问题。在应用中执行多路径会增加代码的复杂性。每个线程必须根据其他系统协调自身的行为,防止它扰乱应用状态信息。由于单个应用的线程共享相同的内存空间,他们都可以访问相同的数据结构。如果两个线程视图同时操作相同数据结构,一个线程可能会覆盖另一个的更改,导致产生的数据结构被扰乱。即使有适当的保护措施,你仍然需要注意编译器优化在你的代码中引入微妙的(和不是那么微妙的)bug

线程术语

在深入讨论线程及其支持的技术之前,需要定义一些基本的术语。

如果你熟悉UNIX 系统,你会发现在本文中的“task”术语用法不同。在UNIX 系统中,“task”这个术语有时用来指一个运行的进程。

本文采用下列术语:

  • 线程(thread)指代码的一个单独执行路径。
  • 进程(process)指运行的可执行代码,可以包含多个线程。
  • 任务(task)指需要执行的工作才抽象概念。

线程的替代方案

自己创建线程的一个问题是会添加不确定因素到你的代码。线程是一个支持应用并发的相对低级别和复杂的方式。如果你不完全理解你设计选择的影响,你很容易面临同步或时机问题,严重的范围可以从细微行为的改变导致应用的崩溃和用户数据的混乱。

考虑另一因素是你是否需要线程或并发。线程解决具体的问题,例如如何在相同进程中如何同时执行多个代码路径。不过,某些情况下,你正在做的工作不保证并发现。线程会在你的进程中引入大量的开销,无论是内存消耗还是CPU时间。你可能会发现这个消耗对于目标任务来说太大,或者其他可选方案更加容易实现。

表1-1 列出了一些线程的替代技术。此表包括线程替代技术(例如操作对象和GCD)和有效使用你现有单线程的备选方案。

表1-1 线程的替代技术

技术

说明

操作对象

操作对象是在OS X v10.5中引入,它是一个任务的包装,通常会在第二个线程上执行。这个包装隐藏执行任务的线程管理,让你自由的专注于任务本身。通常你使用这些对象协同操作队列对象,来实际管理一个或多个线程上的操作对象的执行。

关于如何使用操作对象的更多信息,参见并发编程指南( Concurrency Programming Guide)。

中央调度(GCD

GCDMac OS x v10.6中引入,是线程的另一个替代方案,让你专注于你需要执行的任务而非线程管理。使用GCD,你定义你希望执行的任务并将其添加到工作队列,在适当的线程上处理任务安排。工作队列会考虑可用内核的数量和当前负载,执行任务的效率比你自己使用线程的效率要高。

关于如何使用GCD和工作队列的更多信息,参见并发编程指南( Concurrency Programming Guide)。

空闲时间通知

在相对低优先级的任务,空闲时间通知让你在应用不忙的时候执行任务。Cocoa支持空闲时间通知,使用NSNotificationQueue 对象。为了请求一个空闲通知,使用NSPostWhenIdle 选项推送一个通知到默认的NSNotificationQueue 对象。队列延迟交付你的通知对象直到运行循环变得空闲。更多信息,参阅通知编程主题(Notification Programming Topics)。

异步函数

系统接口包括许多异步函数,这些函数提供自动并发。这些API使用系统后台程序和进程或创建自定义线程来执行他们的任务并返回结果给你。(实际实现不重要,因为它与你的代码是分开的。)当你设计你的应用,寻找提供异步行为的函数,考虑使用它们而不是在自定义线程中使用等效的同步函数。

计时器

你可以在你应用的主线程中使用计时器来执行周期性任务,这些任务用线程通常太琐碎,而且需要定期维修。关于计时器的更多信息,参阅定时器来源(Timer Sources)。

独立进程

尽管比线程更加厚重,创建一个单独的进程在某些情况下非常有用,例如任务与你的应用无关。如果一个任务需要大量的内存或必须使用root权限执行,你可使用一个进程。例如,你可能使用一个64位的服务器进程来技术一个大型的数据集,用32位应用显示结果给用户。

警告,当启动独立进程时使用fork 函数,你必须始终遵守fork 的调用和exec 的调用或类似的函数。基于核心基础、Cocoa或核心数据框架(显式或隐式)的应用必须随后调用一个exec 函数或其他框架

线程支持

如果现有的代码使用了线程,OS XiOS为应用中创建线程提供了几种技术。此外,两个系统还支持在线程上完成管理和同步工作。以下章节描述了一些关键技术,当你在OS XiOS使用线程时必须注意的。

线程包

虽然线程的底层实现机制是Mach线程,你很少(如果有的话)在Mach级别使用线程。相反,你使用更方便的POSIX API或其衍生品。Mach的实现提供了所有线程的基础功能,然而,包括抢占式执行模型和调度线程的能力,所以他们是相互独立的。

清单2-2列出了在你应用中可用使用的线程技术。

表1-2 线程技术

技术

说明

Cocoa线程

Cocoa使用NSThread 类实现线程。Cocoa也提供NSObject 方法在正在运行的线程上产生新线程和执行代码。更多信息,参见使用NSThreadUsing NSThread)和使用NSThread生成线程(Using NSObject to Spawn a Thread)。

POSIX 线程

POSIX 线程提供了一个基于C的接口来创建线程。如果你不写Cocoa应用,则这是创建线程最好的选择。POSIX 接口使用起来相对简单并提供充足的灵活性来配置你的线程。更多信息,参见使用POSIX 线程(Using POSIX Threads)。

多重处理服务

多重处理服务是一个遗留的基于C的接口,主要用于应用从老版本的Mac OS过渡。该技术只可用于OS X上,应该避免任何新的发展。相反,你应该使用NSThread 类或POSIX 线程。如果你需要更多关于该技术的信息,参见多重处理服务编程指南(Multiprocessing Services Programming Guide)。

在应用级别,所有线程的行为在本质上与其他平台是相同的。在启动一个线程后,该线程运行在三个主要状态上:运行,准备或阻塞。如果一个线程当前没有运行也没有阻塞,只是等在输入或它准备运行但是没有安排。该线程继续在这些状态中来回切换,直到最后退出并移动到结束状态。

当你创建一个新线程时,你必须为该线程指定一个入口函数(或Cocoa线程的一个入口方法)。该入口函数构成你想要运行代码的线程。当函数返回时或当你显式终止线程,该线程将永久停止并被系统回收。因为,从内存和时间的角度,线程的创建相对比较昂贵,因此建议你的入口函数做大量的工作或建立一个运行循环允许重复工作。

关于可用的线程技术及如何使用它们的更多信息,参见线程管理(Thread Management)。

运行循环

运行循环是用于管理线程上事件异步到达的基础攻击。运行循环是通过监视线程一个或多个事件源来工作。当事件到达时,系统唤醒线程并分派事件到运行循环上,然后将它们分派你所指定的处理程序。如果没有事件出现并准备处理,运行循环线程进入睡眠状态。

你不需要使用一个运行循环,但这样做可以为用户提供更好的体验。运行循环使创建长期的线程同时使用最少的资源成为可能。因为当无事可做时,运行循环使线程进入睡眠状态,减少轮询。轮询会浪费CPU周期并阻止处理器本身睡眠和节电。

为了配置一个运行循环,你要做的事就是启动线程,获取运行循环对象的引用,安装你的事件处理程序并告知运行循环运行。OS X提供的工具自动为你处理主线程的运行循环配置。如果你打算创建一个长期次要线程,然而,你必须自己为这些线程配置运行循环。

运行循环和如何使用它们的例子的详情见运行循环(Run Loops)。

同步工具

线程编程的危害之一是多个线程资源竞争。如果多个线程试图同时使用或修改相同的资源,会产生问题。缓解这个问题的一个方法是消除共享资源并确保每个线程有自己单独操作的资源。当保持完全独立的资源不是一个可选择的方案,你可能必须使用锁,原子操作和其他技术来同步访问资源。

锁提供了一种保护代码的方法,一次只能执行一个线程。最常见的锁类型是互斥锁,也称为互斥。当一个线程试图获取另一个线程持有的互斥锁,它将阻塞直到另一个线程释放该锁。一些系统框架提供互斥锁的支持,尽管他们都是基于相同底层技术。此外,Cocoa提供一些变体的互斥锁来支持不同类型的行为,例如递归。关于可用锁类型的更多信息,参见锁(Locks)。

除了锁,系统提供支持条件以确保你应用中任务的正确排序。一个条件作为看门人,阻塞一个给定线程直到条件成立。当这种情况发生时,条件释放线程并允许其继续。POSIX层和基础框架都提供条件的直接支持。(如果你使用操作对象,你可以配置你的操作对象之间依赖关系,来排序执行的任务,这是非常类似于条件提供的行为。)

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

关于可用同步工具的更多信息,参见同步工具(Synchronization Tools)。

线程间通信

尽管好的设计最大限度的减少需要的通信,在某些时候,线程之间的通信变得十分必要。(一个线程主要为你的应用工作,但如果该工作的结果并未使用,那么有什么好处?)线程可能需要处理新的工作请求或报告他们的进展到你应用的主线程。在这些情况下,你需要一种方法从一个线程获取信息到另一个线程。幸运的是,线程间共享进程空间表明你有很多可选方案来进行通信。

线程之间交流的方法有很多,每种方法都有自己的优缺点。配置线程局部存储列表你可以使用OS X最常见的通信机制。(除了消息队列和Cocoa分布式对象,这些技术在iOS中也可用)此表中列出的技术越来越复杂。

表1-3 通信机制

机制

说明

直接传递

Cocoa应用支持其他线程上直接执行选择器。这种能力表明一个线程可以执行其他线程上的方法。因为它们在目标线程的环境中执行,这样消息在该线程上自动被序列化。关于输入源的信息参见Cocoa可执行选择器来源( Cocoa Perform Selector Sources)。

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

两个线程间通信的另一个简单的方法是使用一个全局变量,共享对象或共享内存块。尽管共享变量快速简单,比直接传递要脆弱。共享变量必须被锁或其他同步机制保护以确保你代码的正确性。如果不这样做就有可能导致竞态条件、数据毁坏或者崩溃。

条件

条件是一个同步工具,可以用来控制线程执行一段特定代码。你可以认为条件是一个看门人,只有当条件满足时才允许一个线程执行。关于如何使用条件的更多信息,参见使用条件(Using Conditions)。

运行循环来源

自定义运行循环来源是用来接收线程上特定应用消息。因为它们是事件驱动,当无事可做时,运行循环来源让你的线程自动进入睡眠状态,这样可以改善你线程执行的效率。关于运行循环和运行循环来源的更多信息,参阅运行循环(Run Loops)。

端口和套接字

基于端口的通信是两个线程间通信更复杂的一种方式,但它也是一种非常可靠的技术。更重要的是,端口和套接字可用于外部实体的通信,例如其他进程和服务。为了提高效率,端口使用运行循环来源来实现,所以当端口处没有数据时你的线程休眠。关于运行循环和基于端口输入源的更多信息,参见运行循环( Run Loops)。

消息队列

遗留的多重处理服务定义了一个先进先出(FIFO)队列抽象概念来管理传入和传出的数据。虽然消息队列简单方便,他们不想其他通信技术一样高效。关于如何使用消息队列的更多信息,参见多重处理服务编程指南(Multiprocessing Services Programming Guide)。

Cocoa分布式对象

分布式对象是一种Cocoa技术,该技术提供了一个高层次基于端口通信的实现。尽管线程间通信可以使用这种技术,这样做是非常气馁因为它带来的大量开销。分布式对象更适合与其他进程通信,进程间的开销已经本来就很高。更多信息,参见分布式对象编程主题(Distributed Objects Programming Topics)。

设计技巧

以下部分提供了帮助你实现线程的参考,以确保你代码的正确性。这些参考还建议用自己的线程代码来实现更好的性能。对于任何性能技巧,你应该在更改代码前,更改时和更改后收集相关性能统计。

避免显式的创建线程

手动编写线程创建代码是乏味的并且可能出错,你应该尽量避免。OS XiOS通过其他API为并发提供隐式支持。考虑使用异步APIGCD或操作对象来完成这个工作,而非自己创建一个线程。这些技术在后台完成线程相关的工作,并且保证正确的完成。此外,例如GCD等技术和操作对象相对你自己的代码管理线程更加高效,它们可以基于当前系统的复杂来调整活动线程的数量。关于GCD和操作对象的更多信息,参见并发编程指南(Concurrency Programming Guide)。

保持你的线程合理的忙碌状态

如果你决定手动创建和管理线程,记住线程会消耗宝贵的系统资源。你应该尽你所能确保你分配到线程的任务都相当的长久和有成效的。同时,你不应该害怕终止线程话费大部分的空闲时间。线程使用大量的内存,一些是连接的,所以释放一个空闲线程不仅有助于减少应用内存占用,也释放更多其他系统进程使用的物理内存。

重要:在你开始终止空闲线程之前,你应该记录一组你应用当前性能的基线测量。在尝试更改后,使用额外的侧脸验证该变化实际上是提高性能而不是使性能受损。

避免共享数据结构

避免线程相关资源冲突最简单和最容易的方法是给你项目中每个线程自己所需的数据副本。减少线程间的通信和资源竞争的最好方法是并行代码。

创建多线程应用是困难的。即使你非常小心并且在你代码中所有连接处都锁住共享数据结构,你的代码仍然可能不安全。例如,如果它希望共享的数据结构特定的顺序被修改,你的代码可能会遇到问题。修改代码为基于事务模型来补偿可能随后会使多线程性能优势失效。在第一时间消除资源竞争通常会产生一个简单同时具有优良性能的设计。

线程和用户界面

如果你的应用有一个图形用户界面,建议你在应用的主线程中接收用户相关事件和启动界面更新。这种方法有助于避免处理用户事件与绘制窗口内容的同步问题。一些框架,例如Cocoa,通常要求这种行为,但即便是那些不这样做的框架,保持这些行为在主线程的有点是简化管理用户界面的逻辑。

有些明显的例外,从其他线程执行图像操作更有利。例如,你可以使用次要线程来创建和处理图像和执行其他图像相关的计算。使用次要线程来处理这些操作可以大大的提高性能。如果你不确定一个特定图像操作,计划从你的主线程完成。

关于Cocoa线程安全的更多信息,参见线程安全总结(Thread Safety Summary)。关于在Cocoa中绘图的更多信息,参见Cocoa绘图指南(Cocoa Drawing Guide)。

注意线程退出时的行为

进程一直运行直到所有non-detached线程退出。默认情况下,只有应用的主线程创建non-detached,但你可以创建其他线程。当用户退出应用时,他通常被认为是适当的行为立即终止所有分离线程,因为分离线程的工作被认为是可选的。如果你的应用使用后台线程来保存数据到磁盘或做其他重要的工作,然而,你可能想要创建non-detached线程来防止当应用退出时数据丢失。

创建non-detached 线程(也称为可连接线程)需要额外的工作。因为大多数高级线程技术默认不创建可连接线程,你必须使用POSIX API来创建你的线程。此外,当最终退出时,你必须将代码添加到你应用主线程与non-detached线程结合。关于创建可连接线程的更多信息,参见设置线程的分离状态(Setting the Detached State of a Thread)。

如果你正在编写Cocoa应用,你还可以使用applicationShouldTerminate: 代理方法来推迟应用的终止到晚些时候或完全取消。当推迟终止时,你的应用将需要等待直到关键线程已经完成它们的任务,然后调用 replyToApplicationShouldTerminate:方法。关于这些方法的更多信息,参见NSApplication 类引用(NSApplication Class Reference)。

异常处理

当抛出异常时,异常处理机制依赖于当前调用堆栈来执行任何必要的清理。因为每个线程都有自己的调用堆栈,因此每个线程负责捕捉自己的例外。在次要线程捕捉异常失败那么在主线程也会失败:拥有的进程终止。你不能抛出一个未捕获的异常到不同的线程进行处理。

如果你需要通知另一个线程(主线程等)异常情况给当前线程,你需要捕获异常并简单的发送一个消息给另外的线程表明发生了什么。根据你的模型以及你想做什么,线程捕获异常可以继续处理(如果这是可能的),等待指示或简单的退出。

注意:在Cocoa中,NSException 对象是一个独立的对象,当被捕获时,可以从一个线程传递给另一个线程。

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

干净的终止你的线程

退出线程的最佳方式是让它到达主要入口点程序的末尾。尽管有函数可以立即终止线程,这些函数只是作为最后的手段。在线程达到其自然终点前终止一个线程以阻止线程清理本身。如果线程已分配内存,打开一个文件或获取其他类型的资源,你的代码可能无法回收这些资源,将导致内存泄露或其他潜在的问题。

关于退出线程的正确方法的更多信息,参见终止一个线程(Terminating a Thread)。

库中线程安全

尽管应用开发人员控制应用是否执行多个线程,库开发人员不控制。当开发库时,你必须假定调用应用是多线程或随时可以转成多线程。因此,你应该在关键部分的代码总是使用锁。

对于库开发人员,除了应用是多线程的,否则创建锁是不明智的。如果你在某种情况下需要锁代码,在使用你的库之前创建锁对象,最好是显式的调用来初始化库。尽管你也可以使用静态库初始化函数来创建这样的锁,只有当没有其他方法才试图这样做。初始化函数的执行增加了加载库所需的时间,并且可能影响性能。

注意:永远记住在库中要平衡锁和解锁一个互斥锁的调用。你也应该记住锁库数据结构而不是依赖调用代码来提供一个线程安全的环境。

如果你正在开发一个Cocoa库,你可以注册为一个NSWillBecomeMultiThreadedNotification 观察者,如果当应用变成多线程时你想要被通知。你不应该依赖与接收这个通知,不过,因为它有可能在库代码调用前就被发送。

 

官方原文地址:

https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Multithreading/AboutThreads/AboutThreads.html#//apple_ref/doc/uid/10000057i-CH6-SW2

0 0