【iOS】—— 深入了解GCD

一、基本概念

1.进程

  • 进程是一个具有一定独立功能的程序关于某次数据集合的一次运行活动,每个进程都有自己独立的一块内存空间,进程是操作系统分配资源的基本单元。
  • 进程是指在系统中正在运行的一个应用程序,就是一段程序的执行过程,我们可以理解为一个应用程序就是一个进程。
  • 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内,拥有独立运行所需的全部资源。

2.线程

  • 线程是进程中执行运算的最小单位,是进程中的一个实体。
  • 线程是系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源。
  • 一个进程至少有一个线程,应用程序启动的时候,系统会默认开启一条线程,也就是主线程。一个进程可以运行多个线程,多个线程可共享进程所拥有的全部资源。同一进程中的多个线程之间可以并发执行。
    23423423

3.进程和线程的关系

  • 线程是进程的执行单元,进程的所有任务都在线程中执行
  • 线程作为调度和分配的基本单位,进程作为拥有资源的基本单位
  • 一个程序可以对应多个进程(多进程),一个进程中可有多个线程,但至少要有一条线程
  • 同一个进程内的线程共享进程资源

4.串行、并发和并行

串行

多个任务,执行完一个才能执行另一个,也就是排队式执行任务。

并发

多个线程在单个核心运行,同一时间只能一个线程运行,系统不停的切换线程,看起来就像是同时运行,实际是多个线程不停切换。
4234234

并行

每个线程分配给独立的核心,线程同时运行。
4234234

4.多线程编辑

多线程编辑,顾名思义就是多个线程可以共同执行,一个进程中至少拥有一个线程,当然我们也可以定义多个线程,每个线程都完成自己对应的工作,并且各个线程之间可以并行执行代码,这样在就实现了一个程序共同执行多段代码的功能,大大的提高了程序的运行效率,这就是多线程编辑。
4234234

5.GCD的定义

百度:
Grand Central Dispatch(GCD) 是 Apple 开发的一个多核编程的较新的解决方法。它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。它是一个在线程池模式的基础上执行的并发任务。在 Mac OS X 10.6 雪豹中首次推出,也可在 iOS 4 及以上版本使用。

苹果官方:
Grand Central Dispatch(GCD) 是异步执行任务的技术之一。一般将应用程序中记述的线程管理用的代码在系统级实现。开发者只需要定义想执行的任务并追加到适当的Dispatch Queue中,GCD就能生成必要的线程并计划执行任务。由于线程管理是作为系统的一部分来实现的,因此可统一管理,也可执行任务,这样就比以前的线程更有效率。
GCD用非常简洁的记述方法,实现了极为复杂的多线程编程。

6.GCD的好处

  • GCD 可用于多核的并行运算;
  • GCD 会自动利用更多的 CPU 内核(比如双核、四核);
  • GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程);
  • 程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码。

7.GCD任务和队列

任务

任务: 就是执行操作的意思,也就是要在线程中执行的那段代码。在GCD中就是放在Block中的内容。

具体对任务的执行有两种方式:同步执行和异步执行,主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力。

执行任务的两种方式:

  • 同步执行(sync):
    • 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列中的任务完成之后才能执行

    dispatch_sync,这个函数会把一个block加入到指定的队列中,而且会一直等到执行完blcok,这个函数才返回。因此在block执行完之前,调用dispatch_sync方法的线程是阻塞的。

    • 只能在当前线程中执行任务,不具备开启新线程的能力
  • 异步执行(async):
    • 异步添加任务到指定的队列中,不会做任何的等待,可以继续执行任务

    使用dispatch_async,这个函数也会把一个block加入到指定的队列中,但是和同步执行不同的是,这个函数把block加入队列后不等block的执行就立刻返回了。

    • 在新线程中执行任务,具备开启新线程的能力

举个例子:我们要给A和B打电话

  • 同步执行:我们给A打电话的时候,不能同时打给B。至于等到和A打完了,才能打给B(等待任务执行结束)。并且只能使用当前这一个电话(不具备开启新线程的能力)
  • 异步执行:我们给A打电话的时候,不用等着和A通话结束,就能同时打给B(不用等待任务执行结束)。而且除了当前电话,我们还可以使用其他电话(具备开启新线程的能力)

这里异步执行只是具备开启新线程的能力,但不一定会开启新线程,是否开启新线程还与队列的类型有关。

队列

队列(Dispatch Queue):这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用先进先出的原则,即新任务总是会被插在队尾,而读取任务则总是从队列的头部开始读取,每读一个任务,则从队列中释放一个任务。
4324234234

GCD中有两种队列:

  • 串行队列(Serial Dispatch Queue):每次只有一个任务被执行,一个任务执行完再执行下一个,一个接着一个执行。
  • 并发队列(Concurrent Dispatch queue):可以让多个任务并发(同时)执行。

