iOS 多线程技术由浅深入(学习笔记)

原创 2015年07月09日 22:47:06

参考:http://my.oschina.net/aofe/blog/270093

参考:https://en.wikipedia.org/wiki/Grand_Central_Dispatch

参考:http://www.cocoachina.com/ios/20140515/8433.html

参考:http://www.cocoachina.com/ios/20140428/8248.html

参考:http://www.cnblogs.com/wendingding/p/3806821.html

iOS 线程的使用目标:如何控制好各个线程的执行顺序、处理好资源竞争问题。

问题关键:同步会在同一个线程中操作,如果有阻塞则要暂停,异步不会等待,如果有新任务,会在新的线程中操作。一般是不会出现死锁的现象。

本篇文章为个人笔记,搜集了相当一部分资料只是为了搞懂心中的疑问,经过一段时间的测试和查找资料,最终使心中的部分疑问得到解决,其中包含了许多个人小结,或许有些是因为理解不深而思考偏了方向,希望大家指正!

所面临的问题: 进程与线程的区别,并行与串行的区别,并行与并发的区别,GCD怎样使用,NSOperation/NSOperationQueue理解及使用方法,NSThread的使用方法,三种方式各自特点,GCD中并行与串行同步与异步的特点及注意点,线程是如何阻塞的(详细可参见参考网址),怎样避免阻塞,返回主线程的方式,常用的UI更新框架。

当然也有一些深入的问题没有考虑到:如线程资源的抢夺,锁的添加等,后续完善了。

原理定义(认为需要了解的可以看下)

iOS中三种多线程技术:NSThread,NSOperation/NSOperationQueue,GCD

  • 并行:无论微观还是宏观,二者都是一起执行的。
  • 并发:在微观上不是同时执行的,只是把时间分成若干段,使多个进程快速交替执行,从宏观看,好像这些进程都在执行。
  • 进程:正在进行中的程序被称为进程,负责程序运行的内存分配; 
每一个进程都有自己独立的虚拟内存空间。
  • 线程:(主线程最大占1M的栈区空间,每条子线程最大占512K的栈区空间)线程是进程中一个独立的执行路径(控制单元);
一个进程中至少包含一条线程,即主线程; 可以将耗时的执行路径)(如网络请求)放在其他线程中执行; 
线程不能被杀掉,但是可以暂停/休眠一条线程。
创建线程的目的:开启一条新的执行路径,运行指定的代码,与主线程中的代码实现同时运行。

多任务调度系统:

每个应用程序由操作系统分配的短暂的时间片(Timeslice)轮流使用CPU,由于CPU对每个时间片的处理速度非常快,因此,用户看起来这些任务好像是同时执行的。

并发:指两个或多个任务在同一时间间隔内发生。但是,在任意一个时间点上,CPU只会处理一个任务。

  • 优势:发挥多核处理器优势,将不同线程任务分配给不同的处理器,真正进入“并行运算”状态、将耗时任务分配出去,由主线程负责更新界面,提升用户体验、硬件处理器的数量增加,程序会运行更快,而程序无需做任何调整。
  • 弊端:新建线程会消耗内存空间和CPU时间,线程太多会降低系统的运行性能。

三种多线程技术的特点(有点乱,后续再整理)

NSThread:
> 创建线程方便,但管理麻烦(需管理整个生命周期),可使用[NSThread currentThread]跟踪任务所在线程,适用于这三种技术,但要控制线程执行顺序并不容易,另外在这个过程中如果打印线程会发现循环几次就创建了几个线程,这是实际开发不得不考虑的问题,因为每个线程的创建也是相当占用系统开销的。

NSOperation/NSOperationQueue:
> 是使用GCD实现的、面向对像的多线程技术、提供一些在GCD中不容易实现的特性,如:限制最大并发数量,操作之间的依赖关系。

GCD - - - - - Grand Central Dispatch:极好的中枢调度
> 基于C的API、用Block定义任务,使用起来灵活便捷、提供了更多的控制能力以及操作队列中所不能使用的底层函数。所有函数都是以dispatch(分派/调度)开头的


GCD的基本思想:将操作S放到队列S中去执行   队列特点:先进先出
是基于C语言开发的一套多线程开发机制,也是目前苹果官方推荐的多线程开发方法。GCD抽象层次最高,当然用起来也最简单,只是它基于C语言开发,并不像NSOperation是面向对象的开发,而完全是面向过程的。它与C#中的异步调用基本是一样的。这个机制相比较于前面两种多线程开发方式最显著的优点就是它对于多核运算更加有效。
GCD中也有一个类似于NSOperationQueue的队列,GCD统一管理整个队列中的任务。但是GCD中的队列分为并行和串行队列两类:

