欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

iOS GCD

程序员文章站 2022-04-14 07:50:02
...

GCD简介

GCD 是 Apple 开发的一个多核编程的解决方法,简单易用,效率高,速度快。通过 GCD,开发者只需要向队列中添加一段代码块(block或C函数指针),而不需要直接和线程打交道。GCD在后端管理着一个线程池,它不仅决定着你的代码块将在哪个线程被执行,还根据可用的系统资源对这些线程进行管理。这样通过GCD来管理线程,从而解决线程被创建的问题。


GCD基本概念

任务和队列

名词 说明
线程 程序执行任务的最小调度单元
任务 一段代码:GCD中,block中需要执行的code
队列 存放任务的数组,FIFO(先进先出)的原则

异步、同步,并行和串行

名词 说明
异步(async) 具备开新线程的能力
同步(sync) 不具备开新线程的能力
并行 任务可以并发执行
串行 任务按顺序执行

说明:使用GCD开启多线程执行多个任务时,需具备两个条件:

1、能开启新线程
2、任务可以同时执行

即:"异步"+"并行"


GCD几种组合

- 并行队列 串行队列 主队列
异步 开启新线程,任务同时执行(1) 开启新线程,任务顺序执行(2) 不开新线程,任务顺序执行(5)
同步 不开启新线程,任务顺序执行(3) 不开启新线程,任务顺序执行(4) 主线程中:死锁,子线程中:不开启新线程,任务顺序执行(6)

详细说明:
(1)异步使得队列开启了新的线程,并行队列让任务可以同时执行(常用)
(2)虽然开启了新线程,但是队列调度方式是串行的,因此任务只能顺序执行
(3)同步意味着不能开启新线程,虽然是并行队列,但线程只有一个,因此任务只能顺序执行
(4)不能开新线程,任务队列是串行,任务顺序执行

总结:正如上面说到,使用GCD完成多线程多任务时,需要具备两个能力:开启新线程的能力,任务可以同时执行的能力,即”异步”+”并行”。两者缺一不可,缺少任何一个,队列里的任务都是顺序执行。

接下来说明一下主队列:主队列其实是一个串行队列

(5)主队列里的任务都是在主线程中完成的,即使使用异步(async),也不会开启新线程,并且主队列是一个串行队列,任务顺序执行。
(6)在主线程中出现死锁,是因为任务被加到主队列中,想要被执行block中的代码必须等到主线程上的任务都执行完毕,但是,因为是同步任务,想要主线程上的任务执行完毕,势必需要执行任务中的block中的代码,因此两者相互等待,出现死锁;但是如果在子线程中添加同步任务,并不会阻塞主线程上的任务执行完毕,因此结果会和”同步”+”串行”一致。


GCD的基本使用

GCD使用步骤分两步:

1、获取一个队列
2、将任务添加到队列中

系统会自动调度任务,通常是FIFO(先来先服务)

1、获取队列

// DISPATCH_QUEUE_SERIAL 串行
// DISPATCH_QUEUE_CONCURRENT 并行
dispatch_queue_create("队列标识符",队列类型);

我们可以使用dispatch_queue_create来创建队列,队列类型有两种:DISPATCH_QUEUE_SERIAL串行队列,DISPATCH_QUEUE_CONCURRENT并行队列

GCD为我们提供了两种快捷获取队列的方式,一个是主列队(串行队列),一种是全局队列(并行队列)

1.1、获取主队列

dispatch_get_main_queue();

主队列中任务都会放到主线程中执行,并且是顺序执行

1.2、获取全局并发队列

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、创建任务的方法

GCD为我们提供了创建同步和异步的方法,分别是dispatch_syncdispatch_async,其中第一个参数是队列,第二个是需要执行的block块,我们的任务就放在这里

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

GCD线程通信

在开发过程中,我们通常将一些耗时的操作放在子线程,如数据请求,文件下载等,当这些任务完成后,我们需要即使的更新到UI上,这时我们就需要回到主线程

//获取【并行】队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 创建【异步】任务
dispatch_async(queue, ^{
    // do something ...
    [NSThread sleepForTimeInterval:2.0];
    // 回到主线程
    dispatch_queue_t main = dispatch_get_main_queue();
    dispatch_async(main, ^{
        // 更新UI
    });
});

