美文网首页半栈工程师程序员iOS技术点
Objective-C多线程开发之GCD(最全总结)

Objective-C多线程开发之GCD(最全总结)

作者: 儒此优雅 | 来源:发表于2018-05-29 10:20 被阅读152次

什么是CGD呢?以下摘自苹果的官方说明。

  Grand Central Dispatch (GCD) 是异步执行任务的技术之一。应用程序中记述的线程管理用的代码是在系统级中实现的。开发者只需要定义想要执行的任务并追加到适当的Dispatch Queue中,GCD就能生成必要的线程并执行任务。
  也就是说,GCD用我们难以置信的非常简洁的方法,实现了极为复杂的多线程编程。本文将罗列GCD的API及其用法实例来帮助大家了解GCD。

1. GCD的队列和任务

GCD中有两个核心的概念:任务队列

1.1 任务 (Dispatch Block)

任务: 就是你想让系统执行的操作,GCD中通常是放在dispatch_block_t中的代码。任务分为同步执行(sync)异步执行(async)两种执行方式。

同步执行(sync) : 任务被同步添加到指定的队列中,在该任务执行结束前会一直等待。不具备开启线程的能力,只能在当前线程中同步执行任务。

异步执行(async) : 任务被异步添加到指定队列中,不会等待该任务执行。具备开启线程的能力,可在新线程中执行任务。

注: 异步执行虽然具有开启新线程的能力,但只有该任务追加到并发队列才会开启新线程。
1.2 队列 (Dispatch Queue)

队列 (Dispatch Queue) : 是执行任务的的等待队列。开发者通过 dispatch_asyncdispatch_sync函数等API,在dispatch_block_t中记述想要执行的任务并将其追加到Dispatch Queue中。Dispatch Queue按照追加的顺序(先进先出FIFO,First-In-First-Out)执行任务。队列分为 串行队列(Serial Dispatch Queue)并发队列(Concurrent Dispatch Queue)

串行队列(Serial Dispatch Queue) : 只开启一条新的线程,追加到该队列中的任务会依次按顺序执行。

并发队列(Concurrent Dispatch Queue) : 会开辟多条新的线程,追加到该队列中的任务会并行执行。

注: 
1. 并发队列虽然有开启多条新线程的能力,但是只有在异步执行任务时才会开启新线程。
2. 并发队列开启的新线程个数并不等同于任务个数,取决于队列的任务数、CPU核数、以及CPU负荷等当前系统状态。

2. GCD任务的创建、队列的获取和创建

2.1 创建同步及异步任务
dispatch_async(queue, ^{
    //创建异步任务
});
dispatch_sync(queue, ^{
    //创建同步任务
});

或者这样创建:

dispatch_block_t block = ^{
     //任务
};
dispatch_async(queue, block); //异步执行
dispatch_sync(queue, block);  //同步执行
2.2 获取系统主队列

主队列为特殊的串行队列,只有一个线程,即为主线程。主线程用于界面UI更新、用户事件交互等操作。所以比较耗时的操作(如查询数据库,数据请求等)都不应放在主线程中执行,会造成页面卡顿,影响用户体验。

dispatch_queue_t queue = dispatch_get_main_queue();
2.3 获取全局并发队列

系统为我们提供了四个全局的并发队列,我们可以直接获取,用来执行任务。

