iOS多线程编程

废话不多说,直接上干货。先熟悉一下基本知识,然后讲一下常用的两种,NSOperation和GCD。

一、基础概念

进程:

狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。

广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

进程的概念主要有两点:第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。

进程是操作系统中最基本、重要的概念。是多道程序系统出现后,为了刻画系统内部出现的动态情况,描述系统内部各道程序的活动规律引进的一个概念,所有多道程序设计操作系统都建立在进程的基础上。

线程:

    一个CPU一次只能执行一个命令,不能执行某处分开的并列的两个命令,因此通过CPU执行的CPU命令就好比一条无分叉的大道,其执行不对出现分歧 。

    这里所说的“一个CPU执行的CPU命令列为一条无分叉路径”,即为“线程”。

    这种无分叉路径不只1条,存在有多条时即为“多线程”。

    基本上1个CPU核一次能够执行的CPU命令始终为1,那么怎么才能在多条路径中执行CPU命令列呢?

    OS X和iOS的核心XNU内核在发生操作系统事件时(如每隔一定时间,唤起系统调用等情况)会切换执行路径。执行中路径的状态,例如CPU的寄存器等信息保存到各自路径专用的内存块中,从切换目标路径专用的内存块中,复原CPU寄存器等信息,继续执行切换路径的CPU命令列。这被称为“上下文切换”。

    由于使用多线程的程序可以在某个线程和其他线程之间反复多次进行上下文切换,因此看上去就好像1个CPU核能够并列的执行多个线程一样。而且在具有多个CPU核的情况下,就不是“看上去像了”,而是真的提供了多个CPU核并行执行多个线程的技术了。

    这种利用多线程编程的技术就被称为“多线程编程”。

同步:

    就是在发出一个调用时,在没有得到结果之前,该调用就不反回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。

异步:

    而异步则是相反,调用在发出之后,这个调用就直接返回了,所以就没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出之后,被调用者通过“状态”、“通知”、“回调”三种途径通知调用者。

串行:一个线程按顺序执行

并发:由于使用多线程的程序可以在某个线程和其他线程之间反复多次进行上下文切换,因此看上去就好像1个CPU核能够并列的执行多个线程一样。(同时存在)

并行:多个CPU核并行执行多个线程(同时执行)

注意:如果某个系统支持两个或者多个动作(Action)同时存在,那么这个系统就是一个并发系统。如果某个系统支持两个或者多个动作同时执行,那么这个系统就是一个并行系统。并发系统与并行系统这两个定义之间的关键差异在于“存在”这个词。“并行”概念是“并发”概念的一个子集

再简单一点:

并发:交替做不同事的能力

并行:同时做不同事的能力

专业解释:

并发:不同代码块交替执行的性能

并行:不同代码块同时执行的性能

整理一下基本概念:

1 同步异步针对时间,同步会阻塞当前线程,任务完成同步函数才会返回,线程继续执行,异步不会阻塞当前线程,异步函数马上返回,线程继续执行

2 串行,并行针对空间,串行在同一线程顺序执行。并行在不同线程执行

参考资料:如何理解阻塞、非阻塞与同步、异步的区别?

并发与并行的区别?

二、使用GCD进行多线程编程

2.1 什么是GCD:Grand Central Dispatch(GCD)是异步执行任务的技术之一,用我们难以置信的非常简洁的记述方法,实现了极为复杂繁琐的多线程编程。

2.2 GCD的API

  2.2.1 Dispatch Queue:如其名称所示,是执行处理的等待队列。

          开发者要做的只是定义想执行的任务并追加到适当的Dispatch Queue中。