整个过程如上述代码所示,我们获取并行队列,创建异步任务,但完成耗时操作后,再获取到主线程,将更新UI的任务添加到主线程上去。

dispatch_queue_t main = dispatch_get_main_queue();
dispatch_async(main, ^{
    // 更新UI
});

验证组合

(1)【异步】+【并行】

NSLog(@"current thread:%@",[NSThread currentThread]);
// 创建【并行】队列
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
// 开启多个【异步】任务
dispatch_async(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"1:%@",[NSThread currentThread]);
    }
});
dispatch_async(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"2:%@",[NSThread currentThread]);
    }
});
dispatch_async(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"3:%@",[NSThread currentThread]);
    }
});
NSLog(@"end");

iOS GCD

我们发现,”end”在线程输出之前,说明当前线程并未等待,而是开启了新的线程执行任务(3、4、5号线程);任务1、2、3交替完成,说明并发队列在同时执行多个任务

(2)【异步】+【串行】

我们将【并行】队列该为【串行】队列

NSLog(@"current thread:%@",[NSThread currentThread]);
// 创建【串行】队列
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
// 开启多个【异步】任务
dispatch_async(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"1:%@",[NSThread currentThread]);
    }
});
dispatch_async(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"2:%@",[NSThread currentThread]);
    }
});
dispatch_async(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"3:%@",[NSThread currentThread]);
    }
});
NSLog(@"end");

iOS GCD

我们发现,虽然开启了新的线程,但是队列是一个串行队列,因此任务是顺序执行的:1->2->3,另外,我们注意到,串行队列下,dispatch_async只会开启一个线程

(3)【同步】+【并行】

我们再稍稍改动代码,从而产生并行队列,同步任务

NSLog(@"current thread:%@",[NSThread currentThread]);
// 创建【并行】队列
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
// 开启多个【同步】任务
dispatch_sync(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"1:%@",[NSThread currentThread]);
    }
});
dispatch_sync(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"2:%@",[NSThread currentThread]);
    }
});
dispatch_sync(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"3:%@",[NSThread currentThread]);
    }
});
NSLog(@"end");

iOS GCD

我们可以看到,虽然是并行队列,但是dispatch_sync同步的条件使得任务并没有开启新的线程,而是在当前线程(例子中是主线程)执行,并且按照顺序执行,另外,我们可以看到,”end”是在最后才输出,这就是同步的原因

(4)【同步】+【串行】

NSLog(@"current thread:%@",[NSThread currentThread]);
// 获取【串行】队列
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
// 开启多个【同步】任务
dispatch_sync(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"1:%@",[NSThread currentThread]);
    }
});
dispatch_sync(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"2:%@",[NSThread currentThread]);
    }
});
dispatch_sync(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"3:%@",[NSThread currentThread]);
    }
});
NSLog(@"end");

iOS GCD

我们发现,同步+串行和同步+并行的结果是一致的。

最后,我们来看下,比较特殊的串行队列的主队列

(5)【异步】+【主队列】

NSLog(@"current thread:%@",[NSThread currentThread]);
// 获取主队列
dispatch_queue_t queue = dispatch_get_main_queue();
// 开启多个【异步】任务
dispatch_async(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"1:%@",[NSThread currentThread]);
    }
});
dispatch_async(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"2:%@",[NSThread currentThread]);
    }
});
dispatch_async(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"3:%@",[NSThread currentThread]);
    }
});
NSLog(@"end");

iOS GCD

我们发现,结果似乎和【异步】+【串行】一样,其实主队列就是一种串行队列,不同的是,主队列并不会开启新的线程,所有的任务都会放在主线程中完成,并且服从FIFO先来先服务的原则

(6)【同步】+【主队列】

NSLog(@"current thread:%@",[NSThread currentThread]);
// 获取主队列
dispatch_queue_t queue = dispatch_get_main_queue();
// 开启多个【同步】任务
dispatch_sync(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"1:%@",[NSThread currentThread]);
    }
});
dispatch_sync(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"2:%@",[NSThread currentThread]);
    }
});
dispatch_sync(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"3:%@",[NSThread currentThread]);
    }
});
NSLog(@"end");

iOS GCD