注意:并发队列的并发功能只有在异步执行(async)方法下才有效,这个队列中的任务也是按着先进先出的方式开始执行,但结束时间不确定。

4234234
2131231

二、GCD的使用

1.GCD的使用步骤

  • 创建一个队列(串行队列或并发队列)
  • 将任务追加到任务的等待队列中,然后系统会根据任务类型执行任务(同步执行或异步执行)

2.队列的创建

我们使用dispatch_queue_create方法创建队列Dispatch Queue

dispatch_queue_t myQueue = dispatch_queue_create("xiyouedc", DISPATCH_QUEUE_CONCURRENT);
  • 参数一:指定生成返回的Dispatch Queue的名称,它是一个char *类型,可以为空。

    队列的名称推荐使用应用程序 ID 这种逆序全程域名。

  • 参数二:指定为NULLDISPATCH_QUEUE_SERIAL,生成Serial Dispatch Queue(串行队列),指定为DISPATCH_QUEUE_CONCURRENT,生成Concurrent Dispatch Queue(并行队列)。

一个串行队列(Serial Dispatch Queue)只是用一个线程。而当生成多个Serial Dispatch Queue时,各串行队列将并行执行。但如果过多使用多线程,就会消耗大量内存,引起大量的上下文切换,大幅度的降低系统的响应性能。而对于并发队列(Concurrent Dispatch Queue)来说,不管生成多少并发队列,由于XNU内核只使用有效管理的线程,因此不会发生上述问题。
5345345
另外,当多个线程更新相同资源导致数据竞争时,可使用串行队列Serial Dispatch Queue强制让他们一个一个的更新数据,这样就不会造成数据冲突了。
3242342
我们一般在需要更新数据的时候使用Serial Dispatch Queue(串行队列),而在获取数据的时候一般使用Concurrent Dispatch Queue(并发队列),因为更新数据的时候可能会出现数据冲突,使用串行队列来避免,在获取数据的时候没有这个冲突,所以我们怎么快怎么来。

3.队列的获取

队列的获取系统有给我们提供相应的方法:

Main Dispatch Queue获取

// 获取主队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();

在主线程中执行的Dispatch Queue,因为主线程只有一个,所以Main Dispatch Queue是串行队列(Serial Dispatch Queue)。
追加到Main Dispatch Queue的处理会在主线程的RunLoop中执行。

Global Dispatch Queue获取

它是所有应用程序都能够使用的并发队列(Concurrent Dispatch Queue)。
有四个优先级:高优先级、默认优先级、低优先级、后台优先级。

通过XNU内核管理的用于全局并发队列Global Dispatch Queue的线程,将各自使用的Global Dispatch Queue的执行优先级作为线程的执行优先级使用,所以在向Global Dispatch Queue追加任务时,应选择与处理内容对应的执行优先级的Global Dispatch Queue

// 获取全局并发队列
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
  • 参数一:队列的优先级
  • 参数二:暂时没用,可能以后会用来做扩展,现在用0就行

这两种方法同时也可以作为另一种获取串行队列和并发队列的获取方法,不过主队列可能会有点特殊,要特别注意使用。

4.任务的创建方法

GCD提供了同步执行任务的创建方法dispatch_sync和异步执行任务的创建方法dispatch_async

// 同步执行任务创建方法
dispatch_sync(queue, ^{
       // 这里放同步执行任务代码 
 });

// 异步执行任务创建方法
dispatch_async(queue, ^{
      // 这里放异步执行任务代码
});
  • 参数一:要将任务代码放入的执行队列
  • 参数二:要执行的任务代码

我们上面说到了两种任务执行的方法(同步执行、异步执行)和两种队列(串行队列、并发队列),那么现在就出现了4种情况:

  • 同步执行 + 串行队列
  • 同步执行 + 并发队列
  • 异步执行 + 串行队列
  • 异步执行 + 并发队列
    另外,除了普通创建队列方法,我们刚才还说了直接获取主队列和全局并发队列这两种方法,全局并发队列用来做普通的并发队列没问题,但是因为主队列比较特殊,所以我们又产生了两种组合:
  • 同步执行 + 主队列
  • 异步执行 + 主队列

5.任务与不同组合方式的区别

假设当前线程为主线程的情况下:不同执行方式 + 不同队列的区别如下:
4324234

在主线程中调用 同步执行 + 主队列 导致死锁问题的原因:

因为主队列中追加的任务与主线程本身的任务两者相互等待, 阻塞了主线程, 最终导致主线程死锁。
在其他线程中调用 同步执行 + 主队列, 就不会阻塞主队列,不会造成死锁问题,最终结果就是不会开启新线程,串行执行任务。

队列嵌套情况下,不同组合的区别:

5435345345
总结下来就是:主队列 + 同步执行 会导致死锁问题!

同步执行 + 串行队列

没有开启新线程,串行执行任务:

NSLog(@"当前线程:%@", [NSThread currentThread]);
NSLog(@"begin");

dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);

dispatch_sync(queue, ^{
    [NSThread sleepForTimeInterval:3];
    NSLog(@"任务一执行线程:%@", [NSThread currentThread]);
});

dispatch_sync(queue, ^{
    [NSThread sleepForTimeInterval:3];
    NSLog(@"任务二执行线程:%@", [NSThread currentThread]);
});

dispatch_sync(queue, ^{
    [NSThread sleepForTimeInterval:3];
    NSLog(@"任务三执行线程:%@", [NSThread currentThread]);
});

NSLog(@"end");

输出结果:
423423423

  • 因为同步执行不具备创建新线程的能力,并且串行队列也不能开启新线程,所以现在就只有这一个线程(主线程)可用。
  • 又因为同步执行需要代码块中的代码执行完了才会返回,所以他们就会在这个唯一线程(主线程)中逐一执行代码。

同步执行 + 并发队列

没有开启新线程,串行执行任务:

NSLog(@"当前线程:%@", [NSThread currentThread]);
NSLog(@"begin");

dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);

dispatch_sync(queue, ^{
    [NSThread sleepForTimeInterval:3];
    NSLog(@"任务一执行线程:%@", [NSThread currentThread]);
});

dispatch_sync(queue, ^{
    [NSThread sleepForTimeInterval:3];
    NSLog(@"任务二执行线程:%@", [NSThread currentThread]);
});

dispatch_sync(queue, ^{
    [NSThread sleepForTimeInterval:3];
    NSLog(@"任务三执行线程:%@", [NSThread currentThread]);
});

NSLog(@"end");

输出结果:
423423432

  • 因为同步执行不具备创建新线程的能力,虽然并发队列可以开启新线程来使用,但是没有新的线程供它开启了,所以也就只有这一个线程(主线程)了。
  • 又因为同步执行是将代码块中的代码执行完了之后才会返回,并且现在也只有这一个线程(主线程),所以他们就会逐一等待执行。

异步执行 + 串行队列

有开启新线程(1条),串行执行任务:

NSLog(@"当前线程:%@", [NSThread currentThread]);
NSLog(@"begin");

dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:3];
    NSLog(@"任务一执行线程:%@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:3];
    NSLog(@"任务二执行线程:%@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:3];
    NSLog(@"任务三执行线程:%@", [NSThread currentThread]);
});

NSLog(@"end");

输出结果:
43534534

  • 因为异步执行可以创建新线程,但是串行队列不能开启更多的新线程来使用,所以它的任务就只能在一开始创建的第一个线程来使用,这里的这个线程并不是主线程,因为异步执行已经开启了三个线程,只是串行队列没办法将资源最大化利用,它就只能使用一开始创建的第一个线程。
  • 又因为现在已经开启了一个新的线程,那么任务也就会被添加到该线程中,那么主线程就可以执行其他的代码了,将添加到新线程的任务都交给新线程来处理。

异步执行 + 并发队列

有开启新线程,并发执行任务:

NSLog(@"当前线程:%@", [NSThread currentThread]);
NSLog(@"begin");

dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:3];
    NSLog(@"任务一执行线程:%@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:3];
    NSLog(@"任务二执行线程:%@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:3];
    NSLog(@"任务三执行线程:%@", [NSThread currentThread]);
});

NSLog(@"end");

输出结果:
234234234

  • 因为异步执行可以创建新线程,所以这里会开启三个新的线程,同时又因为并发队列可以开启新线程,所以它就会将系统的资源最大化利用起来,就会给每个任务都开启一个线程来执行,这样他们也就无需等待,在三个新线程中肆无忌惮的执行相应的任务。

同步执行 + 主队列

所有放在主队列的任务,都会放在主线程中执行。
同步执行 + 主队列 在不同线程中调用的结果是不一样的,在主线程中调用会发生死锁,在其他线程中调用就不会。

在主线程中调用 同步执行 + 主队列

出现死锁情况,程序崩溃:

NSLog(@"当前线程:%@", [NSThread currentThread]);
NSLog(@"begin");

dispatch_queue_t queue = dispatch_get_main_queue();

dispatch_sync(queue, ^{
    [NSThread sleepForTimeInterval:3];
    NSLog(@"任务一执行线程:%@", [NSThread currentThread]);
});

dispatch_sync(queue, ^{
    [NSThread sleepForTimeInterval:3];
    NSLog(@"任务二执行线程:%@", [NSThread currentThread]);
});

