谈谈iOS面试常提及到的线程间的通信

谈谈iOS面试常提及到的线程间的通信_第1张图片
程序猿思考问题时的样子

我们看图片只是乐呵一下,程序猿思考问题差不多就是这个样子,
今天同事在线程通信这一块有点疑问,我们下面来分析一下,系统都提供给我们那些,其实我们都知道,但是很少去关注这些API,也正是这些API,来回在APP中去执行各种不同的线程和队列


一、常见的线程间通信 GCD

我们先来看一个系统的例子:

    //开启一个全局队列的子线程
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
            //1. 开始请求数据
            //...
            // 2. 数据请求完毕
            //我们知道UI的更新必须在主线程操作,所以我们要从子线程回调到主线程
        dispatch_async(dispatch_get_main_queue(), ^{

                //我已经回到主线程更新
        });

    });

如上所述,我们下面进行一个测试:

     //自定义队列,开启新的子线程
    dispatch_queue_t    custom_queue = dispatch_queue_create("concurrentqueue", DISPATCH_QUEUE_CONCURRENT);
    for (int i=0; i<10; i++) {
        dispatch_async(custom_queue, ^{
            NSLog(@"## 并行队列 %d ##",i);
                //数据更新完毕回调主线程 线程之间的通信
            dispatch_async(dispatch_get_main_queue(), ^{

                NSLog(@"### 我在主线程 通信 ##");

            });


        });
    }

  • 线程中延迟调用某个方法
    //线程延迟调用 通信
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

        NSLog(@"## 在主线程延迟5秒调用 ##");
    });
  • 线程休眠几秒的方法
    sleep(6); : 这里是休眠6秒

  • 常用的线程通信方法有以下几种:(GCD)

    1. 需要更新UI操作的时候使用下面这个GCD的block方法
    //回到主线程更新UI操作
    dispatch_async(dispatch_get_main_queue(), ^{
           //数据执行完毕回调到主线程操作UI更新数据
    });
    
    1. 有时候省去麻烦,我们使用系统的全局队列:一般用这个处理遍历大数据查询操作
    DISPATCH_QUEUE_PRIORITY_HIGH  全局队列高优先级
    DISPATCH_QUEUE_PRIORITY_LOW 全局队列低优先级
    DISPATCH_QUEUE_PRIORITY_BACKGROUND  全局队里后台执行队列
    // 全局并发队列执行处理大量逻辑时使用   
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    
    });
    
    1. 当在开发中遇到一些数据需要单线程访问的时候,我们可以采取同步线程的做法,来保证数据的正常执行
    //当我们需要执行一些数据安全操作写入的时候,需要同步操作,后面所有的任务要等待当前线程的执行
    dispatch_sync(dispatch_get_global_queue(0, 0), ^{
           //同步线程操作可以保证数据的安全完整性
    });
    

二、了解一下NSObject中的对象线程访问模式

  1. 我们介绍简单的perfermselecter选择器实现线程通信

        //数据请求完毕回调到主线程,更新UI资源信息  waitUntilDone    设置YES ,代表等待当前线程执行完毕
    [self performSelectorOnMainThread:@selector(dothing:) withObject:@[@"1"] waitUntilDone:YES];
    
  2. 如果需要执行到后台线程,则直接前往后台线程执行即可

      //将当前的逻辑转到后台线程去执行
    [self performSelectorInBackground:@selector(dothing:) withObject:@[@"2"]];
    
  3. 自己定义线程,将当前数据转移到指定的线程内去通信操作

     //支持自定义线程通信执行相应的操作
    NSThread * thread = [[NSThread alloc]init];
    [thread start];
        //当我们需要在特定的线程内去执行某一些数据的时候,我们需要指定某一个线程操作
    [self performSelector:@selector(dothing:) onThread:thread withObject:nil waitUntilDone:YES];
    //支持自定义线程通信执行相应的操作
    NSThread * thread = [[NSThread alloc]initWithTarget:self selector:@selector(testThread) object:nil];
    [thread start];
       //当我们需要在特定的线程内去执行某一些数据的时候,我们需要指定某一个线程操作
    [self performSelector:@selector(dothing:) onThread:thread withObject:nil waitUntilDone:YES];

  • 上面几种方法就是我们常用的对象调用常用的线程间通信,我们可以根据不同的情况,采取不同的线程执行状态.
增加一个特殊的线程常驻RunLoop 的做法
  • 需求: 我们经常要用到常驻线程来请求数据,但是请求有时候在线程会退出,这个时候我们需要用一下方法:
//有时候需要线程单独跑一个RunLoop 来保证我们的请求对象存在,不至于会被系统释放掉

  NSThread * runLoopThread = [[NSThread alloc]initWithTarget:self selector:@selector(entryThreadPoint) object:nil];
  [runLoopThread start];
  [self performSelector:@selector(handleMutiData) onThread:runLoopThread withObject:nil waitUntilDone:YES];


//给线程增加一个run loop 来保持对象的引用
- (void)entryThreadPoint{

  @autoreleasepool {
      [NSThread currentThread].name = @"自定义线程名字";
      NSRunLoop * runloop = [NSRunLoop currentRunLoop];
      [runloop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
      [runloop run];
  }
}
- (void)handleMutiData{
  NSLog(@"## 我是跑在runloop的线程 ##");
}

最后测试截图如下,看下我们的线程是不是已经加入runloop 了

这个就是最后的执行了

补充1:
  • 有个地方需要补充以下: 请先看完下面的之后,再回头看这个地方。
    需求: 如何动态的调整队列的优先级,执行层级,我们这里多加一个函数
  //  设置目标队列  queue 既是target , object 是我们将要指定的dispatch 对象, 第一个参数要用自定义的队列,不能使用系统的全局队列,系统的队列层级我们无法修改
void dispatch_set_target_queue(dispatch_object_t object, dispatch_queue_t queue);

这个就是我们指定一个队列执行在目标队列上,和目标队列一个层级
我们看下代码如何调用:

 dispatch_queue_t queue1 = dispatch_queue_create("queue1", NULL);
 dispatch_queue_t queue2 = dispatch_queue_create("queue2", NULL);
 dispatch_set_target_queue(queue1, queue2);
 dispatch_async(queue2, ^{
     NSLog(@"## 我是queue2 ----0###");
 });
 dispatch_async(queue1, ^{
     NSLog(@"## 我是queue1 ------1##");
 });
 dispatch_async(queue2, ^{
     NSLog(@"## 我是queue2  ----2 ##");
 });
 dispatch_async(queue1, ^{
     NSLog(@"## 我是queue1 ------3 ##");
 });
 dispatch_async(queue1, ^{
     NSLog(@"## 我是queue1 ------4 ##");
 });
 dispatch_async(queue2, ^{
     NSLog(@"## 我是queue2    ---5 ##");
 });

  • 我们可以先猜想一下输出结果,会按照我们想要的结果显示么
    答案:
2016-06-01 11:53:36.333 RunTimeModify[41214:6432344] ## 我是queue2 ----0###
2016-06-01 11:53:36.339 RunTimeModify[41214:6432344] ## 我是queue1 ------1##
2016-06-01 11:53:36.341 RunTimeModify[41214:6432344] ## 我是queue1 ------3 ##
2016-06-01 11:53:36.343 RunTimeModify[41214:6432344] ## 我是queue1 ------4 ##
2016-06-01 11:53:36.344 RunTimeModify[41214:6432344] ## 我是queue2  ----2 ##
2016-06-01 11:53:36.344 RunTimeModify[41214:6432344] ## 我是queue2    ---5 ##

相信大家发现问题了。
我们调整一下Target 的顺序:

 dispatch_set_target_queue(queue2, queue1);

我们再来看一下输出:

2016-06-01 11:57:30.483 RunTimeModify[41297:6435226] ## 我是queue2 ----0###
2016-06-01 11:57:30.486 RunTimeModify[41297:6435226] ## 我是queue2  ----2 ##
2016-06-01 11:57:30.486 RunTimeModify[41297:6435226] ## 我是queue2    ---5 ##
2016-06-01 11:57:30.487 RunTimeModify[41297:6435226] ## 我是queue1 ------1##
2016-06-01 11:57:30.487 RunTimeModify[41297:6435226] ## 我是queue1 ------3 ##
2016-06-01 11:57:30.487 RunTimeModify[41297:6435226] ## 我是queue1 ------4 ##

以上只是简单的使用,我们不做什么的测试,了解就行:
以上这个地方,大家还会有个小疑问:连接在这里,stack 上找的How does dispatch_set_target_queue work?
大家看到这个或许就明白怎么工作的了

补充2:

谢谢 @激动的马 问题 ,还要增加一种通信方法

  • NSOperationQueue  系统自带的更新UI的方法 ,这种我不经常用,推荐GCD
    
    //注意点 : 设计到UI的更新在主队列
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        NSLog(@"###  我是在主队列执行的block  ####");
    }];
//这个只是用作判断当前线程是否是主线程
    if ([[NSThread currentThread] isMainThread]) {
        NSLog(@"## 我是主线程 ##");
    }else{
        NSLog(@"## 我是子线程 ###");
    }
补充三、
  • GCD中阻塞线程之用法