呃呃。。。我们发现,在主线程中,【同步】+【主队列】的方式发生了死锁,这个是因为dispatch_sync将任务添加到主队列中,任务block部分需要等待主线程上的任务执行完毕之后才会执行,但是由于dispatch_sync会阻塞当前线程,直到之前的任务都完成才会继续执行,这导致主线程的任务永不能完成,任务block里的代码也用不能被执行,从而产生了死锁

既然dispatch_sync会阻塞当前线程,那我们将其放在子线程中会怎么样呢?

我们开启一个子线程测试

[NSThread detachNewThreadSelector:@selector(mainThreadVSSync) toTarget:self withObject:nil];

在子线程任务中,重新执行【同步】+【主队列】任务

-(void)mainThreadVSSync{
    NSLog(@"current thread:%@",[NSThread currentThread]);
    // 获取主队列
    dispatch_queue_t queue = dispatch_get_main_queue();
    // 开启多个【同步】任务
    dispatch_sync(queue, ^{
        for (int i=0; i<2; i++) {
            [NSThread sleepForTimeInterval:1.0];
            NSLog(@"1:%@",[NSThread currentThread]);
        }
    });
    dispatch_sync(queue, ^{
        for (int i=0; i<2; i++) {
            [NSThread sleepForTimeInterval:1.0];
            NSLog(@"2:%@",[NSThread currentThread]);
        }
    });
    dispatch_sync(queue, ^{
        for (int i=0; i<2; i++) {
            [NSThread sleepForTimeInterval:1.0];
            NSLog(@"3:%@",[NSThread currentThread]);
        }
    });
    NSLog(@"end");
}

iOS GCD

我们发现,程序可以继续执行。我们使用【同步】+【主队列】时,线程不在是主线程,而是3号线程,这样dispatch_sync开启同步任务时,并不会影响到主线程,同步任务可以继续执行,只不过都是在主线程中,而且是顺序执行


总结

1、GCD方式实现多线程多任务必须是【异步】+【并行队列】,缺一不可,其他方式的任务都是顺序执行的,无论异步还是同步

2、主队列是一种串行队列,切忌在主线程中使用【同步】+【主队列】的方式开启任务,会出现死锁


GCD其他

1、队列组:dispatch_group和dispatch_group_notify

有时,我们需要开启多个异步任务,并且所有任务都结束之后,再回到主线程执行任务,那么该如何做呢?这里我们就需要dispatch_group了

步骤:创建group->关联任务、队列、group->接受group通知

a、我们通过dispatch_group_create()创建一个队列组

b、使用dispatch_group_async方法,将任务放在队列中,队列则关联到group

c、接收完成通知dispatch_group_notify

// 创建一个队列组
dispatch_group_t group = dispatch_group_create();
// 获取一个【并行】队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 使用队列组发起一个耗时任务
dispatch_group_async(group, queue, ^{
    for (int i = 0; i < 2; ++i) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"1:%@",[NSThread currentThread]);
    }
});
// 另外一个耗时任务
dispatch_group_async(group, queue, ^{
    for (int i = 0; i < 2; ++i) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"2:%@",[NSThread currentThread]);
    }
});
// 完成通知,在主队列中完成UI更新
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"更新UI操作");
    NSLog(@"%@",[NSThread currentThread]);
});

iOS GCD

我们可以看到,只有当多线程中多任务完成后,dispatch_group_notify中更新UI的操作才会被执行

2、dispatch_group_wait

当方法会阻塞当前线程,等待指定当group中当任务执行完成后,才会继续执行

1中的例子,也可以使用该方法达到同样的效果

// 创建一个队列组
    dispatch_group_t group = dispatch_group_create();
    // 获取一个【并行】队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    // 使用队列组发起一个耗时任务
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:1.0];
            NSLog(@"1:%@",[NSThread currentThread]);
        }
    });
    // 另外一个耗时任务
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:1.0];
            NSLog(@"2:%@",[NSThread currentThread]);
        }
    });
    // 阻塞当前线程
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    NSLog(@"更新UI操作");
    NSLog(@"%@",[NSThread currentThread]);

iOS GCD

但是,需要注意的是,dispatch_group_wait是会阻塞当前线程的,而dispatch_group则不会

3、延迟执行:dispatch_after

我们可以使用GCD快速的创建一个延迟执行的任务。当然,由于是添加到主队列的中的,因此这个延迟的时间是不准确的,这里还包括了队列前的任务时长