dispatch_sync(queue, ^{
    [NSThread sleepForTimeInterval:3];
    NSLog(@"任务三执行线程:%@", [NSThread currentThread]);
});

NSLog(@"end");

输出结果:
4234234234

  • 在执行到任务一的时候程序直接崩溃了,我们在主线程中,将任务一又加在了主线程的执行队列中,此时同步执行任务一会等待当前队列(主队列)中的其他任务执行完才会执行任务一,然而同步执行sync函数又会等待任务一执行完毕再继续执行主线程任务,此时任务一和主线程的任务就处于相互等待的情况,这就造成了死锁,所以程序就崩溃了。
避免上述死锁情况的出现:
  • 将同步执行改为异步执行
  • 或者将dispatch_get_main_queue换成其他串行或任意并行队列
在其他线程中调用 同步执行 + 主队列

因为将任务添加到主线程的操作是在新线程中完成的,所以不会造成死锁:

	NSLog(@"当前线程:%@", [NSThread currentThread]);
    NSLog(@"begin");
    // 创建新线程执行syncMain函数
    [NSThread detachNewThreadSelector:@selector(syncMain) toTarget:self withObject:nil];
    [NSThread sleepForTimeInterval:3];
    NSLog(@"end");


- (void)syncMain {
    NSLog(@"当前执行syncMain函数的线程:%@", [NSThread currentThread]);
    NSLog(@"begin---syncMain");
    
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    dispatch_sync(queue, ^{
        [NSThread sleepForTimeInterval:3];
        NSLog(@"任务一执行线程:%@", [NSThread currentThread]);
    });
    
    dispatch_sync(queue, ^{
        [NSThread sleepForTimeInterval:3];
        NSLog(@"任务二执行线程:%@", [NSThread currentThread]);
    });
    
    dispatch_sync(queue, ^{
        [NSThread sleepForTimeInterval:3];
        NSLog(@"任务三执行线程:%@", [NSThread currentThread]);
    });

    NSLog(@"end---syncMain");
}

输出结果:
在342423423片描述

  • 此时程序就不会崩溃了,因为syncMain方法在新线程中执行,间接的任务一、二、三加入到主队列这个操作就是就是在新线程中执行的。当主线程在执行sleep事件时,此时新线程将任务一添加到主线程队列的队尾,等待主线程的任务执行完毕才会执行任务一,任务一执行完了sync函数才能返回,继续执行接下来的任务,所以该代码就不会在卡住主线程了,就不会造成死锁了。

异步执行 + 主队列

只在主线程中执行任务,执行完一个任务,再执行下一个任务:

NSLog(@"当前线程:%@", [NSThread currentThread]);
NSLog(@"begin");

dispatch_queue_t queue = dispatch_get_main_queue();

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:3];
    NSLog(@"任务一执行线程:%@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:3];
    NSLog(@"任务二执行线程:%@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:3];
    NSLog(@"任务三执行线程:%@", [NSThread currentThread]);
});

NSLog(@"end");

输出结果:
5345345

  • 因为是异步执行将任务添加到主线程,并且async方法在将任务添加完给主线程之后就会立刻返回,所以当async在异步执行添加任务一、二、三到主线程的时候,主线程已经执行到接下来的其他代码了(就是输出end代码),在其他代码执行完了之后才会处理添加到主线程队列的任务一、二、三,这也就是为什么end在任务前面输出出来的原因了。

三、GCD的其他方法

GCD栅栏方法:dispatch_barrier_async/dispatch_barrier_sync

当我们需要异步执行两组操作,并且在第一组操作进行完了之后才能开始第二组操作,这时候就得用到GCD的栅栏方法了,将我们要执行的各组操作分隔开。
55435345

dispatch_barrier_async(异步栅栏)

dispatch_barrier_async函数会等待前面追加到并发队列上的任务全部执行完之后,再将指定的任务追加到该并发队列中。然后在dispatch_barrier_async函数追加的任务执行完毕之后,并发队列才恢复为一般动作,追加到该并发队列的任务又开始执行。

NSLog(@"当前线程:%@", [NSThread currentThread]);
NSLog(@"begin");

dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"任务一执行线程:%@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"任务二执行线程:%@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"任务三执行线程:%@", [NSThread currentThread]);
});

dispatch_barrier_async(queue, ^{
    NSLog(@"barrier线程:%@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"任务四执行线程:%@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"任务五执行线程:%@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"任务六执行线程:%@", [NSThread currentThread]);
});

NSLog(@"end");

输出结果:
4324234

  • 根据结果我们可以看到,我们使用 异步执行 + 并发队列 开启了诸多新线程来执行任务,但是当我们在其中间又加了栅栏方法,将这原本可以一起并行执行的任务分隔开来了,只有等加在栅栏方法之前的任务(任务一、二、三)全部执行完毕之后,才可以异步执行接下来的几个任务。并且栅栏方法的任务也算到了新线程中。
dispatch_barrier_sync(同步栅栏)
NSLog(@"当前线程:%@", [NSThread currentThread]);
NSLog(@"begin");

dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"任务一执行线程:%@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"任务二执行线程:%@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"任务三执行线程:%@", [NSThread currentThread]);
});

