[iOS] GCD是神马-队列与并发

2019-08-11  本文已影响0人  木小易Ying

iOS多线程的处理方式主要有四种:pthread / NSThread / GCD / NSOperation,其中用起来最方便也是最常用的大概就是GCD啦,超级强大的简直崇拜脸~
P.S. 小白这周debug要哭所以今天只写基本概念,下周讨论各种dispatch方法哈~

GCD: Grand Central Dispatch
这个名字英文超赞中文大概就是大型调度中心吧,和它的含义也很类似,你给它一个任务,它来进行调度执行。

我们最常遇到的dispatch大概就是往主线程抛任务了,比如在非主线程的时候往主线程post Notification,以免在接收notification的代码对UI进行了操作导致crash;或将与控件尺寸相关的任务抛到主线程在UI绘制后执行等。


往主线程抛任务.png

创建一个任务并且抛给GCD的主要元素分别是同步异步和队列

(1) 同步异步

同步(dispatch_sync)是在任务执行完才会继续执行后面的内容
异步(dispatch_async)是不用等抛出去的任务执行完就执行后面的代码

NSLog(@"同步任务开始");
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
  NSLog(@"同步任务进行中"); 
});
NSLog(@"同步任务结束"); //等待上面的任务做完才会执行
    
    
NSLog(@"异步任务开始");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
  NSLog(@"异步任务进行中");
});
NSLog(@"异步任务结束");  //不等任务做完就执行

输出:
同步任务开始
同步任务进行中
同步任务结束

异步任务开始
异步任务结束
异步任务进行中

(2) 队列

队列主要分两大类——串行队列和并行队列
串行队列:顺序执行放进来的任务,执行完一个再执行下一个(不一定是只有一个线程,但一个队列内一定是顺序执行)
并行队列:可以同时执行多个任务(可能会开启多个线程)

串行队列
并行队列
※那么要如何得到队列呢?

(i) 获取主队列,串行 -> dispatch_get_main_queue()
(ii) 获取全局并发队列 -> dispatch_get_global_queue()
我们经常使用dispatch_get_global_queue(0, 0)来获取全局queue,那么这俩参数是啥呢?第一个就是优先级,而第二个flag其实现在木有用,但是官方说传0(传1返回nil,即不会执行任务但不crash;传2是隐藏操作,可以在当前没有可用线程时不等待单开线程)。

#define DISPATCH_QUEUE_PRIORITY_HIGH 2
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
global queue参数.png

在任务加入时间相近的时候,优先级高的任务队列会优先执行

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
    NSLog(@"4");
    NSLog(@"current thread: %@", [NSThread currentThread]);
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
    NSLog(@"3");
    NSLog(@"current thread: %@", [NSThread currentThread]);
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"2");
    NSLog(@"current thread: %@", [NSThread currentThread]);
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    NSLog(@"1");
    NSLog(@"current thread: %@", [NSThread currentThread]);
});

输出:
1
2
current thread: <NSThread: 0x60000362ca80>{number = 4, name = (null)}
current thread: <NSThread: 0x6000036dc440>{number = 5, name = (null)}
3
current thread: <NSThread: 0x60000362ca80>{number = 4, name = (null)}
4
current thread: <NSThread: 0x6000036dc440>{number = 5, name = (null)}

可以看到thread还是在复用的,只是优先级高的先执行罢了。

用不同优先级创建出的队列是不一样的哦,高优先级的队列会分到更多的时间片,被执行的概率会更高,也就更容易早一点被执行。

global queue优先级.png

(iii) 创建串行/并行队列 -> dispatch_queue_create()

dispatch_queue_t serialQueue = dispatch_queue_create("serial_queue", DISPATCH_QUEUE_SERIAL);

自己创建queue只要给出一个label&是串行/并行就可以啦(DISPATCH_QUEUE_SERIAL / DISPATCH_QUEUE_CONCURRENT)。

注意给出的label并不是queue的唯一标识符,不给都可以,只是一个和queue绑定的字符串,即使我们给出相同的label,返回的仍旧是不同的queue,这个label主要是让我们给自己一个分辨是哪个queue的方式,如果想作为标识符,需要人为确保名称唯一。