队列:dispatch_queue_t

  • 串行队列:队列中的任务只会顺序执行;
  • 并发队列:队列中的任务通常会并发执行。
其实在GCD中还有一个特殊队列就是主队列,用来执行主线程上的操作任务(从后面可以看出其实在NSOperation中也有一个主队列)

操作:
  • dispatch_async异步操作,会并发执行,无法确定任务的执行顺序;  只有异步操作才会新建线程
  • dispatch_sync同步操作,会依次顺序执行,能够决定任务的执行顺序。
 队列不是线程,也不表示对应的CPU,队列就是负责调度的,多线程技术的目的,就是为了在一个CPU上实现快速的切换!


串行队列顺序执行,并行队列无法确定顺序、UI界面的更新最好采用同步方法,其他操作采用异步方法

在串行队列中:

  • 同步操作不会新建线程,操作顺序执行(没用!);
  • 异步操作会新建线程,操作顺序执行(非常有用!)(应用场景:既不影响主线程,又需要顺序执行的操作)

在并行队列中:【并行队列还是并发队列,各有说辞,不过一般是相对纡CPU处理核心来说的,核心足够多实现并行也是可能的,如果不行的话,则是并发,只不过是让人感觉同时进行的。对于多核心CPU来说,其核心与核心之间是并行计算,但对于每一个核心内的线程运算则是属于并发】
  • 同步操作不会新建线程,操作顺序执行;  【既然不会创建线程那就是在主线程中操作的了】
  • 异步操作会新建多个线程,操作无序执行(有用,容易出错),队列前如果有其他任务,会等待前面任务完成之后再执行,场景:既不影响主线程又不需要顺序执行的操作。
全局队列:
全局队列是系统的,直接拿来(GET),与并行对立类似,但在调试时,无法确认操作所在队列。  在实际开发中我们通常不会创建一个并发队列而是使用dispatch_get_global_queue()方法取得一个全局的并发队列(当然如果有多个并发队列可以使用前者创建)。

主队列:每一个应用程序都对应唯一一个主队列,直接GET即可,在多线程开发中,使用主队列更新UI;

注意:主队列中的操作都应该在主线程上顺序执行,不存在异步的概念。(只是不会执行到,应该不会是死锁吧【死锁】:两个或两个以上的进程在执行过程中,由于竞争资源或者由于 彼此通信造成的一种阻塞现象,若无外力作用,它们将无法推进下去。此时称系统处于死锁状态或系统产生了死锁)

同步操作dispatch_sync的应用场景

阻塞并行队列的执行,要求某一操作执行后再进行后续操作,如用户登录。
确保代码块之外的局部变量确实被修改。
[NSThread sleepForTimeInterval:2.0f]通常在多线程调试中用于模拟耗时操作,在发布的应用程序中不要使用此方法!

无论什么队列和什么任务,线程的创建和回收都不需要程序员参与。线程的创建回收工作是由队列负责的。

GCD优点:

  • 开发者不用直接跟线程打交道,只需要向队列中添加代码块即可。
  •  GCD在后端管理着一个线程池,GCD不仅决定着代码将在哪个线程被执行,它还根据可用的系统资源对这些线程进行管理,从而让开发者从线程管理的工作中解放出来; 通过集中的管理线程,缓解大量线程被创建的问题。
  •  使用GCD,开发者可以将工作考虑为一个队列,而不是一堆线程,这种并行的抽象模型更容易掌握和使用。
提示:串行和并行队列,自定义队列非常强大,建议在开发中使用。 不建议使用不同优先级的队列,因为如果设计不当,可能会出现优先级反转,即低优先级的操作阻塞高优先级的操作。

ios6.0之前,如果自定义队列,则需要手动释放
dispatch_release(队列名);

常用框架:(虽然dispatch queue是引用计数的对象,但是以上两个都是全局的队列,不用retain或release)
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 无法保证执行顺序
        
        // 耗时操作
        dispatch_async(dispatch_get_main_queue(), ^{
            //更新界面
        });
    });
dispatch_async(serialQueue, ^{ // 可以保证顺序执行,会开一个新线程,不至于阻塞主线程


            dispatch_queue_t mainQueue= dispatch_get_main_queue();
    dispatch_sync(mainQueue, ^{
// 更新界面
    });    

    });

理解歧义:在主线程中添加更新UI的操作,但由于main队列是串行队列,而且不存在同步异步之分,所以在使用主队列时不用担心是用同步还是异步操作,都只是一样的。但其外围的操作则要求必须为异步的,因为其只有异步操作才会创建新线程不然就是在其主线程中进行的操作。