dispatch_barrier_sync(queue, ^{
    NSLog(@"barrier线程:%@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"任务四执行线程:%@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"任务五执行线程:%@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"任务六执行线程:%@", [NSThread currentThread]);
});

NSLog(@"end");

输出结果:
42342343

  • 代码与上述的异步栅栏无异,不过是将异步栅栏换为了同步栅栏,我们可以发现,换成同步栅栏之后,栅栏中的任务添加到了主线程中执行,而且还必须得等这个栅栏任务执行完了主线程也才能接着执行,那也就是说,使用同步栅栏会阻塞主线程的执行,并且只有当该同步栅栏之前的任务和该同步栅栏的任务执行完了之后,程序才会接着向下执行。

注意:异步栅栏

The queue you specify should be a concurrent queue that you create yourself using the dispatch_queue_create function. If the queue you pass to this function is a serial queue or one of the global concurrent queues, this function behaves like the dispatch_async function.
您指定的队列应该是您自己使用dispatch_queue_create函数创建的并发队列。如果你传递给这个函数的队列是一个串行队列或一个全局并发队列,该异步栅栏函数的行为类似于dispatch_async函数,就没有什么特殊的了。

下面验证一下这个说法:

异步栅栏 + 串行队列
NSLog(@"当前线程:%@", [NSThread currentThread]);
NSLog(@"begin");

dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"任务一执行线程:%@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"任务二执行线程:%@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"任务三执行线程:%@", [NSThread currentThread]);
});

dispatch_barrier_async(queue, ^{
    NSLog(@"barrier线程:%@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"任务四执行线程:%@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"任务五执行线程:%@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"任务六执行线程:%@", [NSThread currentThread]);
});

NSLog(@"end");

输出结果:
42342334

  • 从结果看来,这样使用栅栏也没有什么用了,与 异步执行 + 串行队列 无异了。
异步栅栏 + 全局并发队列
NSLog(@"当前线程:%@", [NSThread currentThread]);
NSLog(@"begin");

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"任务一执行线程:%@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"任务二执行线程:%@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"任务三执行线程:%@", [NSThread currentThread]);
});

dispatch_barrier_async(queue, ^{
    NSLog(@"barrier线程:%@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"任务四执行线程:%@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"任务五执行线程:%@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"任务六执行线程:%@", [NSThread currentThread]);
});

NSLog(@"end");

输出结果:
64232423

  • 这样也就和 异步执行 + 并发队列 无异了。

GCD延时执行方法:dispatch_after

他就是一个线程延时处理操作,如果我们想要在指定时间后执行某个处理,就可以使用它。

dispatch_after并不是在指定时间后执行处理,而是在指定时间后追加处理到队列中。严格来说,这个时间并不是绝对准确的,但想要大致延迟执行时间,该方法是非常有效的。

  • 参数一:指定时间用的dispatch_time_t类型的值。

    该值可以使用dispatch_time函数或dispatch_walltime函数获得,dispatch_time函数用于计算相对时间,dispatch_walltime用于计算绝对时间。
    dispatch_time函数能够获取从第一个参数dispatch_time_t类型值中指定的时间开始,到第二个参数指定的毫微秒单位时间时间后的时间。第一个参数经常使用DISPATCH_TIME_NOW,表示现在的时间。
    ull”是C语言的数值字面量,是显示表明类型时使用的字符串(unsign long long)。
    SEC 秒
    PER 每
    NSEC 纳秒
    MSEC 毫秒
    USEC 微秒

  • 参数二:指定要追加到的队列。
  • 参数三:要执行处理的任务块。
NSLog(@"当前线程:%@", [NSThread currentThread]);
NSLog(@"begin");

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 2ull * NSEC_PER_SEC);

dispatch_after(time, dispatch_get_main_queue(), ^{
    NSLog(@"dispatch_after任务线程:%@", [NSThread currentThread]);
});

NSLog(@"end");

输出结果:
5345345

  • 这里的延时操作就是在到达该操作2秒后才开始执行该操作的,并且因为它是在两秒后才将这个延时操作的任务加到线程中去的,而不是遇到这个操作就直接加到线程中的,所以它也不会造成线程阻塞。

GCD一次性代码(只会执行一次):dispatch_once