dispatch_async(queue, ^{
        NSLog(@"想执行的任务";
    });

  编程人员在Block语法中记述想执行的处理并将其追加到Dispatch Queue中。Dispatch Queue按照追加顺序(先进先出 FIFO,First-In-First-Out)执行处理。

  DIspatch Queue有两种,一种是Serial Dispatch Queue(串行队列),一种是Concurrent Dispatch Queue(并行队列)。

串行队列(Serial Dispatch Queue):

 // 自定义串行队列,将任务ABC加到串行队列中,顺序执行
    dispatch_queue_t customSerialQueue = dispatch_queue_create("test.wangdachui.MyCustomQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(customSerialQueue, ^{
        NSLog(@"customSerialQueue-A");
    });
    dispatch_async(customSerialQueue, ^{
        NSLog(@"customSerialQueue-B");
    });
    dispatch_async(customSerialQueue, ^{
        NSLog(@"customSerialQueue-C");
    });
    // 由于上面是异步执行操作,所以很难知道下面的打印和上面异步操作中的打印谁先谁后
    NSLog(@"customSerialQueue-D");
    
    //多个串行队列并行执行,系统对于一个serialQueue就只生成并使用一个线程。如果生成2000个serialQueue,那么就生成2000个线程
    dispatch_queue_t customSerialQueue1 = dispatch_queue_create("test.wangdachui.MyCustomQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t customSerialQueue2 = dispatch_queue_create("test.wangdachui.MyCustomQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t customSerialQueue3 = dispatch_queue_create("test.wangdachui.MyCustomQueue", DISPATCH_QUEUE_SERIAL);
    
    dispatch_async(customSerialQueue1, ^{
        NSLog(@"customSerialQueue1-A");
    });
    dispatch_async(customSerialQueue2, ^{
        NSLog(@"customSerialQueue2-B");
    });
    dispatch_async(customSerialQueue3, ^{
        NSLog(@"customSerialQueue3-C");
    });
    //注意:过多使用多线程,就会消耗大量内存问题,引起大量的上下文切换,大幅度降低系统的响应性能

并行队列(Concurrent Dispatch Queue):

iOS和OS X的核心--XNU内核决定应当使用的线程数,并只生成所需的线程执行处理。另外,当处理结束,应当执行的处理数减少时,XNU内核会结束不再需要的线程。XNU内核仅使用Concurrent Dispatch Queue便可以完美地管理并行执行多个处理的线程。

     假设准备4个Concurrent Dispatch Queue 用线程。首先blk0在线程0中开始执行,接着blk1在线程1中、blk2在线程2中、blk3在线程3中开始执行。线程0中blk0执行结束后开始执行blk4,由于线程1中blk1的执行没有结束,因此线程2中blk2执行结束后开始执行blk5,就这样循环往复。

     像这样在Concurrent Dispatch Queue中执行处理时,执行顺序会根据处理内容和系统状态发生改变。

     为了说明线程分配原理,这里假设线程数为4,实测iOS11线程数可达20个,所以想测试的同学,在并发队列中必须追加20个以上的任务

     对于Concurrent Dispatch Queue来说,不管生成多少,由于XNU内核只使用有效管理的线程,因此不会发生串行队列的那些问题(过多使用多线程,降低系统的响应性能)

 // 自定义并行队列,将任务1234567加到串行队列中
    dispatch_queue_t customConcurrentQueue = dispatch_queue_create("test.wangdachui.MyCustomQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(customConcurrentQueue, ^{
        NSLog(@"blk0");
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });
    dispatch_async(customConcurrentQueue, ^{
        NSLog(@"blk1");
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });
    dispatch_async(customConcurrentQueue, ^{
        NSLog(@"blk2");
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });
    dispatch_async(customConcurrentQueue, ^{
        NSLog(@"blk3");
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });
    dispatch_async(customConcurrentQueue, ^{
        NSLog(@"blk4");
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });
    dispatch_async(customConcurrentQueue, ^{
        NSLog(@"blk5");
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });
    dispatch_async(customConcurrentQueue, ^{
        NSLog(@"blk6");
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });

2.2.2 Dispatch Group:在使用Concurrent Dispatch Queue或同时使用多个Dispatch Queue时,想要在Dispatch Queue中的处理全部结束后再执行其他处理。可以使用Dispatch Group。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();
    // 把 queue 加入到 group
    dispatch_group_async(group, queue, ^{
        // 一些异步操作任务
        sleep(2);
        NSLog(@"任务GroupA\n当前线程:%@",[NSThread currentThread]);
    });
    // code 你可以在这里写代码做一些不必等待 group 内任务的操作
    NSLog(@"任务GroupB\n当前线程:%@",[NSThread currentThread]);
    // 当你在 group 的任务没有完成的情况下不能做更多的事时,阻塞当前线程等待 group 完工
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    NSLog(@"任务GroupC\n当前线程:%@",[NSThread currentThread]);
    dispatch_group_notify(group, dispatch_get_main_queue(), ^(){
        // 从主线程上执行 UI 界面更新
        NSLog(@"任务GroupD\n当前线程:%@",[NSThread currentThread]);
    });