CPU执行任务处理时按照队列的优先级来分配资源,决定任务执行的先后顺序。但是苹果通过XUN内核用于Global Dispatch Queue 的线程并不能保证实时性 (Why? I don't know.),因此执行优先级只是大致的判断。

//第一个参数为队列优先级,第二个参数没用到,传0就行。

dispatch_queue_t queue =
 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
全局并发队列的优先级:

DISPATCH_QUEUE_PRIORITY_HIGH //高优先级
DISPATCH_QUEUE_PRIORITY_DEFAULT //默认优先级
DISPATCH_QUEUE_PRIORITY_LOW //低优先级
DISPATCH_QUEUE_PRIORITY_BACKGROUND //后台级
2.4 创建 串行队列(Serial Dispatch Queue)
//1.第一个参数为队列名称,推荐使用工程ID这种逆序命名方式,便于管理和调试。
//2.第二个参数传NULL或DISPATCH_QUEUE_SERIAL,表示串行队列。

    dispatch_queue_t queue =
     dispatch_queue_create("com.example.gcd", DISPATCH_QUEUE_SERIAL);
2.5 创建 并发队列(Concurrent Dispatch Queue)
//1.第一个参数为队列名称,推荐使用工程ID这种逆序命名方式,便于管理和调试。
//2.第二个参数传DISPATCH_QUEUE_CONCURRENT,表示并发队列。

    dispatch_queue_t queue =
     dispatch_queue_create("com.example.gcd", DISPATCH_QUEUE_CONCURRENT);

3. 不同队列及任务的不同执行方式对比

GCD 串行队列(Serial Dispatch Queue) 并发队列(Concurrent Dispatch Queue) 主队列
同步执行(sync) 不会开启新线程,任务同步执行 不会开启新线程,任务同步执行 主线程发生死锁
异步执行(async) 开启一条新线程,任务同步执行 会开启新线程,任务并发执行 不会开启新线程,任务同步执行
由上我们可以看出:要想并发执行某些任务,只有使用 〖并发队列 + 异步执行〗这种组合方式。
这也是我们开发中最常用的组合方式。

下面我们来具体试试这几种组合的应用:

3.1 串行队列 + 同步执行
- (void)serialAndSync {
    NSLog(@"begin ==== >>: %@",[NSThread currentThread]);

    dispatch_queue_t queue = \
    dispatch_queue_create("com.example.gcd", DISPATCH_QUEUE_SERIAL);
    dispatch_block_t block1 = ^{
        sleep(2);
        NSLog(@"1     ==== >>: %@",[NSThread currentThread]);
    };
    dispatch_block_t block2 = ^{
        sleep(1);
        NSLog(@"2     ==== >>: %@",[NSThread currentThread]);
    };
    dispatch_block_t block3 = ^{
        NSLog(@"3     ==== >>: %@",[NSThread currentThread]);
    };
    dispatch_sync(queue, block1);
    dispatch_sync(queue, block2);
    dispatch_sync(queue, block3);
    NSLog(@"end   ==== >>: %@",[NSThread currentThread]);
}

输出结果:

begin ==== >>: <NSThread: 0x1d4074f80>{number = 1, name = main}
1     ==== >>: <NSThread: 0x1d4074f80>{number = 1, name = main}
2     ==== >>: <NSThread: 0x1d4074f80>{number = 1, name = main}
3     ==== >>: <NSThread: 0x1d4074f80>{number = 1, name = main}
end   ==== >>: <NSThread: 0x1d4074f80>{number = 1, name = main}
  • 结论:
  1. 所有任务都在主线程中执行(【串行+同步】不会开启新线程)。
  2. 所有任务按照追加顺序依次执行。
  3. 任务执行在begin和end之间,同步执行任务会阻塞主线程。
3.2 串行队列 + 异步执行
- (void)serialAndAsync {
    NSLog(@"begin ==== >>: %@",[NSThread currentThread]);

    dispatch_queue_t queue = \
    dispatch_queue_create("com.example.gcd", DISPATCH_QUEUE_SERIAL);
    
    dispatch_block_t block1 = ^{
        sleep(2);
        NSLog(@"1     ==== >>: %@",[NSThread currentThread]);
    };
    dispatch_block_t block2 = ^{
        sleep(1);
        NSLog(@"2     ==== >>: %@",[NSThread currentThread]);
    };
    dispatch_block_t block3 = ^{
        NSLog(@"3     ==== >>: %@",[NSThread currentThread]);
    };
    dispatch_async(queue, block1);
    dispatch_async(queue, block2);
    dispatch_async(queue, block3);

    NSLog(@"end   ==== >>: %@",[NSThread currentThread]);
}

输出结果:

begin ==== >>: <NSThread: 0x1d0078800>{number = 1, name = main}
end   ==== >>: <NSThread: 0x1d0078800>{number = 1, name = main}
1     ==== >>: <NSThread: 0x1c0073200>{number = 3, name = (null)}
2     ==== >>: <NSThread: 0x1c0073200>{number = 3, name = (null)}
3     ==== >>: <NSThread: 0x1c0073200>{number = 3, name = (null)}

  • 结论:
  1. 【串行+异步】只开启一条新线程,所有任务按照追加顺序依次执行。
  2. 任务执行在end之后,异步执行任务不会阻塞主线程。
3.3 并发队列 + 同步执行
- (void)concurrentAndSync {
    NSLog(@"begin ==== >>: %@",[NSThread currentThread]);
    
    dispatch_queue_t queue = \
    dispatch_queue_create("com.example.gcd", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_block_t block1 = ^{
        sleep(2);
        NSLog(@"1     ==== >>: %@",[NSThread currentThread]);
    };
    dispatch_block_t block2 = ^{
        sleep(1);
        NSLog(@"2     ==== >>: %@",[NSThread currentThread]);
    };
    dispatch_block_t block3 = ^{
        NSLog(@"3     ==== >>: %@",[NSThread currentThread]);
    };
    dispatch_sync(queue, block1);
    dispatch_sync(queue, block2);
    dispatch_sync(queue, block3);
    NSLog(@"end   ==== >>: %@",[NSThread currentThread]);
}

输出结果:

begin ==== >>: <NSThread: 0x1d4074f80>{number = 1, name = main}
1     ==== >>: <NSThread: 0x1d4074f80>{number = 1, name = main}
2     ==== >>: <NSThread: 0x1d4074f80>{number = 1, name = main}
3     ==== >>: <NSThread: 0x1d4074f80>{number = 1, name = main}
end   ==== >>: <NSThread: 0x1d4074f80>{number = 1, name = main}
  • 结论:
  1. 所有任务都在主线程中执行(同步执行不会开启新线程)。
  2. 所有任务按照追加顺序依次执行。
  3. 任务执行在begin和end之间,同步执行任务会阻塞主线程。
3.4 并发队列 + 异步执行
- (void)concurrentAndAsync {
    NSLog(@"begin ==== >>: %@",[NSThread currentThread]);
    
    dispatch_queue_t queue = \
    dispatch_queue_create("com.example.gcd", DISPATCH_QUEUE_CONCURRENT);
    dispatch_block_t block1 = ^{
        sleep(2);
        NSLog(@"1     ==== >>: %@",[NSThread currentThread]);
    };
    dispatch_block_t block2 = ^{
        sleep(1);
        NSLog(@"2     ==== >>: %@",[NSThread currentThread]);
    };
    
    dispatch_block_t block3 = ^{
        NSLog(@"3     ==== >>: %@",[NSThread currentThread]);
    };
    dispatch_async(queue, block1);
    dispatch_async(queue, block2);
    dispatch_async(queue, block3);
    NSLog(@"end   ==== >>: %@",[NSThread currentThread]);
}

输出结果:

begin ==== >>: <NSThread: 0x1d0263e00>{number = 1, name = main}
end   ==== >>: <NSThread: 0x1d0263e00>{number = 1, name = main}
3     ==== >>: <NSThread: 0x1d4269fc0>{number = 3, name = (null)}
2     ==== >>: <NSThread: 0x1c007f140>{number = 4, name = (null)}
1     ==== >>: <NSThread: 0x1c807e740>{number = 5, name = (null)}
  • 结论:
  1. 任务执行在end之后 (不在主线程中执行,开启多条新线程执行)。不会阻塞主线程
  2. 所有任务并发执行,不会等待。
3.5 主队列 + 同步执行
- (void)mainAndSync {
    NSLog(@"begin ==== >>: %@",[NSThread currentThread]);
    
    dispatch_queue_t queue = \
    dispatch_get_main_queue();
    
    dispatch_block_t block1 = ^{
        sleep(2);
        NSLog(@"1     ==== >>: %@",[NSThread currentThread]);
    };
    dispatch_block_t block2 = ^{
        sleep(1);
        NSLog(@"2     ==== >>: %@",[NSThread currentThread]);
    };
    dispatch_block_t block3 = ^{
        NSLog(@"3     ==== >>: %@",[NSThread currentThread]);
    };
    dispatch_sync(queue, block1);
    dispatch_sync(queue, block2);
    dispatch_sync(queue, block3);
    NSLog(@"end   ==== >>: %@",[NSThread currentThread]);
}

输出结果:

begin ==== >>: <NSThread: 0x1d0263e00>{number = 1, name = main}
  • 结论:

发生死锁,任务不会执行。由于主队列为串行队列,主线程在执行mainAndSync 函数,而 mainAndSync 在等待主线程执行结束,就造成了互相等待,均不会执行。(开发中要极力避免这种情况)

3.6 主队列 + 异步执行
- (void)mainAndAsync {
    NSLog(@"begin ==== >>: %@",[NSThread currentThread]);
    
    dispatch_queue_t queue = \
    dispatch_get_main_queue();
    
    dispatch_block_t block1 = ^{
        sleep(2);
        NSLog(@"1     ==== >>: %@",[NSThread currentThread]);
    };
    dispatch_block_t block2 = ^{
        sleep(1);
        NSLog(@"2     ==== >>: %@",[NSThread currentThread]);
    };
    dispatch_block_t block3 = ^{
        NSLog(@"3     ==== >>: %@",[NSThread currentThread]);
    };
    dispatch_async(queue, block1);
    dispatch_async(queue, block2);
    dispatch_async(queue, block3);
    NSLog(@"end   ==== >>: %@",[NSThread currentThread]);
}

输出结果:

begin ==== >>: <NSThread: 0x1d4076040>{number = 1, name = main}
end   ==== >>: <NSThread: 0x1d4076040>{number = 1, name = main}
1     ==== >>: <NSThread: 0x1d4076040>{number = 1, name = main}
2     ==== >>: <NSThread: 0x1d4076040>{number = 1, name = main}
3     ==== >>: <NSThread: 0x1d4076040>{number = 1, name = main}
  • 结论:
  1. 由于主队列为串行队列(不会开启新线程),所有任务都在主线程中执行。
  2. 任务在end之后执行,主队列中开启异步任务,不会开启新线程,会降低异步任务的优先级,在CPU空闲时才会执行该任务。

4. GCD中的 dispatch_set_target_queue 的用法及作用

4.1 更改Dispatch Queue 的执行优先级

通过 dispatch_queue_create 函数生成的GCD队列,不管是 Serial Dispatch Queue 还是 Concurrent Dispatch Queue,其优先级都为系统默认优先级,若想改变创建队列的优先级,则可以使用 dispatch_set_target_queue 函数。

    // 第一个参数为需要更改优先级的queue, 第二个参数为参照队列,
    // 将参照队列的优先级设为目标队列的优先级

dispatch_queue_t queue = \
    dispatch_queue_create("com.example.gcd", DISPATCH_QUEUE_SERIAL);
    
    dispatch_queue_t globalQueue = \
    dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    
    dispatch_set_target_queue(queue, globalQueue);
4.2 设置多个 Dispatch Queue 的层级

由上我们知道,通过 dispatch_queue_create 函数创建的 Serial Dispatch Queue 任务执行是同步的,同时只能执行一个任务,虽然GCD队列受到系统资源的限制,但通过 dispatch_queue_create 函数可以生成任意多个 Dispatch Queue

当生成多个 Serial Dispatch Queue时,各个 Serial Dispatch Queue将并行执行。

如下代码:

- (void)setTargetQueue {
    
    dispatch_queue_t queue1 = dispatch_queue_create("com.example.gcd.queue1", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queue2 = dispatch_queue_create("com.example.gcd.queue2", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queue3 = dispatch_queue_create("com.example.gcd.queue3", DISPATCH_QUEUE_SERIAL);
    
    dispatch_async(queue1, ^{
        NSLog(@"1 ==== >>: %@",[NSThread currentThread]);
        sleep(3);
        NSLog(@"2 ==== >>: %@",[NSThread currentThread]);
    });

    dispatch_async(queue2, ^{
        NSLog(@"3 ==== >>: %@",[NSThread currentThread]);
        sleep(2);
        NSLog(@"4 ==== >>: %@",[NSThread currentThread]);
    });

    dispatch_async(queue3, ^{
        NSLog(@"5 ==== >>: %@",[NSThread currentThread]);
        sleep(1);
        NSLog(@"6 ==== >>: %@",[NSThread currentThread]);
    });
}

执行结果:

3 ==== >>: <NSThread: 0x1d0066180>{number = 5, name = (null)}
5 ==== >>: <NSThread: 0x1d4468e80>{number = 4, name = (null)}
1 ==== >>: <NSThread: 0x1d0066700>{number = 3, name = (null)}
6 ==== >>: <NSThread: 0x1d4468e80>{number = 4, name = (null)}
4 ==== >>: <NSThread: 0x1d0066180>{number = 5, name = (null)}
2 ==== >>: <NSThread: 0x1d0066700>{number = 3, name = (null)}

多次运行发现: 其中1、3、5的输出顺序不固定,多个任务并发执行。

假如此时我想让多个同步队列中的任务还是依次同步执行,或者让多个并发队列中的任务同步执行,我该怎么办呢?对,使用 dispatch_set_target_queue ,将三个队列指定到同一串行目标队列上,此时多个队列任务就会同步执行,不再是并发执行了。

如下代码:

- (void)setTargetQueue {
    
    dispatch_queue_t queue1 = dispatch_queue_create("com.example.gcd.queue1", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue2 = dispatch_queue_create("com.example.gcd.queue2", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue3 = dispatch_queue_create("com.example.gcd.queue3", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_queue_t targetQueue = dispatch_queue_create("com.example.gcd.targetQueue", DISPATCH_QUEUE_SERIAL);

    //指定到同一串行队列
    dispatch_set_target_queue(queue1, targetQueue);
    dispatch_set_target_queue(queue2, targetQueue);
    dispatch_set_target_queue(queue3, targetQueue);
    
    dispatch_async(queue1, ^{
        NSLog(@"1 ==== >>: %@",[NSThread currentThread]);
        sleep(3);
        NSLog(@"2 ==== >>: %@",[NSThread currentThread]);
    });

    dispatch_async(queue2, ^{
        NSLog(@"3 ==== >>: %@",[NSThread currentThread]);
        sleep(2);
        NSLog(@"4 ==== >>: %@",[NSThread currentThread]);
    });

    dispatch_async(queue3, ^{
        NSLog(@"5 ==== >>: %@",[NSThread currentThread]);
        sleep(1);
        NSLog(@"6 ==== >>: %@",[NSThread currentThread]);
    });
}

执行结果:

1 ==== >>: <NSThread: 0x1d02602c0>{number = 3, name = (null)}
2 ==== >>: <NSThread: 0x1d02602c0>{number = 3, name = (null)}
3 ==== >>: <NSThread: 0x1d02602c0>{number = 3, name = (null)}
4 ==== >>: <NSThread: 0x1d02602c0>{number = 3, name = (null)}
5 ==== >>: <NSThread: 0x1d02602c0>{number = 3, name = (null)}
6 ==== >>: <NSThread: 0x1d02602c0>{number = 3, name = (null)}

在必须将不可并行执行的多个任务追加到多个 Serial Dispatch Queue 中时,可使用 dispatch_set_target_queue函数,防止任务并发执行。

注: 一旦生成 Serial Dispatch Queue 并追加任务处理,系统对于一个 Serial Dispatch Queue 
就会生成一条线程。假如生成 1000个 Serial Dispatch Queue ,那么就生成了 1000 条线程。
此时会大量消耗内存,大幅度降低系统的响应性能。

5. GCD时间: dispatch_time_t

dispatch_time_t 为GCD的时间类,用于获取距离某个目标时间间隔的时间

计算相对时间:

   dispatch_time_t time = \
    dispatch_time(<#dispatch_time_t when#>, <#int64_t delta#>)

//第一个参数为某个目标时间,常用 DISPATCH_TIME_NOW,表示现在时间。
//第二个参数表示具体的时间长度,不能直接传 int或者float,需 (int64_t)3* NSEC_PER_SEC 这种形式。

注: delta单位是纳秒!

NSEC_PER_SEC  1000000000ull  每秒有1000000000纳秒
NSEC_PER_MSEC 1000000ull     每毫秒有1000000纳秒
USEC_PER_SEC  1000000ull     每秒有1000000微秒
NSEC_PER_USEC 1000ull        每微秒有1000纳秒

"ull"是C语言的数值字面量,是显示表明类型时使用的字符串(表示“unsigned long long”)。

例如 dispatch_time(DISPATCH_TIME_NOW, 3*NSEC_PER_SEC) 表示距离当前时间 3秒后的时间。
计算绝对时间:

dispatch_time_t time = \
    dispatch_walltime(<#const struct timespec * _Nullable when#>, <#int64_t delta#>)
    
/* 第一个参数是一个 timespec 的结构体, 计算的是一个绝对的时间点,比如 2016年10月10日8点30分30秒, 
如果你不需要自某一个特定的时刻开始,可以传 NUll,表示自动获取当前时区的当前时间作为开始时刻,
第二个参数同上。*/

例如 dispatch_walltime(NULL, 3*NSEC_PER_SEC)表示距离当前时间 3秒后的时间。

struct timespec 类型的时间可以通过NSDate对象生成:

// 通过指定日期获取一个 dispatch_time_t 对象
- (dispatch_time_t)getDispatchTimeByDate:(NSDate *)date {
    NSTimeInterval interval;
    double second, subsecond;
    struct timespec time;
    dispatch_time_t milestone = 0;
    
    interval = [date timeIntervalSince1970];
    subsecond = modf(interval, &second);
    time.tv_sec = second;
    time.tv_nsec = dispatch_walltime(&time, 0);
    return milestone;
}

6. GCD延时操作: dispatch_after

我们可能经常会有这样的需求,想在3秒后执行某项操作,可能不限于3秒,总之,这种想在指定时间后执行操作的情况,可用 dispatch_after 来实现。

例如:

//第一个参数是 dispatch_time_t 的值(详见5)。
//第二个参数是执行的 Dispatch Queue 。
//第三个参数是要追加的操作。

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3*NSEC_PER_SEC);
    
    dispatch_after(time, dispatch_get_main_queue(), ^{
        NSLog(@"Three seconds later...");
    });

注: dispatch_after 函数并不是在指定的时间过后执行操作,而只是在指定时间后追加操作到 Dispatch Queue 中。

因为 Mian Dispatch Queue 在主线程的RunLoop 中执行,假如主线程的刷新频率为 60 帧,则追加的操作最快在3秒后执行,最慢则在 3 + 1/60 秒后执行,并且假如在Mian Dispatch Queue 中有大量操作要执行或者主线程本身有延迟的话,这个时间会更长。

7. Dispatch Group

7.1 dispatch_group_notify

假如在追加到 Dispatch Queue 中的多个操作全部结束后想要执行某项操作,假如只使用一个 Serial Dispatch Queue时,只需将结束操作追加到所有任务的最后即可实现。但是在使用 Concurrent Dispatch Queue或同时使用多个 Dispatch Queue时,实现起来就比较复杂了。

这时我们就可以使用 Dispatch Group 。下面我们来看一段代码:

- (void)dispatchGroup {
    NSLog(@"begin  ==== >>: %@",[NSThread currentThread]);

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_async(group, queue, ^{
        NSLog(@"Task 1 ==== >>: %@",[NSThread currentThread]);
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"Task 2 ==== >>: %@",[NSThread currentThread]);
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"Task 3 ==== >>: %@",[NSThread currentThread]);
    });
    dispatch_group_notify(group, queue, ^{
        NSLog(@"Done!  ==== >>: %@",[NSThread currentThread]);
    });
    NSLog(@"end    ==== >>: %@",[NSThread currentThread]);
}

执行结果:

begin  ==== >>: <NSThread: 0x1d006f940>{number = 1, name = main}
end    ==== >>: <NSThread: 0x1d006f940>{number = 1, name = main}
Task 2 ==== >>: <NSThread: 0x1d0473700>{number = 3, name = (null)}
Task 3 ==== >>: <NSThread: 0x1c806cd00>{number = 4, name = (null)}
Task 1 ==== >>: <NSThread: 0x1c406c740>{number = 5, name = (null)}
Done!  ==== >>: <NSThread: 0x1c806cd00>{number = 4, name = (null)}

因为Global Dispatch Queue 为 Concurrent Dispatch Queue ,多个线程并发执行任务,所以任务执行顺序不定,但是执行结果 Done 一定是最后执行的。无论使用的是什么类型的 Dispatch Queue,Dispatch Group 都可以监测到这些任务执行的结束。

通过 dispatch_group_create() 函数生成 Dispatch Group .

dispatch_group_asyncdispatch_async 相同,都是追加 Block操作到指定 Dispatch Queue中。不同的是 dispatch_group_async 多了一个 Group 参数。

7.2 dispatch_group_wait

如上情况我们也可以使用 dispatch_group_wait 函数去实现,如下代码:

- (void)dispatchGroup {
    NSLog(@"begin  ==== >>: %@",[NSThread currentThread]);

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_async(group, queue, ^{
        sleep(3);
        NSLog(@"Task 1 ==== >>: %@",[NSThread currentThread]);
    });
    dispatch_group_async(group, queue, ^{
        sleep(1);
        NSLog(@"Task 2 ==== >>: %@",[NSThread currentThread]);
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"Task 3 ==== >>: %@",[NSThread currentThread]);
    });
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    NSLog(@"Done!  ==== >>: %@",[NSThread currentThread]);
    
    NSLog(@"end    ==== >>: %@",[NSThread currentThread]);
}

执行结果:

begin  ==== >>: <NSThread: 0x1d4262fc0>{number = 1, name = main}
Task 3 ==== >>: <NSThread: 0x1d026f040>{number = 3, name = (null)}
Task 2 ==== >>: <NSThread: 0x1c007ff40>{number = 4, name = (null)}
Task 1 ==== >>: <NSThread: 0x1cc07fe80>{number = 5, name = (null)}
Done!  ==== >>: <NSThread: 0x1d4262fc0>{number = 1, name = main}
end    ==== >>: <NSThread: 0x1d4262fc0>{number = 1, name = main}

从结果我们可以看出 dispatch_group_wait一样可以达到我们想要的结果。但是不同之处也非常明显,dispatch_group_wait 会阻塞当前线程。

另外 dispatch_group_wait 函数可以指定等待的时间,传入 DISPATCH_TIME_FOREVER 则表示永远等待,直到所有任务执行结束。且该函数还有 long 型的返回值,返回0则表示所有任务都执行结束。

如我们修改代码如下:

long result = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 2*NSEC_PER_SEC));
    
    if (result == 0) {
        //所有任务都执行结束
        NSLog(@"Done!  ==== >>: %@",[NSThread currentThread]);
    }else {
        //有任务尚未结束
        NSLog(@"Not Done!  ==== >>: %@",[NSThread currentThread]);
    }

执行结果为:

begin  ==== >>: <NSThread: 0x1d0264f40>{number = 1, name = main}
Task 3 ==== >>: <NSThread: 0x1d066e980>{number = 3, name = (null)}
Task 2 ==== >>: <NSThread: 0x1cc07f9c0>{number = 4, name = (null)}
Not Done!  ==== >>: <NSThread: 0x1d0264f40>{number = 1, name = main}
end    ==== >>: <NSThread: 0x1d0264f40>{number = 1, name = main}
Task 1 ==== >>: <NSThread: 0x1c807fc80>{number = 5, name = (null)}

由上我们可以看出 dispatch_group_notifydispatch_group_wait 两个函数的异同之处。在开发过程中可根据具体情况选择使用。

7.3 dispatch_group_enterdispatch_group_leave

这两个函数配对使用,表示 Dispatch Group 开启任务和结束任务。这两种通知可以在多线程间自由穿梭,不局限于特定的某个线程。

dispatch_group_enter: 通知group,下面的任务马上要放到group中执行了。
dispatch_group_leave: 通知group,任务完成了,该任务要从group中移除了。

猜测: 可能 Dispatch Group 内部有着一个类似引用计数的存在,调用 dispatch_group_enter 会加一,调用 dispatch_group_leave会减一,当该值为0时,则调用 dispatch_group_notifydispatch_group_wait 函数。

当我们异步开启一个任务,并不指定特定的队列及线程时,可使用这两个函数去通知 Dispatch Group 任务的开启和结束。

如下代码:

- (void)dispatchGroup {
    NSLog(@"begin  ==== >>: %@",[NSThread currentThread]);

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();

    [self downloadImaegWithUrl:@"http://pic10.photophoto.cn/20090224/0036036802407491_b.jpg" InGroup:group];
    [self downloadImaegWithUrl:@"http://pic25.photophoto.cn/20121220/0036036800277861_b.jpg" InGroup:group];
    [self downloadImaegWithUrl:@"http://img.taopic.com/uploads/allimg/140806/235020-140P60H10661.jpg" InGroup:group];

    dispatch_group_notify(group, queue, ^{
        NSLog(@"Done!  ==== >>: %@",[NSThread currentThread]);
    });
    NSLog(@"end    ==== >>: %@",[NSThread currentThread]);
}

//利用 SDWebImage异步下载图片
- (void)downloadImaegWithUrl:(NSString *)url InGroup:(dispatch_group_t)group{
    dispatch_group_enter(group);//开启任务
    [[SDWebImageDownloader sharedDownloader] downloadImageWithURL:[NSURL URLWithString:url] options:(SDWebImageDownloaderContinueInBackground) progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) {
        NSLog(@"下载完成! ====>>: %@",[NSThread currentThread]);
        dispatch_group_leave(group);//结束任务
    }];
}

执行结果:

begin  ==== >>: <NSThread: 0x1d406ee00>{number = 1, name = main}
end    ==== >>: <NSThread: 0x1d406ee00>{number = 1, name = main}
下载完成! ====>>: <NSThread: 0x1d406ee00>{number = 1, name = main}
下载完成! ====>>: <NSThread: 0x1d406ee00>{number = 1, name = main}
下载完成! ====>>: <NSThread: 0x1d406ee00>{number = 1, name = main}
Done!  ==== >>: <NSThread: 0x1cc069cc0>{number = 3, name = (null)}

8. GCD栅栏 (dispatch_barrier_sync)

当我们在访问数据库或者文件时,使用 Serial Dispatch Queue 可避免数据竞争导致程序异常的问题(多个写入操作不可并行执行)。但是这样的话,程序的执行效率就比较低。

为了高效率的进行访问,我们使用 Concurrent Dispatch Queue,并使用 dispatch_barrier_sync函数解决数据竞争问题。

dispatch_barrier_sync俗称栅栏,顾名思义,即可以将某些操作隔绝开来,互不影响。

假如我想在4次读取操作中间加入2次写入操作, 如下代码:

- (void)dispatchBarrier {
    
    dispatch_queue_t queue = dispatch_queue_create("com.gcd.example", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"reading1 ===>>: %@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"reading2 ===>>: %@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        //写入操作
        NSLog(@"writing1 ===>>: %@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        //写入操作
        NSLog(@"writing2 ===>>: %@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"reading3 ===>>: %@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"reading4 ===>>: %@",[NSThread currentThread]);
    });
}

Concurrent Dispatch Queue 的性质我们可知,4次读取和2次写入均会并发执行,先后顺序不定,这样必然会导致数据竞争,出现异常。

修改代码: 我们只需将写入操作替换成 dispatch_barrier_sync 函数即可解决该问题。

dispatch_barrier_sync(queue, ^{
        //写入操作
        NSLog(@"writing1 ===>>: %@",[NSThread currentThread]);
    });
dispatch_barrier_sync(queue, ^{
        //写入操作
        NSLog(@"writing2 ===>>: %@",[NSThread currentThread]);
    });

dispatch_barrier_sync 函数会等待追加到 Dispatch Queue 上的并行任务全部执行结束之后,再将指定的操作追加到 Dispatch Queue 中。该指定的操作执行完毕后,Dispatch Queue 才会恢复正常操作。开始处理其他任务。

因此修改后的代码执行顺序为 reading1和reading2并发执行在前,之后为 writing1 和 writing2 串行执行,最后 reading3和reading4并发执行。

9. GCD信号量 (dispatch_semaphore_t)

信号量(dispatch_semaphore_t)可以理解为有信号通过,无信号则等待。具体的我们慢慢来分析。

与信号量有关的主要有三个函数:

dispatch_semaphore_create(<#long value#>)
//创建一个信号量,传入一个long型的信号值.
//传入信号值为大于或者等于0的值,否则将返回 NULL.
dispatch_semaphore_wait(<#dispatch_semaphore_t  _Nonnull dsema#>, <#dispatch_time_t timeout#>)
//与 dispatch_group_wait(详见7.2)类似,会阻塞当前线程,并有返回值.
//当当前信号量等于0时,持续等待.
//当当前信号量大于0时,执行返回,并将当前信号量减一.
dispatch_semaphore_signal(<#dispatch_semaphore_t  _Nonnull dsema#>)
//该函数会将当前信号量加一.

我理解的信号量主要有三个作用:线程同步线程锁控制并发数.

9.1 线程同步

先看代码:

__block int count = 0;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        count = 10;
    });
    NSLog(@"%d",count);

由异步并发执行我们可知,此时输出的count值为 0。假如我想让主线程保持与任务异步线程一致,输出 count 值为10,我该怎么做呢?

修改代码:

dispatch_semaphore_t semphore = dispatch_semaphore_create(0);
    __block int count = 0;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        count = 10;
        dispatch_semaphore_signal(semphore);
    });
    dispatch_semaphore_wait(semphore, DISPATCH_TIME_FOREVER);
    NSLog(@"%d",count);

分析:
我们初始化了一个值为0信号量,执行 dispatch_semaphore_wait 函数时发现信号量为0,则会等待信号,执行完 count = 10 后,通过 dispatch_semaphore_signal 函数获得一个信号量,此时 dispatch_semaphore_wait 监测到信号量大于0,则执行返回,线程继续执行,输出 count 为10。这就保证了线程间的同步了。

9.2 线程锁

假如我想异步并发执行1000次任务,给同一块内存区间赋值,会发生什么情况呢?
先看代码:

NSMutableArray *temArr = @[].mutableCopy;
    for (int i = 0; i < 1000; i++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [temArr addObject:[NSString stringWithFormat:@"%d",i]];
        });
    }