说队列:每一个队列都有使用一个线程,其实这个线程就是主线程。我们所说的使用新线程就是在此基础上来说的,如果是创建了新线程则是与主线程并列执行了。 直接调用在主线程中操作是没有任何意义的。因为所有操作本身就是在主线程中的。主线程中所有操作都应该在主线程上顺序执行,不存在异步的概念如果把主线程中的操作看作是一个大的Block,那么除非主线程被用户杀掉,否则永远不会结束,所以主队列中添加了同步操作永远不会执行。

dispatch_sync添加任务到一个队列并等待直到任务完成。dispatch_async做类似的事情,但不同之处是它不会等待任务的完成,而是立即继续“调用线程”的其他任务。

解决:外围没有其他队列时,不能直接对主队列添加同步操作,会阻塞主线程,但可以对其添加异步操作,虽然为异步操作,但其也是在主线程中执行。【类似于,主队列中的所有操作都会在主线程】

总结:无论主队列使用同步还是异步,外围用串行还是并行,外围一定要用的是异步【保障所有操作可执行】。

GCD执行任务的方法并非只有简单的同步调用方法和异步调用方法,还有其他一些常用方法:

dispatch_apply():重复执行某个任务,但是注意这个方法没有办法异步执行(为了不阻塞线程可以使用dispatch_async()包装一下再执行)。
dispatch_once():单次执行一个任务,此方法中的任务只会执行一次,重复调用也没办法重复执行(单例模式中常用此方法)。
dispatch_time():延迟一定的时间后执行。
dispatch_barrier_async():使用此方法创建的任务首先会查看队列中有没有别的任务要执行,如果有,则会等待已有任务执行完毕再执行;同时在此方法后添加的任务必须等待此方法中任务执行后才能执行。(利用这个方法可以控制执行顺序,例如前面先加载最后一张图片的需求就可以先使用这个方法将最后一张图片加载的操作添加到队列,然后调用dispatch_async()添加其他图片加载任务)
dispatch_group_async():实现对任务分组管理,如果一组任务全部完成可以通过dispatch_group_notify()方法获得完成通知(需要定义dispatch_group_t作为分组标识)。

全局并发队列 手动创建串行队列 主队列

  • 同步(sync) 没有开启新线程、串行执行任务没有开启新线程,串行执行任务没有开启新线程,串行执行任务
  • 异步(async) 有开启新线程、并发执行任务有开启新线程、串行执行任务没有开启新线程、串行执行任务
// 在主队列添加同步或异步方法没执行起来不会有什么区别,因为不是自己创建的串行队列,不会开启新线程,都只是顺序执行

NSOperation/NSOperationQueue

简介:NSOperationQueue是由GCD提供的队列模型的Cocoa抽象,是一套Objective-C的API;
GCD提供了更加底层的控制,而NSOperationQueue(操作队列)则在GCD之上实现了一些方便的功能,这些功能对开发者而言通常是最好最安全的选择。

使用NSOperation和NSOperationQueue进行多线程开发类似于C#中的线程池,只要将一个NSOperation(实际开发中需要使用其子类NSInvocationOperation、NSBlockOperation)放到NSOperationQueue这个队列中线程就会依次启动。NSOperationQueue负责管理、执行所有的NSOperation,在这个过程中可以更加容易的管理线程总数和控制线程之间的依赖关系。
NSOperation有两个常用子类用于创建线程操作:NSInvocationOperation和NSBlockOperation,两种方式本质没有区别,但是后者使用Block形式进行代码组织,使用相对方便。

对比NSThread加载图片可以发现核心代码简化了不少,这里主要着重强调几点:

  •  使用NSBlockOperation方法,所有的操作不必单独定义方法,同时解决了只能传递一个参数的问题。
  •  调用主线程队列的addOperationWithBlock:方法进行UI更新,不用再定义一个参数实体(之前必须定义一个image对象【索引和数据】)
  • 使用NSOperation进行多线程开发可以设置最大并发线程,有效的对线程进行了控制(最大并发为几也就是基本几个一起加载)
  • 使用NSThread很难控制线程的执行顺序,但是使用NSOperation就容易多了,每个NSOperation可以设置依赖线程。假设操作A依赖于操作B,线程操作队列在启动线程时就会首先执行B操作,然后执行A.【注意:不可设置循环依赖,否则不会被执行】

队列及操作:
NSOperationQueue有两种不同类型的队列:主队列和自定义队列。
主队列运行在主线程上,自定义队列在后台执行。

队列处理的任务是NSOperation的子类:NSInvocationOperation和NSBlockOperation.