dispatch_once这个函数我们经常在书写单例的时候用到,它保证其中的代码只执行一次,通过这个函数,即使在多线程的环境下,也可以保证线程安全。

  • 参数一:判断代码块是否要执行的变量。

    当onceToken为0时,线程执行dispatch_once的block中的代码
    当onceToken为-1时,线程跳过dispatch_once的block中的代码
    当onceToken为其他值时,线程被阻塞,等待onceToken值改变

  • 参数二:执行的代码块任务。
NSLog(@"当前线程:%@", [NSThread currentThread]);
NSLog(@"begin");

dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    NSLog(@"dispatch_once方法执行的线程:%@", [NSThread currentThread]);
});

dispatch_once(&onceToken, ^{
    NSLog(@"dispatch_once方法执行的线程:%@", [NSThread currentThread]);
});

dispatch_once(&onceToken, ^{
    NSLog(@"dispatch_once方法执行的线程:%@", [NSThread currentThread]);
});

NSLog(@"end");

输出结果:
4234234

  • 可以看出它确实只执行了一次,它最重要的判断依据就是参数一,并且一个线程只能拥有一个该函数,如果在该线程中有重新定义一个并执行,程序就会崩溃。

快速迭代方法:dispatch_apply

类似于for循环遍历方法,GCD给我们提供了快速迭代的方法dispatch_apply

如果在串行队列中使用dispatch_apply方法,那么就和for循环一样,按照顺序同步进行,那么使用这个方法就没有意义了,但是我们如果在并发队列中使用dispatch_apply方法,dispatch_apply就可以在多个线程同时遍历数据。

注意:无论是串行队列还是并发队列,dispatch_apply都会等待其中的任务执行完才会返回。

  • 参数一:重复次数
  • 参数二:参数三中的任务要添加到的队列
  • 参数三:追加的任务
NSLog(@"当前线程:%@", [NSThread currentThread]);
NSLog(@"begin");

dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_apply(10, queue, ^(size_t iteration) {
    NSLog(@"第%zu次任务", iteration);
});

NSLog(@"end");

输出结果:
534534534

  • 可以看到他是并发执行循环任务的,但是也得注意这点,不可以无脑使用,随意使用可能还会造成程序crash的情况。

队列组:dispatch_group

在追加到Dispatch Queue中的多个处理全部结束后想执行结束处理任务,就可以使用该方法。
如果使用一个串行队列,因为它是逐一执行的,所以只要将结束处理最后追加到队列中即可。但是如果使用并发队列Concurrent Dispatch Queue时,想要做一个全部线程都完成的任务的最终处理任务就需要使用Dispatch Group了。

  • 调用队列组的dispatch_group_async先把任务放在队列中,然后再将队列放入队列组group中,或者使用队列组的dispatch_group_enterdispatch_group_leave组合来实现dispatch_group_async
  • 调用队列组的dispatch_group_notify回到指定线程执行任务,或者使用dispatch_group_wait回到当前线程继续向下执行,但是dispatch_group_wait会阻塞线程。
dispatch_group_notify

监听group中任务的完成状态,当全部的任务都执行完成后,追加任务到DIspatch Queue中

  • dispatch_group_create函数生成dispatch_group_t类型的Dispatch Group
  • dispatch_group_async函数:
    • 参数一:指定的group
    • 参数二:参数三中的任务要添加到的队列
    • 参数三:要添加的任务
  • dispatch_group_notify函数:
    • 参数一:要监视的group
    • 参数二:追加任务所在的执行处理该任务的线程
    • 参数三:要追加的任务
    • 注意:当要监视的group的全部任务处理完了,才会将要追加的任务追加到想追加的线程去处理
NSLog(@"当前线程:%@", [NSThread currentThread]);
NSLog(@"begin");

dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group, queue, ^{
    
    [NSThread sleepForTimeInterval:2];
    NSLog(@"1, %@", [NSThread currentThread]);
});

dispatch_group_async(group, queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"2, %@", [NSThread currentThread]);
});
    
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"end group线程:%@", [NSThread currentThread]);
});
    
dispatch_group_async(group, queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"3, %@", [NSThread currentThread]);
});

NSLog(@"end");

输出结果:
4234234

  • 因为我们使用的是 异步执行group + 并发队列,所以程序会创建新线程来同步完成任务,并且我们能看到最终当该group中的任务全部执行完毕之后,才会执行notify中的任务。
dispatch_group_wait

该函数如字面意思,wait(等待),一旦调用该函数,该函数就处于调用状态不返回,即执行dispatch_group_wait函数的线程(当前线程)阻塞,在经过该函数中指定的时间 或 指定的group中的任务全部执行完之后,才会往下继续执行,也就是才会执行dispatch_group_wait之后的操作。

  • 参数一:指定的group
  • 参数二:等待的时间
  • 返回值为0,意味着指定的group中的全部任务执行结束了
  • 返回值不为0,意味着过了等待时间,但指定的group中的任务并没有执行完