dispatch_queue_t serialQueue = dispatch_queue_create("serial_queue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t serialQueue2 = dispatch_queue_create("serial_queue", DISPATCH_QUEUE_SERIAL);
    
NSLog(@"serialQueue地址为:%p", serialQueue);
NSLog(@"serialQueue2地址为:%p", serialQueue2);

输出:
serialQueue地址为:0x600000ed3100
serialQueue2地址为:0x600000ed3180

ARC已经可以自己管理队列的生命周期了,如果block都执行完了(block其实持有了queue的引用),并且没有其他引用指向queue了,它就会自己销毁掉,不需要我们手动调用dispathc_release了哦。


(3) 排列组合

这里来尝试一下各种可能性~

① 异步+主队列
for (NSInteger i = 0; i < 4; i++) {
  dispatch_async(dispatch_get_main_queue(), ^{
      NSLog(@"current thread: %@", [NSThread currentThread]);
      NSLog(@"current task: %ld", (long)i);
  });
}

输出:
current thread: <NSThread: 0x600000828b80>{number = 1, name = main}
current task: 0
current thread: <NSThread: 0x600000828b80>{number = 1, name = main}
current task: 1
current thread: <NSThread: 0x600000828b80>{number = 1, name = main}
current task: 2
current thread: <NSThread: 0x600000828b80>{number = 1, name = main}
current task: 3

话说这个NSThread的number什么呢?
根据https://stackoverflow.com/questions/15558411/nsthread-number-on-ios的解释,其实他就是一个线程的序号没多大意义,正常都不会用到的

队列的规则都是FIFO(first in first out,先入先出),所以如果往主队列连续抛四个异步任务,因为主队列只有一个主线程,所以会按照抛入的顺序执行,并且在一个执行完毕后再执行下一个。

② 异步+全局并发队列
for (NSInteger i = 0; i < 4; i++) {
  dispatch_async(dispatch_get_global_queue(0, 0), ^{
      NSLog(@"current thread: %@", [NSThread currentThread]);
      [NSThread sleepForTimeInterval:1];
      NSLog(@"current task: %ld", (long)i);
  });
}

输出:
current thread: <NSThread: 0x60000270f680>{number = 3, name = (null)}
current thread: <NSThread: 0x600002798480>{number = 5, name = (null)}
current thread: <NSThread: 0x60000278ae00>{number = 7, name = (null)}
current thread: <NSThread: 0x60000278e340>{number = 6, name = (null)}
current task: 1
current task: 2
current task: 3
current task: 0

全局并发队列可能会用多个线程执行任务,可以看到四个任务使用了4个不同的线程,对于每个线程而言,抛给它的任务其实是串行的,但是因为有多个线程,所以给global queue的任务是并行的。

队列不是FIFO么,为什么task 0不是第一个执行的?
如果有线程空闲了,会自己找队列要任务,那么task 0会被第一个分配给线程0x60000278e340,但是线程什么时候可以执行任务要看CPU分配的时间片,虽然他空闲但是不一定现在有拿到时间片,毕竟线程的并发其实都是交替执行。所以对于并发队列,第一个被抛进去不代表会最先被执行,具体什么时候执行要看thread的状况。

③ 异步+创建串行队列
dispatch_queue_t serialQueue = dispatch_queue_create("serial_queue", DISPATCH_QUEUE_SERIAL);
for (NSInteger i = 0; i < 4; i++) {
  dispatch_async(serialQueue, ^{
      NSLog(@"current thread: %@", [NSThread currentThread]);
      NSLog(@"current task: %ld", (long)i);
  });
}

输出:
current thread: <NSThread: 0x600000ed0040>{number = 5, name = (null)}
current task: 0
current thread: <NSThread: 0x600000ed0040>{number = 5, name = (null)}
current task: 1
current thread: <NSThread: 0x600000ed0040>{number = 5, name = (null)}
current task: 2
current thread: <NSThread: 0x600000ed0040>{number = 5, name = (null)}
current task: 3

和①异步+主队列类似,创建一个串行队列也是会顺序执行抛入的任务,区别只是它并不会在主线程执行这些任务。

④ 异步+创建并行队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
for (NSInteger i = 0; i < 4; i++) {
  dispatch_async(concurrentQueue, ^{
      NSLog(@"current thread: %@", [NSThread currentThread]);
      NSLog(@"current task: %ld", (long)i);
  });
}

输出:
current thread: <NSThread: 0x6000023472c0>{number = 3, name = (null)}
current task: 1
current thread: <NSThread: 0x60000239d0c0>{number = 5, name = (null)}
current task: 3
current thread: <NSThread: 0x6000023472c0>{number = 3, name = (null)}
current task: 2
current thread: <NSThread: 0x60000234da80>{number = 4, name = (null)}
current task: 0

和②异步+全局队列类似,创建并行队列会使用多个线程,并且task执行顺序并不是抛入的顺序。使用现有的thread还是开启几个新thread会由GCD来决定的哦。

⑤ 同步+主队列

如果在主线程运行同步+主队列会造成死锁crash哈,因为主线程在等block运行完,而block在等主线程有空闲,于是两个人互相等待就造成了死锁。 (这里的解释其实是有问题的,下一个例子会详细讲死锁怎么产生)

//会crash哦
//主线程等待block结束后继续运行
dispatch_sync(dispatch_get_main_queue(), ^{
    //抛给主线程的任务,等待主线程有空闲的时候运行
    NSLog(@"current thread: %@", [NSThread currentThread]);
});

如果想解除crash,可以dispatch_async给一个global queue,在异步任务里面dispatch_sync给主队列。

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"current thread: %@", [NSThread currentThread]);
    });
});