NSOperation的基本使用步骤:
定义操作队列————>定义操作————>将操作添加到队列。
提示:
一旦将操作添加到队列,操作就会立即被调度执行。

创建一个操作队列:

myQueue = [[NSOperationQueue alloc] init];
定义操作,并将其添加到队列中
   NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(operationAction:) object:@(5)];
    [myQueue addOperation:op];
定义操作方法:
- (void) operationAction:(id)obj
{
    NSLog(@"当前的线程名称:%@-------obj:%@",[NSThread currentThread],obj);
}

NSBlockOperation(块操作)
定义块,并将其添加到队列

    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        [self operationAction:@"Block Operation"];
    }];
    [myQueue addOperation:op];

// 设置同时并发的线程数量:   
 [myQueue setMaxConcurrentOperationCount:2];
 [op1 addDependency:op]; // 添加依赖关系,先执行op再执行op1,记着是在两个操作添加到队列前


NSOperation小结:
从本质上看,操作队列的性能会比GCD略低,不过,操作队列是并发编程的首选工具。

NSThread的多线程方法

> 开启后台执行任务的方法:

- (void)performSelectorInBackground:(SEL)@selector withObject:(id)arg
> 在后台线程中通知主线程执行任务的方法:
- (void)performSelectorOnMainThread:(SEL)@selector withObject:(id)arg
> 获取线程信息
[NSThread currentThread];
> 线程休眠
[NSThread sleepForTimeInterval:2.0f];
特点:使用简单,轻量级、不能控制线程的数量及执行顺序

注意:NSThread的多线程技术不会自动使用@autoreleasepool。
在使用NSObject或NSThread的多线程技术时,如果涉及到对象分配,需要手动添加@autoreleasepool.

创建NSThread的三种方式:

    [NSThread detachNewThreadSelector:@selector(myThreadMethod:) toTarget:self withObject:nil]; //调用立即创建一个新线程执行操作
    NSThread* myThread = [[NSThread alloc] initWithTarget:self selector:@selector(myThreadMethod:)  object:nil];
    [myThread start];

NSThread初始化之后,新的线程并没有执行,而是调用 start 时才会创建线程执行。这种方法相对上面的方法更加灵活,在启动新的线程之前,对线程进行相应的操作,比如设置优先级,加锁。

扩展-NSObject分类扩展方法
为了简化多线程开发,苹果官方对NSObject进行分类扩展(本质还是创建NSThread),对于简单的多线程操作可以直接使用这些扩展方法。
[myObj performSelectorInBackground:@selector(myThreadMainMethod) withObject:nil];  

 利用 NSObject 的类方法 performSelectorInBackground:withObject: 来创建一个线程:

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thread withObject:(id)arg waitUntilDone:(BOOL)wait:在指定的线程上执行一个方法,需要用户创建一个线程对象。
以上都可以在新的线程中调用performSelectorOnMainThread: withObject:waitUntilDone:更新UI,因为大部分时候子线程不能直接更新UI【具体何时并未深入研究】。

线程的状态分为isExecuting(正在执行)、isFinished(已经完成)、isCancelled(已经取消)三种。其中取消状态程序可以干预设置,只要调用线程的cancel方法即可。但是需要注意在主线程中仅仅能设置线程状态,并不能真正停止当前线程,如果要终止线程必须在线程中调用exist方法,这是一个静态方法,调用该方法可以退出当前线程。

阻塞原理(解释)
- (void)viewDidLoad 
{ 
  [super viewDidLoad]; 
 
  dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ 
 
      NSLog(@"First Log"); 
 
  }); 
 
  NSLog(@"Second Log"); 
} 

1. 主队列一路按顺序执行任务——接着是一个实例化 UIViewController 的任务,其中包含了 viewDidLoad 。
2. viewDidLoad 在主线程执行。
3. 主线程目前在 viewDidLoad 内,正要到达 dispatch_sync 。
4. dispatch_sync Block 被添加到一个全局队列中,将在稍后执行。进程将在主线程挂起直到该 Block 完成。同时,全局队列并发处理任务;要记得 Block 在全局队列中将按照 FIFO 顺序出列,但可以并发执行。
5. 全局队列处理 dispatch_sync Block 加入之前已经出现在队列中的任务。
6. 终于,轮到 dispatch_sync Block 。
7. 这个 Block 完成,因此主线程上的任务可以恢复。
8. viewDidLoad 方法完成,主队列继续处理其他任务。


dispatch_sync 添加任务到一个队列并等待直到任务完成。dispatch_async 做类似的事情,但不同之处是它不会等待任务的完成,而是立即继续“调用线程”的其它任务。