NSLog(@"当前线程:%@", [NSThread currentThread]);
NSLog(@"begin");

dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group, queue, ^{
    
    [NSThread sleepForTimeInterval:2];
    NSLog(@"1, %@", [NSThread currentThread]);
});

dispatch_group_async(group, queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"2, %@", [NSThread currentThread]);
});
    
// 等待上面的任务全部完成后,会往下继续执行(会阻塞当前线程)
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

dispatch_group_async(group, queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"3, %@", [NSThread currentThread]);
});

NSLog(@"end");

输出结果:
42342342

  • 我们发现还是跟使用notify的作用不太相同,notify是异步,并且会将该group(不管在其定义前加入到group,还是在其定义后加入到group)中的所有任务都执行完了,才会调用其最终处理的任务,但是wait不同,wait直接阻塞了线程,使程序在其定义后的所有代码都不能执行,只有它中的group或者等待时间到了之后,他才会返回调用,线程才能再开始执行。
dispatch_group_enter、dispatch_group_leave
  • dispatch_group_enter标志着要有一个任务追加到group中,执行一次,相当于group中未执行完毕的任务数+1
  • dispatch_group_leave标志着有一个任务离开了group,执行一次,相当于group中未执行完毕的任务数-1
NSLog(@"当前线程:%@", [NSThread currentThread]);
NSLog(@"begin");

dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();

dispatch_group_enter(group);
dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"1, %@", [NSThread currentThread]);
    dispatch_group_leave(group);
});

dispatch_group_enter(group);
dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"2, %@", [NSThread currentThread]);
    dispatch_group_leave(group);
});
    
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    //等前面的异步操作都执行完毕,将该任务添加到主队列,回到主线程执行
    NSLog(@"end group执行线程:%@", [NSThread currentThread]);
});

dispatch_group_enter(group);
dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"3, %@", [NSThread currentThread]);
    dispatch_group_leave(group);
});

NSLog(@"end");

输出结果:
4234234

  • 我们可以看出,这里的dispatch_group_enterdispatch_group_leave组合,其实等同于dispatch_group _async,其实还有点像MRC中自己实现对象的引用计数,执行前先加入,执行完了不需要了,再移除。

当group中所有的任务执行完成之后,才会执行dispatch_group_notify中的任务,跟代码的位置无关,只要你在第一个任务加入到该group之后定义的notify,都是group中的任务全部执行完了,才会调用notify中的任务。

GCD信号量:dispatch_semaphore

GCD中的信号量是指Dispatch Semaphore,是持有计数的信号,该计数是多线程编程中的计数类型信号。

  • dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);:创建一个Semaphore并初始化信号的总量
  • dispatch_semaphore_signal:发送一个信号,信号量+1
  • dispatch_semaphore_wait:信号量减1,信号总量小于0时会一直等待(阻塞所在线程),否则就可以正常运行

Dispatch Semaphore在实际开发中主要用于:

  • 保持线程同步,将异步执行任务转为同步执行任务
  • 保证线程安全,为线程加锁
Dispatch Semaphore 线程同步

在开发中可能会遇到:异步执行耗时操作,需要使用异步执行的结果进行一些额外的操作。换句话说,就是将异步执行任务转换成同步任务来执行。

NSLog(@"当前线程:%@", [NSThread currentThread]);
NSLog(@"begin");

dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);

dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

__block int num = 0;

dispatch_async(queue, ^{
    NSLog(@"%@", [NSThread currentThread]);
    num = 10;
    dispatch_semaphore_signal(semaphore);
});

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

NSLog(@"end");
NSLog(@"num = %d, 当前线程:%@", num, [NSThread currentThread]);

输出结果:
43242342

  • 我们可以看到,该程序发生了线程等待,原本被异步执行 + 并发队列 完全不会影响当前的主线程,但是主线程却是在最后才执行输出的的。
  • 这是因为当时我们创建信号量的时候赋初值为0,async方法异步执行将任务添加到并发队列之后又会直接返回,接着代码走到wait方法中,使信号量 - 1,此时的信号量 < 0,当前线程(主线程)阻塞,进入等待状态。
  • 然后异步任务开始执行,当执行到signal之后, 信号量 + 1,此时的信号量 = 0,被阻塞的线程(主线程)恢复,继续执行代码,所以才会在最后才打印。
dispatch semaphore 线程安全和线程同步(为线程加锁)

线程安全:如果代码所在的进程中有多个线程同时运行,而这些线程可能会同时运行这段代码,如果每次运行的结果和单线程运行的结果是一样的,变量的值也与预期的是一样的,就是线程安全的。

若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量就是线程安全的。若有多个线程同时执行写操作(更改变量),一般都需要考虑线程同步,否则就可能影响线程安全,运行出的结果可能并不是预期的结果。