运行会发现程序异常崩溃的概率相当高,这是因为我们异步并发的操作同一内存导致数据错乱引起的异常。

使用线程锁修改代码:

dispatch_semaphore_t semphore = dispatch_semaphore_create(0);
    NSMutableArray *temArr = @[].mutableCopy;
    for (int i = 0; i < 1000; i++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            //等待信号
            dispatch_semaphore_wait(semphore, DISPATCH_TIME_FOREVER);
            //到这里,信号量会减一
            [temArr addObject:[NSString stringWithFormat:@"%d",i]];
            dispatch_semaphore_signal(semphore);
            //信号量加一
        });
    }

通过这种方法,保证了同一时间只有一个操作去改变内存。当然在实际操作中我们不会这样去for循环使用,通常是设置一个全局的 dispatch_semaphore_t 对象,将需要加锁的操作放在,dispatch_semaphore_wait 和 dispatch_semaphore_signal 函数中间。保证唯一性。

例如:

@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) dispatch_semaphore_t semaphore;

self.semaphore = dispatch_semaphore_create(1);

- (void)setName:(NSString *)name {
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    _name = name;
    dispatch_semaphore_signal(self.semaphore);
}
9.2 控制并发数

当我们在处理大量线程时,怎么来进行并发控制呢,假如我有1000个任务要执行,但是同时执行的并发数我想控制在10个,该怎么做呢?以前我们使用NSOperationQueue可以控制并发数,在GCD中怎么简单快速的控制并发呢?那就是使用 dispatch_semaphore_t.