但其实自己给自己抛同步任务完全木有必要哦-。-

⑥ 同步+全局并发队列
for (NSInteger i = 0; i < 4; i++) {
  dispatch_sync(dispatch_get_global_queue(0, 0), ^{
      NSLog(@"current thread: %@", [NSThread currentThread]);
      NSLog(@"is main thread: %@", [NSThread currentThread] == [NSThread mainThread] ? @"yes" : @"no");
      [NSThread sleepForTimeInterval:1];
      NSLog(@"current task: %ld", (long)i);
  });
}

输出:
current thread: <NSThread: 0x600001316940>{number = 1, name = main}
is main thread: yes
current task: 0
current thread: <NSThread: 0x600001316940>{number = 1, name = main}
is main thread: yes
current task: 1
current thread: <NSThread: 0x600001316940>{number = 1, name = main}
is main thread: yes
current task: 2
current thread: <NSThread: 0x600001316940>{number = 1, name = main}
is main thread: yes
current task: 3

global queue是并发队列,但是如果是同步任务,任务会依次在global queue里的主线程执行,不会用多个线程,因为分配给了一个线程所以就是顺序的啦,不会像异步那种顺序错乱。

dispatch_sync给global queue实际在主线程执行,那么为什么不会死锁呢?
其实造成死锁的不是主线程在等待主线程执行结束,而是主队列在等待主队列,如果不是同一个队列是OK的

※但不是所有global queue + sync任务都在主线程执行哦~

dispatch_queue_t serialQueue = dispatch_queue_create("serial_queue", DISPATCH_QUEUE_SERIAL);

dispatch_async(serialQueue, ^{
  NSLog(@"serialQueue thread:%@", [NSThread currentThread]);
  dispatch_sync(dispatch_get_global_queue(0, 0), ^{
    NSLog(@"serialQueue task thread:%@", [NSThread currentThread]);
  });
});

输出:
serialQueue thread:<NSThread: 0x600001849b80>{number = 5, name = (null)}
serialQueue task thread:<NSThread: 0x600001849b80>{number = 5, name = (null)}

如果我们新建一个串行队列,里面的任务并不在主线程执行,然后抛一个同步任务,发现它运行的线程和当前线程是一致的,所以其实同步任务就是执行在当前线程


※什么时候会死锁?

main函数默认是运行在主队列的,也就是我们dispatch_get_main_queue得到的queue,当我们在主队列里面给主队列抛一个同步任务,会造成后面抛入的任务在前面的任务执行完毕,前面的又在等后面的。


主队列死锁

基于先入先出原则,只有A执行完毕才能执行B,但A任务内要等待后被抛入的任务B执行完才能继续执行,所以有死锁问题。

在串行队列中同步等待串行队列容易发生死锁

例如执行下面的代码,虽然不会crash,但是控制台只输出了“任务1开始”。