线程同步:可理解为线程A和线程B一起配合,线程A执行到一定程度时要依靠线程B的某个结果/数据,于是停下来,示意B运行。B执行完,再将结果/数据给A,A再继续执行。

线程的同步关系源于他们之间的相互合作,下面通过模拟售卖门票,来实现线程安全和解决线程同步问题:

总共20张门票,有两个售卖门票的窗口,窗口A、窗口B

  • 非线程安全(不使用信号量)
- (void)initializeTicketInformation {
    NSLog(@"begin 当前线程:%@", [NSThread currentThread]);
    
    self.count = 20;
    
    dispatch_queue_t queueA = dispatch_queue_create("queueA", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queueB = dispatch_queue_create("queueB", DISPATCH_QUEUE_SERIAL);
    
    __weak id weakSelf = self;
    dispatch_async(queueA, ^{
        [weakSelf saleTicket];
    });
    dispatch_async(queueB, ^{
        [weakSelf saleTicket];
    });
    
}

- (void)saleTicket {
    while (1) {
        if (_count > 0) { // 还有门票,可以继续卖
            self.count--;
            NSLog(@"剩余门票:%ld,窗口:%@", _count, [NSThread currentThread]);
            [NSThread sleepForTimeInterval:0.2];
        } else { // 没有门票了,收摊
            NSLog(@"门票卖完了");
            break;
        }
    }
}

输出结果:
5345345345

  • 其中有很多重复的数据,这是不应该出现的,但是由于我们同时有两个线程对count进行修改,并且没有做到数据同步,导致修改后的值并不是我们预期的值,得到的剩余票数是错乱的,那么此时的线程就是不安全的。

  • 线程安全(使用semaphore加锁)

- (void)initializeTicketInformation {
    NSLog(@"begin 当前线程:%@", [NSThread currentThread]);
    
    self.count = 20;
    
    self.semaphore = dispatch_semaphore_create(1);
    
    dispatch_queue_t queueA = dispatch_queue_create("queueA", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queueB = dispatch_queue_create("queueB", DISPATCH_QUEUE_SERIAL);
    
    __weak id weakSelf = self;
    dispatch_async(queueA, ^{
        [weakSelf saleTicket];
    });
    dispatch_async(queueB, ^{
        [weakSelf saleTicket];
    });
    
}

- (void)saleTicket {
    while (1) {
        
        dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
        
        if (_count > 0) { // 还有门票,可以继续卖
            self.count--;
            NSLog(@"剩余门票:%ld,窗口:%@", _count, [NSThread currentThread]);
            [NSThread sleepForTimeInterval:0.2];
            
            dispatch_semaphore_signal(self.semaphore);
            
        } else { // 没有门票了,收摊
            NSLog(@"门票卖完了");
            
            dispatch_semaphore_signal(self.semaphore);
            
            break;
        }
    }
}

输出结果:
4234234

  • 这里我们设置信号量初始值为1,对票数count的修改是互斥进行的,开始买票时wait信号量-1,此时信号量 < 0,给线程上锁,票数count–-完或门票卖完signal信号量+1,此时信号量 == 0,给线程开锁,即可保证线程安全

自旋锁与互斥锁

自旋锁

是一种用于保护多线程共享资源的锁,与一般互斥锁不同之处在于:当自旋锁尝试获取锁时以忙等的形式不断的循环检查锁是否可用。当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程就会一直等待(不会睡眠),当上一个线程的任务执行完毕,下一个线程会立即执行。
在多CPU环境中,对持有锁较短的程序来说,使用自旋锁代替一般的互斥锁往往能够提高串程序的性能。

  • 优点:
    自旋锁不会引起调用着线程睡眠,所以不会进行线程调度上下文切换 ,自旋锁的效率远高于互斥锁。
  • 缺点:
    自旋锁一直占用CPU,在为获得锁的情况下,一直运行(自旋)占用着CPU,如果不能在很短的时间内获得锁,那么CPU的效率就会降低。

举例:

atomic、OSSpinLock、dispatch_semaphore_t

互斥锁

当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入睡眠状态等待任务执行完毕。当上一个线程的任务执行完毕,下一个线程会自动会醒然后执行任务。

举例:

pthread_mutex、@synchronized、NSLock、NSConditionLock 、NSCondition、NSRecursiveLock

总结:

自旋锁会忙等:所谓忙等,即在访问被锁资源时,调用者线程不会休眠,而是不停循环访问看是否可以占用线程来执行任务,直到被锁资源释放锁。

互斥锁会休眠:所谓休眠,即在访问被锁资源时,调用者线程会休眠,此时CPU可以调度其他线程工作。直到被锁资源释放锁,此时会唤醒休眠线程。

参考房学姐的iOS——GCD详解

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值