2.2.3 dispatch_barrier_async:

在访问数据库或文件时,为了高效地进行访问,读取处理追加到Concurrent Dispatch Queue中,写入处理在任一读取处理没有执行的状态下,追加到Serial Dispatch Queue中即可(在写入处理结束之前,读取处理不可执行)

 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
        NSLog(@"blk0_for_reading");
    });
    dispatch_async(queue, ^{
        NSLog(@"blk1_for_reading");
    });
    dispatch_async(queue, ^{
        NSLog(@"blk2_for_writing");
    });
    dispatch_async(queue, ^{
        NSLog(@"blk3_for_reading");
    });
    dispatch_async(queue, ^{
        NSLog(@"blk4_for_reading");
    });
    /*如上,如果简单地在dispatch_async函数中加入写入处理,那么根据Concurrent Dispatch Queue的性质,就有可能在追加到写入处理前面的处理中读取到与期待不符的数据,还可能因非法访问导致应用程序异常结束。因此我们要使用dispatch_barrier_async函数。dispatch_barrier_async函数会等待追加到Concurrent Dispatch Queue上的并行执行的处理全部结束之后,再将指定的处理追加到该Concurrent Dispatch Queue中。然后在由dispatch_barrier_async函数追加的处理执行完毕后,Concurrent Dispatch Queue才恢复为一般的动作,追加到该Concurrent Dispatch Queue的处理又开始执行。*/
    dispatch_async(queue, ^{
        NSLog(@"blk0_for_reading");
    });
    dispatch_async(queue, ^{
        NSLog(@"blk1_for_reading");
    });
    dispatch_barrier_async(queue, ^{
        NSLog(@"blk2_for_writing");
    });
    dispatch_async(queue, ^{
        NSLog(@"blk3_for_reading");
    });
    dispatch_async(queue, ^{
        NSLog(@"blk4_for_reading");
    });

2.2.4 Dispatch Semaphore:信号量,关于信号量可以看我另外一篇帖子:iOS 信号量

// 创建信号量,并且设置值为10
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(10);
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    for (int i = 0; i < 100; i++)
    {   // 由于是异步执行的,所以每次循环Block里面的dispatch_semaphore_signal根本还没有执行就会执行dispatch_semaphore_wait,从而semaphore-1.当循环10次后,semaphore等于0,则会阻塞线程,直到执行了Block的dispatch_semaphore_signal 才会继续执行
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        dispatch_async(queue, ^{
            NSLog(@"信号量-index=%i",i);
            NSLog(@"信号量当前线程:%@",[NSThread currentThread]);
            sleep(2);
            // 每次发送信号则semaphore会+1,
            dispatch_semaphore_signal(semaphore);
        });
    }

2.2.5 dispatch_apply:该函数按指定的次数将指定的Block追加到指定的Queue中,并等待全部处理执行结束

 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_apply(10, queue, ^(size_t index) {
        NSLog(@"%zu",index);
    });
    NSLog(@"done");
    /*打印结果
     2017-09-20 10:49:29.760594+0800 Multithreading[11622:459025] 2
     2017-09-20 10:49:29.760594+0800 Multithreading[11622:458947] 3
     2017-09-20 10:49:29.760594+0800 Multithreading[11622:459027] 0
     2017-09-20 10:49:29.760594+0800 Multithreading[11622:459026] 1
     2017-09-20 10:49:29.760609+0800 Multithreading[11622:459077] 4
     2017-09-20 10:49:29.760634+0800 Multithreading[11622:459078] 5
     2017-09-20 10:49:29.760650+0800 Multithreading[11622:459079] 6
     2017-09-20 10:49:29.760653+0800 Multithreading[11622:459080] 7
     2017-09-20 10:49:29.760737+0800 Multithreading[11622:458947] 8
     2017-09-20 10:49:29.760738+0800 Multithreading[11622:459025] 9
     2017-09-20 10:49:29.761195+0800 Multithreading[11622:458947] done
     */