先看代码:

- (void)dispatchSemaphore {

    dispatch_group_t group = dispatch_group_create();
    
    dispatch_semaphore_t semphore = dispatch_semaphore_create(10);
    
    for (int i = 0; i < 100; i++) {
        dispatch_semaphore_wait(semphore, DISPATCH_TIME_FOREVER);
        
        dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSLog(@"%d ===>>: %@",i,[NSThread currentThread]);
            sleep(2);
            dispatch_semaphore_signal(semphore);
        });
    }
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    NSLog(@"end");
}

首先我们创建了一个值为10的信号量,每次循环会减少一个信号量,执行结束会增加一个信号量,当执行操作为10个时,此时信号量为0,dispatch_semaphore_wait 函数将会等待,如此则保证了最多同时有10个任务在执行。由此看出,通过 dispatch_semaphore_t 来控制并发数简单快速又实用。

10. dispatch_suspend / dispatch_resume

当追加大量的处理任务到 Dispatch Queue 时,有时候希望不执行某些已经追加的任务,在这种情况下,只需要挂起 Dispatch Queue即可,当可以执行时再恢复。

  • dispatch_suspend : 挂起指定的 Dispatch Queue。可理解为暂停执行。
  • dispatch_resume : 恢复指定的 Dispatch Queue。可理解为继续执行。