- (void)viewDidLoad 
{ 
  [super viewDidLoad]; 
 
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ 
 
      NSLog(@"First Log"); 
 
  }); 
 
  NSLog(@"Second Log"); 
} 
1.主队列一路按顺序执行任务——接着是一个实例化 UIViewController 的任务,其中包含了 viewDidLoad 。
2. viewDidLoad 在主线程执行。
3.主线程目前在 viewDidLoad 内,正要到达 dispatch_async 。
4.dispatch_async Block 被添加到一个全局队列中,将在稍后执行。
5.viewDidLoad 在添加 dispatch_async 到全局队列后继续进行,主线程把注意力转向剩下的任务。同时,全局队列并发地处理它未完成地任务。记住 Block 在全局队列中将按照 FIFO 顺序出列,但可以并发执行。
6.添加到 dispatch_async 的代码块开始执行。
7.dispatch_async Block 完成,两个 NSLog 语句将它们的输出放在控制台上。

具体参见:http://www.cocoachina.com/ios/20140428/8248.html  讲解超好!

【总结】同步加同步操作会阻塞原理:
主细线程挂起,等待另一个队列先去执行,然后才可以执行,但此时执行到block时,发现其中要求进入到主线程中,执行另一个block,而其中要求,执行完成了这个block才可以使得这个队列中得到执行,由于没有开新的进程。 主线程中有两个操作,其中第一个操作在等待另一个队列先执行,而另一个队列中又在等待主线程执行完一个顺序才可以执行,而主线程此时已经处于等待状态了。所以两个相互等待就处于死锁状态。

【整理】回到主线程的方式

1. 
dispatch_async(dispatch_get_main_queue(), ^{
// 回到主线程,执⾏UI刷新操作
});

2. 

performSelectorOnMainThread: withObject:waitUntilDone:

3.

[[NSOperationQueue mainQueue] addOperationWithBlock:^{ // 放到主队列
        //回到主线程要做的操作
    }];
版权声明:本文为学习讨论使用,转载标注出处!

相关文章推荐

JAVA多线程技术学习笔记——volatile关键字解析

Java并发编程:volatile关键字解析    volatile这个关键字可能很多朋友都听说过,或许也都用过。在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结...

ios学习--多线程技术

进程与线程概念 --一个运行的程序就是一个进程或者叫做一个任务 --一个进程至少包含一个线程,线程是程序的执行流iOS程序启动时,在创建一个进程的同时, 会开始运行一个线程,该线程被称为主线程 ...

iOS- 多线程技术的概述及优点

//判断101-200之间有多少个素数,并输出所有素数。 void fun_sushu(); void fun_sushu(){ int i,j; for (i=101; i...

iOS三种多线程技术NSThread,NSOperation/NSOperationQueue,GCD-Grand Central Dispatch

一.NSThread 建立一个线程方便,但是要使用NSThread管理多个线程非常困难,只要记住以下几个方法. (1) [NSThread currentThread] //跟踪任务所在线程,适用于这...

iOS的三种多线程技术

1.iOS的三种多线程技术1.NSThread 每个NSThread对象对应一个线程,量级较轻(真正的多线程) 2.以下两点是苹果专门开发的“并发”技术,使得程序员可以不再去关心线程的具体使用问题 ...

iOS多线程技术

原文章点此多线程技术我们为何需要多线程呢?多线程其实是为了实现并发执行,而且线程是并发执行多个代码路径的多种技术之中比较轻量级的一种(对应较重的实现是多进程)。在单核 CPU 时代,支持多线程的操作系...

IOS多线程技术详解

并发所描述的概念就是同时运行多个任务。这些任务可能是以在单核 CPU 上分时(时间共享)的形式同时运行,也可能是在多核 CPU 上以真正的并行方式来运行。OS X 和 iOS 提供了几种不同的 API...

IOS 多线程技术

IOS有三种多线程编程技术,分别是:     1)NSThread     2)Cocoa NSOperation     3)GCD(Grand Central Dispatch)     这三种编...

多线程技术方案(ios)

多线程技术方案 一、多线程简介 1、多线程的由来 一个进程(进程)在执行一个线程(线程中有很多函数或方法(后面简称Function))的时候,其中有一个Function执行的时候需要消耗...

iOS 多线程技术1

iOS 多线程技术1iOS 有三种多线程编程技术: NSThread NSOperation GCD 它们的抽象程度由低到高,越高的使用起来越简单.NSThread显示调用 NSthread 类 类方...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:iOS 多线程技术由浅深入(学习笔记)
举报原因:
原因补充:

(最多只允许输入30个字)