2.3 关于同步异步,串行并行的思考,看看打印结果,你就能悟真谛了。看的时候可以把测试代码拷贝到自己的项目中,自己思考一下,再回头看看打印结果。这样效果更好。大家不要嫌我写的烦,要好好听课。

//关于同步异步,串行,并行的思考
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_queue_t serialQueue = dispatch_queue_create("test.Lision.MyCustomQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t concurrentQueue = dispatch_queue_create("test.Lision.MyCustomQueue", DISPATCH_QUEUE_CONCURRENT);
    
    [self dispatchAsyncWith:mainQueue];
    /* 打印结果
     2017-09-20 19:25:40.815678+0800 Multithreading[34153:2558588] 任务C
     当前线程:<NSThread: 0x604000077440>{number = 1, name = main}
     2017-09-20 19:25:42.843920+0800 Multithreading[34153:2558588] 任务A
     当前线程:<NSThread: 0x604000077440>{number = 1, name = main}
     2017-09-20 19:25:44.845176+0800 Multithreading[34153:2558588] 任务B
     当前线程:<NSThread: 0x604000077440>{number = 1, name = main}
     */
    
    //[self dispatchAsyncWith:globalQueue];
    /* 打印结果:A/B的顺序随机
     2017-09-20 19:28:26.395395+0800 Multithreading[34347:2580292] 任务C
     当前线程:<NSThread: 0x60400006dc80>{number = 1, name = main}
     2017-09-20 19:28:28.397384+0800 Multithreading[34347:2580443] 任务B
     当前线程:<NSThread: 0x604000275ac0>{number = 3, name = (null)}
     2017-09-20 19:28:28.397384+0800 Multithreading[34347:2580446] 任务A
     当前线程:<NSThread: 0x60c00007c680>{number = 4, name = (null)}
     */
    
    //[self dispatchAsyncWith:serialQueue];
    /* 打印结果:串行队列生成一个新线程,AB顺序执行
     2017-09-20 19:29:45.542392+0800 Multithreading[34394:2585219] 任务C
     当前线程:<NSThread: 0x60000007f880>{number = 1, name = main}
     2017-09-20 19:29:47.547666+0800 Multithreading[34394:2585681] 任务A
     当前线程:<NSThread: 0x604000277540>{number = 3, name = (null)}
     2017-09-20 19:29:49.550736+0800 Multithreading[34394:2585681] 任务B
     当前线程:<NSThread: 0x604000277540>{number = 3, name = (null)}
     */
    
    //[self dispatchAsyncWith:concurrentQueue];
    /* 打印结果 A/B的顺序随机,AB并行执行,生成两个线程,最多生成几个线程由系统决定
     2017-09-20 19:30:56.998360+0800 Multithreading[34446:2593417] 任务C
     当前线程:<NSThread: 0x60400006ba40>{number = 1, name = main}
     2017-09-20 19:30:59.001208+0800 Multithreading[34446:2593777] 任务B
     当前线程:<NSThread: 0x608000275ac0>{number = 3, name = (null)}
     2017-09-20 19:30:59.001206+0800 Multithreading[34446:2593775] 任务A
     当前线程:<NSThread: 0x60400007ae40>{number = 4, name = (null)}
     */

    //[self dispatchSyncWith:mainQueue];//死锁
    /*
     死锁原因:我们要在主线程同步执行任务A,但是同步执行任务A也算一个任务,我们称呼它为W。mainQueue是顺序执行的,当前正在执行的任务是W,W的内容是要执行A,所以把A加到mainQueue的尾部等待执行。A要执行,必须等W完成,W要完成,必须要执行A,相互等待,进入死锁。
     所以,同步的时候,不能将任务添加到当前线程的串行Queue中
     */
    //[self dispatchSyncWith:globalQueue];
    /* 打印结果 同步阻塞,顺序执行ABC
     2017-09-20 19:59:25.961727+0800 Multithreading[34773:2655128] 任务A
     当前线程:<NSThread: 0x60c000261bc0>{number = 1, name = main}
     2017-09-20 19:59:27.962571+0800 Multithreading[34773:2655128] 任务B
     当前线程:<NSThread: 0x60c000261bc0>{number = 1, name = main}
     2017-09-20 19:59:27.962824+0800 Multithreading[34773:2655128] 任务C
     当前线程:<NSThread: 0x60c000261bc0>{number = 1, name = main}
     */
//    [self dispatchSyncWith:serialQueue];
    /*
     自己猜
     */
//    [self dispatchSyncWith:concurrentQueue];
    /*
     自己猜
     */
}