值得注意的是,dispatch_suspend 对于正在执行的任务是没有影响的,挂起后,只有 Dispatch Queue 中尚未执行的任务停止执行。而恢复则使得这些任务能够继续执行。

11. GCD快速遍历 (dispatch_apply)

dispatch_apply 函数是dispatch_sync函数和 Dispatch Group的关联API。该函数按指定次数将指定的 Block 追加到指定的 Dispatch Queue中,并等待全部处理执行结束。

如下代码:

- (void)dispatchApply {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    dispatch_apply(10, queue, ^(size_t index) {
        NSLog(@"%zu",index);
    });
    NSLog(@"end");
}

执行结果:

0
2
4
3
5
6
7
8
9
1
end

因为在 Global Dispatch Queue 中执行操作,10个任务并发执行,但是最后的end一定在最后的位置上。因为 dispatch_apply 会等待全部任务执行结束。

另外,由于dispatch_apply 函数与 dispatch_sync函数一样,会等待任务结束,阻塞当前线程,因此推荐在 dispatch_async 函数中异步执行 dispatch_apply 函数。

例如:

- (void)dispatchApply {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //异步执行
    dispatch_async(queue, ^{
        dispatch_apply(10, queue, ^(size_t index) {
            NSLog(@"%zu",index);
        });
        
        //等待处理结束
        dispatch_async(dispatch_get_main_queue(), ^{
            //主线程回调,刷新UI
            NSLog(@"end");
        });
    });
    NSLog(@"main");
}

12. GCD单例 (dispatch_once)

dispatch_once函数是保证在应用程序执行中只执行一次指定处理的API。

下面这种经常出现的用来进行初始化的代码可通过 dispatch_once 函数来简化。

static int count = 0;
    if (count == 0) {
        //在这里初始化
        count = 100;
    }

使用 dispatch_once 函数:

static int count = 0;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //在这里初始化
        count = 100;
    });

代码看起来并没有太大的变化。但是通过 dispatch_once 函数,该代码即使在复杂的多线程环境下执行,也可保证百分之百安全。之前的代码在大多数情况下也是安全的。但是在多核CPU中,读取数据时,有可能会多次执行初始化处理。而用 dispatch_once 函数就不必担心这样的问题。这就是所说的单例模式,在生成单例对象时使用。


以上是我对于GCD简单理解之后的总结,若有不正确之处请评论指正,我会尽快验证修改!

                                ------来自一只菜鸟iOS程序猿。

相关文章

网友评论

本文标题:Objective-C多线程开发之GCD(最全总结)

本文链接:https://www.haomeiwen.com/subject/nuyojftx.html