dispatch_queue_t serialQueue = dispatch_queue_create("serial_queue", DISPATCH_QUEUE_SERIAL);

dispatch_async(serialQueue, ^{
    NSLog(@"任务1开始");
        
    dispatch_sync(dispatch_get_main_queue(), ^{
      NSLog(@"任务2");
    });

    NSLog(@"任务1结束");
});
    
dispatch_sync(serialQueue, ^{
    NSLog(@"任务3");
});
死锁.png

幸好这种死锁不会crash,一个圈的互相等待会比较不容易发现,所以日常用GCD的时候如果是串行同步的话需要格外注意一下哈。

为什么并行队列执行同步任务等待自己队列不会死锁呢?
因为主队列和串行队列,当队列中有任务在执行时就会暂停调度,等待调度中任务执行完毕后再执行后面的任务,但是并行队列没有这个限制。

死锁示例:
dispatch_queue_t serialQueue = dispatch_queue_create("serial_queue", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
    dispatch_sync(serialQueue, ^{
        NSLog(@"sync task");
    });
    
    NSLog(@"finished");
});

不死锁示例:
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
    dispatch_sync(concurrentQueue, ^{
        NSLog(@"sync task");
    });
    
    NSLog(@"finished");
});
⑦ 同步+创建串行队列
dispatch_queue_t serialQueue = dispatch_queue_create("serial_queue", DISPATCH_QUEUE_SERIAL);
for (NSInteger i = 0; i < 4; i++) {
  dispatch_sync(serialQueue, ^{
      NSLog(@"current thread: %@", [NSThread currentThread]);
      NSLog(@"is main thread: %@", [NSThread currentThread] == [NSThread mainThread] ? @"yes" : @"no");
      NSLog(@"current task: %ld", (long)i);
  });
}

输出:
current thread: <NSThread: 0x600001b32880>{number = 1, name = main}
is main thread: yes
current task: 0
current thread: <NSThread: 0x600001b32880>{number = 1, name = main}
is main thread: yes
current task: 1
current thread: <NSThread: 0x600001b32880>{number = 1, name = main}
is main thread: yes
current task: 2
current thread: <NSThread: 0x600001b32880>{number = 1, name = main}
is main thread: yes
current task: 3

和全局并行一样,同步+创建串行队列使用的也仍旧是当前线程(这里是主线程)。

其实同一个队列不一定使用同一个线程,当我们用异步+串行队列的时候,会新建一个线程执行,但如果是同步的任务,会在当前线程执行。

⑧ 同步+创建并行队列

同上哈,也是会顺序执行并且用当前线程。那么它和用同步串行的区别是啥呢?就同步任务其实没啥区别,但如果你还往里面抛异步任务就有区别啦。

dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
for (NSInteger i = 0; i < 4; i++) {
  dispatch_sync(concurrentQueue, ^{
      NSLog(@"current thread: %@", [NSThread currentThread]);
      NSLog(@"is main thread: %@", [NSThread currentThread] == [NSThread mainThread] ? @"yes" : @"no");
      [NSThread sleepForTimeInterval:1];
      NSLog(@"current task: %ld", (long)i);
  });
}

输出:
current thread: <NSThread: 0x6000016cd3c0>{number = 1, name = main}
is main thread: yes
current task: 0
current thread: <NSThread: 0x6000016cd3c0>{number = 1, name = main}
is main thread: yes
current task: 1
current thread: <NSThread: 0x6000016cd3c0>{number = 1, name = main}
is main thread: yes
current task: 2
current thread: <NSThread: 0x6000016cd3c0>{number = 1, name = main}
is main thread: yes
current task: 3

(4) 总结

/ 主队列 全局队列 串行队列 并行队列
同步 主线程运行(可能死锁) + 串行 当前线程运行 + 串行 当前线程运行 + 串行 当前线程运行 + 串行
异步 主线程运行 + 串行 多线程 + 并行 新线程 + 串行 多线程 + 并行

参考文章:
1.『GCD』详尽总结:https://www.jianshu.com/p/2d57c72016c6

  1. 关于死锁:https://www.jianshu.com/p/a6a581cbce4c
上一篇下一篇

猜你喜欢

热点阅读