//异步调用各种Queue
- (void)dispatchAsyncWith:(dispatch_queue_t)queue {
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"任务A\n当前线程:%@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"任务B\n当前线程:%@",[NSThread currentThread]);
    });
    NSLog(@"任务C\n当前线程:%@",[NSThread currentThread]);
}

//同步调用各种Queue
- (void)dispatchSyncWith:(dispatch_queue_t)queue {
    dispatch_sync(queue, ^{
        sleep(2);
        NSLog(@"任务A\n当前线程:%@",[NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        sleep(2);
        NSLog(@"任务B\n当前线程:%@",[NSThread currentThread]);
    });
    NSLog(@"任务C\n当前线程:%@",[NSThread currentThread]);
}

留了两个没有给结果,很简单的,经过上面的学习,结果是不是很简单。帖子准备了好几天,查阅了很多资料。关于使用NSOperation进行多线程编程,看我这篇帖子:iOS多线程--NSOperation

demo下载:https://github.com/wangdachui/multithreading.git

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏木宛城主

ASP.NET MVC 随想录—— 使用ASP.NET Identity实现基于声明的授权,高级篇

在这篇文章中,我将继续ASP.NET Identity 之旅,这也是ASP.NET Identity 三部曲的最后一篇。在本文中,将为大家介绍ASP.NET ...

2308
来自专栏xx_Cc的学习总结专栏

iOS-多线程详解

3049
来自专栏恰同学骚年

自己动手写工具:百度图片批量下载器

开篇:在某些场景下,我们想要对百度图片搜出来的东东进行保存,但是一个一个得下载保存不仅耗时而且费劲,有木有一种方法能够简化我们的工作量呢,让我们在离线模式下也能...

3471
来自专栏王大锤

iOS多线程编程

3726
来自专栏移动开发的那些事儿

BlockCanary源码解析

如上代码中的loop()方法是Looper中的,我们的目的是监测主线程的卡顿问题,因为UI更新界面都是在主线程中进行的,所以在主线程中做耗时操作可能会造成界面卡...

1352
来自专栏菩提树下的杨过

Oracle中使用Entity Framework 6.x Code-First方式开发

去年写过一篇EF的简单学习笔记,当时EF还不支持Oracle的Code-First开发模式,今天无意又看了下Oracle官网,发现EF6.X已经支持了,并且给出...

2485
来自专栏iOS技术杂谈

iOS多线程——你要知道的NSOperation都在这里你要知道的iOS多线程NSThread、GCD、NSOperation、RunLoop都在这里

你要知道的iOS多线程NSThread、GCD、NSOperation、RunLoop都在这里 转载请注明出处 https://cloud.tencent.co...

3795
来自专栏Rgc

requests模块报错:Use body.encode('utf-8') if you want to send it encoded in UTF-8.

在做 企业向微信用户个人付款  功能时,调用第三方sdk,在 进行 requests 的post请求时,

2451
来自专栏Android研究院

彻底理解OkHttp - OkHttp 源码解析及OkHttp的设计思想

在OKhttp 源码解析之前,我们必须先要了解http的相关基础知识,任何的网络请求都离不开http。

4902
来自专栏前端黑板报

HTTP2基础教程-读书笔记(四)

? 记录一下HTTP/2的底层原理,帮助理解协议实现细节。 连接 每个端点都需要发送一个连接作为最终确认使用的协议,并建立http/2连接的初始设置。客户端和...

3496

扫码关注云+社区

领取腾讯云代金券