//主要涉及到下面这个API,该API是保证自己的block会等待当前队列之前的block全部执行完毕,在执行自己的block,最后在执行后面的任务
void
dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);

我们用代码简单的看一下调用顺序:

   dispatch_async(dispatch_get_global_queue(0, 0), ^{
       NSLog(@"## 全局队列异步执行 第1个任务 ##");
   });
   dispatch_async(dispatch_get_global_queue(0, 0), ^{
       NSLog(@"## 全局队列异步执行 第2个任务 ##");
   });
   dispatch_async(dispatch_get_global_queue(0, 0), ^{
       NSLog(@"## 全局队列异步执行 第4个任务 ##");
   });
   dispatch_barrier_async(dispatch_get_global_queue(0, 0), ^{
       NSLog(@"## 异步阻塞当前线程,会等待当前线程执行完毕  ###");
   });
   dispatch_async(dispatch_get_global_queue(0, 0), ^{
       NSLog(@"### 我在全局队列线程内执行  第3个任务###");
   });
* 我们看下控制台输出:
     2016-06-01 14:04:13.973 RunTimeModify[42377:6489044] ## 全局队列异步执行 第1个任务 ##
     2016-06-01 14:04:13.973 RunTimeModify[42377:6489051] ## 全局队列异步执行 第2个任务 ##
     2016-06-01 14:04:13.973 RunTimeModify[42377:6489063] ## 全局队列异步执行 第4个任务 ##
     2016-06-01 14:04:13.973 RunTimeModify[42377:6489064] ## 异步阻塞当前线程,会等待当前线程执行    完毕  ###
     2016-06-01 14:04:13.973 RunTimeModify[42377:6489065] ### 我在全局队列线程内执行  第3个任务###

大概可以看一下阻塞这个API是怎么用的,可以在项目总很好的处理我们的问题

  • 我们在继续看下面的循环执行的block任务:
for (int i=0; i<5; i++) {
        dispatch_barrier_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"## 我是阻塞线程执行顺序 %d ###",i);
        });

    }
    for (int i=0; i<5; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"### 我是全部异步队列任务 : %d  ###",i);
        });
    }

我们看下控制台打印输出的日志系统:

2016-06-01 14:07:42.534 RunTimeModify[42437:6491161] ## 我是阻塞线程执行顺序 1 ###
2016-06-01 14:07:42.534 RunTimeModify[42437:6491166] ## 我是阻塞线程执行顺序 0 ###
2016-06-01 14:07:42.534 RunTimeModify[42437:6491168] ## 我是阻塞线程执行顺序 2 ###
2016-06-01 14:07:42.534 RunTimeModify[42437:6491179] ## 我是阻塞线程执行顺序 3 ###
2016-06-01 14:07:42.534 RunTimeModify[42437:6491180] ## 我是阻塞线程执行顺序 4 ###
2016-06-01 14:07:42.535 RunTimeModify[42437:6491181] ### 我是全部异步队列任务 : 0  ###
2016-06-01 14:07:42.535 RunTimeModify[42437:6491161] ### 我是全部异步队列任务 : 1  ###
2016-06-01 14:07:42.536 RunTimeModify[42437:6491166] ### 我是全部异步队列任务 : 2  ###
2016-06-01 14:07:42.536 RunTimeModify[42437:6491182] ### 我是全部异步队列任务 : 3  ###
2016-06-01 14:07:42.537 RunTimeModify[42437:6491168] ### 我是全部异步队列任务 : 4  ###

虽然都是异步任务,但是执行的很有规则。我们可以去尝试测试一下反过来的结果

  • 可以测试下面这个执行顺序:
//测试一下,是不是阻塞API之前的任务必须要执行完毕,看下有什么问题
 for (int i=0; i<5; i++) {

       dispatch_barrier_async(dispatch_get_global_queue(0, 0), ^{
           NSLog(@"## 我是阻塞线程执行顺序 %d ###",i);
       });
       dispatch_async(dispatch_get_global_queue(0, 0), ^{
           NSLog(@"### 我是全部异步队列任务 : %d  ###",i);
       });
   }
  • 经常遇到的问题:
    1. 网络请求到数据,但是UI没有及时更新。
    2. 在子线程的block内执行更新UI操作,控制台一般都会有错误线程输出的
    3. 如果只是逻辑的处理,无关UI,处理数据量较大,可以选择子线程处理。

本文暂时先介绍到这里,如果还想了解用信号量控制线程可以参考另外一篇博客:关于dispatch 下一遍补充了源的问题
iOS 关于dispatch_semaphore_t 和 dispatch_group_t 的简单实用,用于多网络异步回调通知

你可能感兴趣的:(谈谈iOS面试常提及到的线程间的通信)