NSLog(@"执行前:%@",[NSThread currentThread]);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"执行时:%@",[NSThread currentThread]);
});

iOS GCD

我们发现,时差为2.2的左右,是大于我们所设的2.0秒的

4、一次性代码:dispatch_once

GCD可以创建一次性代码,在制作单例时,我们常常使用到它dispatch_once,该函数可以保证某段代码在程序中只执行1次,并且在多线程的环境下,也可以保证线程安全

-(void)onceTask{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 一次性任务
    });
}

5、快速迭代方法:dispatch_apply

通常我们会使用for循环遍历数组,GCD中的dispatch_apply提供了类似的方法,不同的是,dispatch_apply不仅可以是顺序的遍历,还可以是并发的遍历,主要看队列是串行的还是并行的

NSLog(@"apply---begin");
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, queue, ^(size_t index) {
    NSLog(@"%zd---%@",index, [NSThread currentThread]);
});
NSLog(@"apply---end");

iOS GCD

我们发现遍历并不是顺序执行的,如果使用的谁串行队列,和使用for循环遍历是一样的效果

6、信号量:dispatch_semaphore

信号量类似生活当中的信号灯,红灯停,绿灯行。GCD中的信号量Dispatch Semaphore是持有计数的信号,计数为0时等待,不可通行,计数为1或者大于1时,计数减1且允许通过。

dispatch_semaphore有三个函数,分别用来创建信号量,增加计数量

dispatch_semaphore_create:创建并初始化信号的总量
dispatch_semaphore_signal:发送一个信号,让信号总量加1
dispatch_semaphore_wait:可以使总信号量减1,当信号总量为0时就会一直等待(阻塞所在线程),否则就可以正常执行

使用信号量时,需要清除的分清等待和执行的线程

应用:

1、保持线程同步,将异步执行的任务转为同步执行任务
2、保证线程安全,为线程加锁

应用一:异步转同步

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:1.0];
    NSLog(@"1:%@",[NSThread currentThread]);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"semaphore end");

iOS GCD

我们在当前线程中开启了一个异步并行队列任务,由于我们使用了信号量semaphore(其初始值为0,不可通行),当我们使用dispatch_semaphore_wait,会阻塞当前线程,直到信号量不为0时,才会执行NSLog(@"semaphore end")的输出,上述例子中,并没有给信号量计数器加1,因此不会执行后面输出任务

我们为其添加加一

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:1.0];
    NSLog(@"1:%@",[NSThread currentThread]);
    dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"semaphore end");

iOS GCD

此时我们发现,程序正常输出。另外,我们这里通过通过信号量实现线程的同步操作(输出在异步任务之后),原本的异步线程在这里并没有什么效果,和同步任务没有任何区别

应用二:线程安全

结合GCD的信号量的特性,我们还可以使用其达到线程安全的目的,即在多线程下,保证事务的原子性

假设我分别开启两个线程去做加一操作,在不保证线程安全的情况下,势必会出现线程争抢资源,导致意想不到的错误产生

首先我们来看下非线程安全

dispatch_queue_t queue1 = dispatch_queue_create("one", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("two", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue1, ^{
    [self addCount];
});
dispatch_async(queue2, ^{
    [self addCount];
});

-(void)addCount{
    while (1) {
        if (count>=100) {
            break;
        }else{
            count++;
            NSLog(@"%ld-%@",count,[NSThread currentThread]);
        }
    }
}

iOS GCD

我们发现,在此次过程中,线程3和4发生了资源争抢的问题,导致在加1的过程中发生了错误,两个线程累加的次数总和超过了100次

接下来,我们使用信号量来保证事务的原子性

// 创建信号量
semaphore = dispatch_semaphore_create(1);
// 在事务的开始和结束时操作信号量
-(void)addCount{
    while (1) {
        // 信号量减一,进入等待状态
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        if (count>=100) {
            // 信号量加一,进入可通行状态
            dispatch_semaphore_signal(semaphore);
            break;
        }else{
            count++;
            NSLog(@"%ld-%@",count,[NSThread currentThread]);
        }
        // 信号量加一,进入可通行状态
        dispatch_semaphore_signal(semaphore);
    }
}

iOS GCD

我们发现,线程3和4交替操作累加,数字累计达到100时,操作总和为100,因此我们可以认定,信号量起到了很好的效果,保证了事务的原子性