多线程
为什么要使用多线程
- 好事的操作使用线程, 提高应用程序响应.
- 并行操作时使用线程,如C/S架构的服务器端并发线程响应用户的请求。
- 多CPU系统中,使用线程提高CPU利用率
- 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。
- 使用多线程的理由之一是和进程相比,它是一种非常
花销小,切换快
,更”节俭”的多任务操作方式。在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种”昂贵”的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间
,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间
。 - 使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。
除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:
提高应用程序响应
。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。多CPU系统更加有效
。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。改善程序结构
一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。
理解的线程
- 要讲解线程,不得不说一下进程,
进程是应用程序的执行实例
,每个进程是由私有的虚拟地址空间、代码、数据和其它系统资源组成。进程在运行时创建的资源随着进程的终止而死亡。线程的基本思想很简单,它是一个独立的执行流,是进程内部的一个独立的执行单元,相当于一个子程序,它对应Visual C++中的CwinThread类的对象。单独一个执行程序运行时,缺省的运行包含的一个主线程,主线程以函数地址的形式,如main或WinMain函数,提供程序的启动点,当主线程终止时,进程也随之终止,但根据需要,应用程序又可以分解成许多独立执行的线程,每个线程并行的运行在同一进程中
。 一个进程中的所有线程都在该进程的虚拟地址空间中,使用该进程的全局变量和系统资源。
操作系统给每个线程分配不同的CPU时间片,在某一个时刻,CPU只执行一个时间片内的线程,多个时间片中的相应线程在CPU内轮流执行,由于每个时间片时间很短,所以对用户来说,仿佛各个线程在计算机中是并行处理的。操作系统是根据线程的优先级来安排CPU的时间,优先级高的线程优先运行,优先级低的线程则继续等待。- 线程被分为两种:
用户界面线程
和工作线程(又称为后台线程)
。用户界面线程通常用来处理用户的输入并响应各种事件和消息,其实,应用程序的主执行线程CWinAPP对象就是一个用户界面线程,当应用程序启动时自动创建和启动,同样它的终止也意味着该程序的结束,进城终止。工作者线程用来执行程序的后台处理任务,比如计算、调度、对串口的读写操作等,它和用户界面线程的区别是它不用从CwinThread类派生来创建,对它来说最重要的是如何实现工作线程任务的运行控制函数。工作线程和用户界面线程启动时要调用同一个函数的不同版本;最后需要读者明白的是,一个进程中的所有线程共享它们父进程的变量,但同时每个线程可以拥有自己的变量。
多线程和异步操作
- 多线程和异步操作两者都可以达到避免调用线程阻塞的目的,从而提高软件的可响应性。甚至有些时候我们就认为多线程和异步操作是等同的概念。但是,多线程和异步操作还是有一些区别的。而这些区别造成了使用多线程和异步操作的时机的区别。
异步操作的本质
所有的程序最终都会由计算机硬件来执行,所以为了更好的理解异步操作的本质,我们有必要了解一下它的硬件基础。 熟悉电脑硬件的朋友肯定对DMA这个词不陌生,硬盘、光驱的技术规格中都有明确DMA的模式指标,其实网卡、声卡、显卡也是有DMA功能的。DMA就是直接内存访问的意思,也就是说,拥有DMA功能的硬件在和内存进行数据交换的时候可以不消耗CPU资源。只要CPU在发起数据传输时发送一个指令,硬件就开始自己和内存交换数据,在传输完成之后硬件会触发一个中断来通知操作完成。这些无须消耗CPU时间的I/O操作正是异步操作的硬件基础。所以即使在DOS这样的单进程(而且无线程概念)系统中也同样可以发起异步的DMA操作。
线程的本质
线程不是一个计算机硬件的功能,而是操作系统提供的一种逻辑功能
线程本质上是进程中一段并发运行的代码,所以线程需要操作系统投入CPU资源来运行和调度
异步操作的优缺点
因为异步操作无须额外的线程负担,并且使用回调的方式进行处理,在设计良好的情况下,处理函数可以不必使用共享变量(即使无法完全不用,最起码可以减少共享变量的数量),减少了死锁的可能。当然异步操作也并非完美无暇。编写异步操作的复杂程度较高,程序主要使用回调方式进行处理,与普通人的思维方式有些初入,而且难以调试。
多线程的优缺点
多线程的优点很明显,线程中的处理程序依然是顺序执行,符合普通人的思维习惯,所以编程简单。但是多线程的缺点也同样明显,线程的使用(滥用)会给系统带来上下文切换的额外负担。并且线程间的共享变量可能造成死锁的出现。
使用范围 - 在了解了线程与异步操作各自的优缺点之后,我们可以来探讨一下线程和异步的合理用途。我认为:
当需要执行I/O操作时,使用异步操作比使用线程+同步I/O操作更合适
。I/O操作不仅包括了直接的文件、网络的读写,还包括数据库操作、Web Service、HttpRequest以及.net Remoting等跨进程的调用。 - 而
线程的适用范围则是那种需要长时间CPU运算的场合
,例如耗时较长的图形处理和算法执行。但是往往由于使用线程编程的简单和符合习惯,所以很多朋友往往会使用线程来执行耗时较长的I/O操作。这样在只有少数几个并发操作的时候还无伤大雅,如果需要处理大量的并发操作时就不合适了。
iOS 中的多线程
- 多线程是一个比较轻量级的方法来实现单个应用程序内多个代码执行路径,在系统级别内,程序并排执行,程序分配到每个程序的执行时间是基于该程序的所需时间和其他程序的所需时间来决定的。
- 然而,在每个程序内部,存在一个或者多个执行线程,它同时或在一个几乎同时发生的方式里执行不同的任务。
概要提示:
iPhone中的线程应用并不是无节制的,官方给出的资料显示,iPhone OS下的主线程的堆栈大小是1M,第二个线程开始就是512KB,并且该值不能通过编译器开关或线程API函数来更改,只有主线程有直接修改UI的能力
线程概述: - 有些程序是一条直线,起点到终点——如简单的hello world,运行打印完,它的生命周期便结束了,像是昙花一现。有些程序是一个圆,不断循环直到将它切断——如操作系统,一直运行直到你关机。
- 一个运行着的程序就是一个进程或者叫做一个任务,一个进程至少包含一个线程,线程就是程序的执行流。Mac和IOS中的程序启动,创建好一个进程的同时,一个线程便开始运作,这个线程叫做主线程。主线成在程序中的位置和其他线程不同,它是其他线程最终的父线程,且所有的界面的显示操作即AppKit或UIKit的操作必须在主线程进行。
- 系统中每一个进程都有自己独立的虚拟内存空间,而同一个进程中的多个线程则公用进程的内存空间。
每创建一个新的进成,都需要一些内存(如每个线程有自己的stack空间)和消耗一定的CPU时间。 - 当多个进成对同一个资源出现争夺的时候需要注意线程安全问题
多线程的优势:
- 充分发挥多核处理器优势,将不同线程任务分配给不同的处理器,真正进入”并行运算”状态;
- 将耗时的任务分配到其他线程执行,由主线程负责统一更新界面会使应用程序更加流畅,用户体验更好;
- 当硬件处理器的数量增加,程序会运行更快,而程序无需做任何调整.
弊端
新建线程会消耗内存空间和CPU时间,线程太多会降低系统的运行性能.
iOS的三种多线程
- NSThread:
- 使用NSThread对象建立一个线程非常方便;
- 但是!要使用NSThread管理多个线程非常困难,不推荐使用;
- 技巧!使用[NSThread currentThread]跟踪任务所在线程,适用于这三种技术.
- NSOperation/NSOperationQueue:
- 是使用GCD实现的一套Objective-C的API;
- 是面向对象的多线程技术;
- 提供了一些在GCD中不容易实现的特性,如:限制最大并发数量,操作之间的依赖关系.
- GCD—Grand Central Dispatch:
- 是基于C语言的底层API;
- 用Block定义任务,使用起来非常灵活便捷;
- 提供了更多的控制能力以及操作队列中所不能使用的底层函数.
NSThread
NSThree是官方推荐的线程处理方式,它在处理机制上,需要开发者负责手动管理Thread的生命周期,包括子线程与主线程之间的同步等。线程共享同一应用程序的部分内存空间,它们拥有对数据相同的访问权限。你得协调多个线程 对同一数据的访问,一般做法是在访问之前加锁,这会导致一定的性能开销。
使用步骤
- 声明一个NSCondition同步锁;
- 声明若干个NSThread子线程;
- 指定NSThread子线程的目标指行方法(可以在构造函数中指定);
- 设置子线程的名称;
- star启动子线程。
其中,子线程的执行方法一般只需要一个共同方法即可(可以通过线程名,分辨当前的执行线程)。下面通过代码演示NSThread类的使用。假设我们需要下载网络图片,在非异步形式的情况下,IOS界面必须等到图片下载完毕后,UI才会反应。这时利用多线程就可以达到异步下载的效果。1
2
3
4
5
6
7#import <UIKit/UIKit.h>
@interface ViewController1 : UIViewController{
NSCondition *_condition;
NSThread *_thread_1;
NSThread *_thread_2;
}
@end
1
|
@interface ViewController1 ()
|
NSOperationQueue
如果需要让线程同时并行运行多个,可以将线程加入队列(Queue)中
,NSOperationQueue类就是一个线程队列管理类
,他提供了线程并行、队列的管理。可以认为NSOperationQueue就是一个线程管理器,通过addOperations方法,我们可以一次性把多个(数组形式)线程添加到队列中。同时,NSOperationQueue允许通过setMaxConcurrentOperationCount方法设置队列的并行(同一时间)运行数量。
使用步骤
- 声明一个NSOperationQueue对象;
- 声明若干个NSInvocationOperation子线程对象,并指定回调方法;
- 将NSInvocationOperation子线程添加到数组;
- 把数组赋给NSOperationQueue类中的addOperations方法;
- 实现回调方法;
- 在回调方法中实现performSelectorOnMainThread方法,更新主线程上的界面UI元素。
下面,使用NSOperationQueue类,实现前面NSThread类相同的功能(即下载图片)。代码如下。
1
|
#import "ViewController2.h"
|
说到这里, 就不得不提一下这个 performSelector 了,么我们简单的介绍一下他的几种用法
performSelector
API是这么解释的 Sends a specified message to the receiver and returns the result of the message.
- (id)performSelector:(SEL)aSelector
API 解释:A selector identifying the message to send. If aSelector is NULL, an NSInvalidArgumentException is raised.
- (id)performSelector:(SEL)aSelector withObject:(id)anObject
API 解释:Sends a message to the receiver with an object as the argument.
- (id)performSelector:(SEL)aSelector withObject:(id)anObject withObject:(id)anotherObject
API 解释:Sends a message to the receiver with two objects as arguments
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay
API 解释:Invokes a method of the receiver on the current thread using the default mode after a delay.
常用的延迟执行的方法
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay
API 解释:Invokes a method of the receiver on the current thread using the default mode after a delay.
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes
API 解释:Invokes a method of the receiver on the current thread using the specified modes after a delay.
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg
API 解释:Invokes a method of the receiver on a new background thread.
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait
API 解释:Invokes a method of the receiver on the main thread using the default mode.
GCD(Grand Central Dispatch)
这里转载于Dreamingwish
- Grand Central Dispatch或者GCD,是一套低层API,提供了一种新的方法来进行并发程序编写。从基本功能上讲,GCD有点像NSOperationQueue,他们都允许程序将任务切分为多个单一任务然后提交至工作队列来并发地或者串行地执行。GCD比之NSOpertionQueue更底层更高效,并且它不是Cocoa框架的一部分。
- 除了代码的平行执行能力,GCD还提供高度集成的事件控制系统。可以设置句柄来响应文件描述符、mach ports(Mach port 用于 OS X上的进程间通讯)、进程、计时器、信号、用户生成事件。这些句柄通过GCD来并发执行。
- GCD的API很大程度上基于block,当然,GCD也可以脱离block来使用,比如使用传统c机制提供函数指针和上下文指针。实践证明,当配合block使用时,GCD非常简单易用且能发挥其最大能力。
为何使用 GCD
- 易用: GCD比之thread跟简单易用。由于GCD基于work unit而非像thread那样基于运算,所以GCD可以控制诸如等待任务结束、监视文件描述符、周期执行代码以及工作挂起等任务。基于block的血统导致它能极为简单得在不同代码作用域之间传递上下文。
- 效率: GCD被实现得如此轻量和优雅,使得它在很多地方比之专门创建消耗资源的线程更实用且快速。这关系到易用性:导致GCD易用的原因有一部分在于你可以不用担心太多的效率问题而仅仅使用它就行了。
- 性能: GCD自动根据系统负载来增减线程数量,这就减少了上下文切换以及增加了计算效率。
GCD中有三种队列类型:
- The main queue: 与主线程功能相同。实际上,提交至main queue的任务会在主线程中执行。main queue可以调用dispatch_get_main_queue()来获得。因为main queue是与主线程相关的,所以这是一个串行队列。
- Global queues: 全局队列是并发队列,并由整个进程共享。进程中存在三个全局队列:高、中(默认)、低三个优先级队列。可以调用dispatch_get_global_queue函数传入优先级来访问队列。
- 用户队列: 用户队列 (GCD并不这样称呼这种队列, 但是没有一个特定的名字来形容这种队列,所以我们称其为用户队列) 是用函数 dispatch_queue_create 创建的队列. 这些队列是串行的。正因为如此,它们可以用来完成同步机制, 有点像传统线程中的mutex。
下面转载于唐巧的技术博客
系统提供的dispatch方法
为了方便地使用GCD,苹果提供了一些方法方便我们将block放在主线程 或 后台线程执行,或者延后执行。使用的例子如下:
1
|
/ 后台执行:
|
dispatch_queue_t 也可以自己定义,如要要自定义queue,可以用dispatch_queue_create方法,示例如下:
1
|
dispatch_queue_t urls_queue = dispatch_queue_create("blog.devtang.com", NULL);
|
另外,GCD还有一些高级用法,例如让后台2个线程并行执行,然后等2个线程都结束后,再汇总执行结果。这个可以用dispatch_group
, dispatch_group_async
和 dispatch_group_notify
来实现,示例如下:
1
|
dispatch_group_t group = dispatch_group_create();
|
修改block之外的变量
默认情况下,在程序块中访问的外部变量是复制过去的,即写操作不对原变量生效。但是你可以加上 __block来让其写操作生效,示例代码如下:
1
|
__block int a = 0;
|
后台执行
使用block的另一个用处是可以让程序在后台较长久的运行。在以前,当app被按home键退出后,app仅有最多5秒钟的时候做一些保存或清理资源的工作。但是应用可以调用UIApplication的beginBackgroundTaskWithExpirationHandler
方法,让app最多有10分钟的时间在后台长久运行。这个时间可以用来做清理本地缓存,发送统计数据等工作。
让程序在后台长久运行的示例代码如下:
1
|
// AppDelegate.h文件
|
这里感谢Dreamingwish , 唐巧的技术博客
_performSelector: 和dispatchtime的不同
- iOS中timer相关的延时调用,常见的有NSObject中的performSelector:withObject:afterDelay:这个方法在调用的时候会设置当前runloop中timer,还有一种延时,直接使用NSTimer来配置任务。
-
这两种方式都一个共同的前提,就是当前线程里面需要有一个运行的runloop并且这个runloop里面有一个timer。
-
我们知道:只有主线程会在创建的时候默认自动运行一个runloop,并且有timer,普通的子线程是没有这些的。这样就带来一个问题了,有些时候我们并不确定我们的模块是不是会异步调用到,而我们在写这样的延时调用的时候一般都不会去检查运行时的环境,这样在子线程中被调用的时候,我们的代码中的延时调用的代码就会一直等待timer的调度,但是实际上在子线程中又没有这样的timer,这样我们的代码就永远不会被调到。
- 在有多线程操作的环境中,这样performSelector的延时调用,其实是缺乏安全性的。而且如果做延时,延迟的时间因为ronloop的原因延时时间可能不受控制,我们可以用另一套方案来解决这个问题,就是使用GCD中的dispatch_after来实现单次的延时调用