iOS中的网络和多线程编程(九)

2022-02-21  本文已影响0人  anny_4243

摘自《iOS程序员面试笔试宝典》

GCD中有哪几种队列

在GCD中,派发队列(Dispatch Queue)是最重要的概念之一。派发队列是一个对象,它可以接受任务,并将任务以FIFO(先进先出)的顺序来执行。派发队列可以是并发的或串行的。并发队列可以执行多任务,串行队列同一时间只执行单一任务。在GCD中,有3种类型的派发队列。

1)串行队列。串行队列中的任务按先后顺序逐个执行,通常用于同步访问一个特定的资源。使用dispatch_queue_create函数,可以创建串行队列。

2)并发队列。在GCD中也称为全局并发队列,可以并发地执行一个或者多个任务。并发队列有高、中、低、后台4个优先级别,中级是默认级别。可以使用dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0)函数来获取全局并发队列对象。串行队列和异步队列的区别在于同步执行和异步执行时的表现。

串行队列和异步队列的区别

3)主队列。它是一种特殊的串行队列。它在应用程序的主线程中用于更新UI。其他的两种队列不能更新UI。使用dispatch_get_main_queue函数,可以获得主队列对象。

如何理解GCD死锁

所谓死锁,通常指两个操作相互等待对方完成,造成死循环,于是两个操作都无法完成,就产生了死锁。下面是一个死锁的代码示例。

int main(int argc, char * argv[]) {
    @autoreleasepool {
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"这里死锁了");
        });
    }
    return 0;
}

这个程序就是典型的死锁。程序将主队列和一个block传入GCD的同步函数dispatch_sync中,等待同步函数执行,直到同步函数返回。但是事实上,这个block永远不会被执行。因为main函数是在主队列中的,它是正在被执行的任务,而主队列中同时只能有一个任务在执行,也就是说只有队头的任务才能被执行。由于主队列是一个特殊的串行队列,它严格遵循FIFO的原则,所以block中的任务必须等到main函数执行完,才能被执行。另外,dispatch_sync函数的特性是,只有block中的任务被执行完毕,才会返回。因此,只要block不被执行,它就不会返回。所以,在这段代码中,main函数等待dispatch_sync函数返回,而dispatch_sync的返回又依赖block执行完毕,block的执行又需要等待main函数的执行结束。这样就造成了三方循环等待,即死锁。

可以总结出GCD死锁的原因大体有以下两点:

1)GCD函数未返回,会阻塞正在执行的任务。这里需要强调的是,阻塞(blocking)和死锁(deadlock)是不同的意思。阻塞表示A任务的执行需要等待B任务的完成,称作B会阻塞A,通俗来讲就是强制等待的意思。而死锁表示A任务和B任务相互等待,形成阻塞闭环。

2)队列中的任务无法并发执行。

以上两点,如果同时出现,那么就会产生阻塞闭环,形成死锁。所以针对以上情况,只需要消除其中任何一个因素,就可以打破这个闭环,避免死锁。

解决GCD死锁的方法有以下几种方式:

1)使用dispatch_async函数。dispatch_async函数是异步函数,具备开启新线程的能力,但是不一定会开启新线程。如果传入的队列参数是主队列,那么任务仍然会在主线程中等待执行,函数不会立即返回。如果传入的队列是普通的串行队列或者并发队列,那么该函数就会立即返回。

2)将有可能形成阻塞闭环的任务分别放到不同的队列中执行。如案例中,可以新建一个串行队列,将block放入自己的串行队列中,不再和main函数处于一个队列,就能够解决队列阻塞,因此避免了死锁问题。示例代码如下:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        dispatch_queue_t serialQueue = dispatch_queue_create("这是一个串行队列", DISPATCH_QUEUE_SERIAL);
        dispatch_sync(serialQueue, ^{
            NSLog(@"这里不会死锁了");
        });
        }
    return 0;
}

另外,有些面试题,如“是否在主线程使用sync函数就会造成死锁”或者“是否在主线程使用sync函数,同时传入串行队列就会死锁”,答案都是否定的,只要能够真正了解GCD死锁的原理,就能很好地回答类似问题了。

如何使用GCD实现线程之间的通信

在iOS应用程序的开发中,一般需要在主线程中进行UI刷新。例如,响应单击、滚动或者拖曳等事件,所以主线程一般也被称为UI线程。在主线程中,应该尽量避免在主线程中执行一些耗时的操作,如文件的上传和下载等。应该将这些耗时操作放到子线程中执行,等子线程的耗时操作执行完成后,再通知主线程更新相应的UI。要完成这样的操作,就必须实现线程之间的通信,GCD是实现线程之间通信的常用方式。示例代码如下:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    /*执行耗时操作*/
    for (int i = 0; i < 10000; i++) {
        NSLog(@"i = %i", i);
    }
    /*回到主线程*/
    dispatch_async(dispatch_get_main_queue(), ^{
        //更新UI
    });
});

先将需要执行的耗时操作放入全局并发队列中,再使用dispatch_async异步函数执行并发队列,这样就会开启新的线程执行耗时操作,不会阻塞主线程的任务。当耗时任务完成后,通过主队列回到主线程执行相应的UI更新操作。需要强调的是,当使用主队列时,无论是使用dispatch_async异步函数,还是使用dispatch_sync同步函数,执行的结果是一样的。因为主队列是一种特殊的串行队列,在主队列中任务总会在主线程中执行。

GCD如何实现线程同步

NSOperation可以通过使用addDependency函数直接设置操作之间的依赖关系来调整操作之间的执行顺序从而实现线程同步,还可以使用setMaxConcurrentOperationCount函数来直接设置并控制最大并发数量,那么在GCD中如何实现呢?

GCD实现线程同步的方法有以下3种:

1)组队列(dispatch_group)。

2)阻塞任务(dispatch_barrier_(a)sync)。

3)信号量机制(dispatch_semaphore)。

信号量机制主要是通过设置有限的资源数量来控制线程的最大并发数量及阻塞线程实现线程同步等。

GCD中使用信号量需要用到3个函数:

1)dispatch_semaphore_create用来创建一个semaphore信号量并设置初始信号量的值。

2)dispatch_semaphore_signal发送一个信号让信号量增加1(对应PV操作的V操作)。

3)dispatch_semaphore_wait等待信号使信号量减1(对应PV操作的P操作)。

那么如何通过信号量来实现线程同步呢?下面介绍使用GCD信号量来实现任务间的依赖和最大并发任务数量的控制。

引申1:使用信号量实现任务2依赖于任务1,即任务2要等待任务1结束才开始执行

方法很简单,创建信号量并初始化为0,让任务2执行前等待信号,实现对任务2的阻塞。然后在任务1完成后再发送信号,从而任务2获得信号开始执行。需要注意的是,这里任务1和2都是异步提交的,如果没有信号量的阻塞,那么任务2是不会等待任务1的,实际上这里使用信号量实现了两个任务的同步。示例代码如下:

/*创建一个信号量*/
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
/*任务1*/
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    /*耗时任务1*/
    NSLog(@"任务1开始");
    [NSThread sleepForTimeInterval:3];
    NSLog(@"任务1结束");
    /*任务1结束,发送信号告诉任务2可以开始了*/
    dispatch_semaphore_signal(semaphore);
});
/*任务2*/
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    /*等待任务1结束获得信号量,无限等待*/
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    /*如果获得信号量,那么开始任务2*/
    NSLog(@"任务2开始");
    [NSThread sleepForTimeInterval:3];
    NSLog(@"任务2结束");
});
[NSThread sleepForTimeInterval:10];

通过打印的时间可以看到任务2是在任务1结束后紧接着执行的。打印结果如下:

引申2:通过信号量控制最大并发数量

通过信号量控制最大并发数量的方法为:创建信号量并初始化信号量为想要控制的最大并发数量,例如想要保证最大并发数为5,则信号量初始化为5。然后在每个新任务执行前进行P操作,等待信号使信号量减1;每个任务结束后进行V操作,发送信号使信号量加1。这样即可保证信号量始终在5以内,当前最多也只有5个以内的任务在并发执行。示例代码如下:

/*创建一个信号量并初始化为5*/
dispatch_semaphore_t semaphore = dispatch_semaphore_create(5);
/*模拟1000个等待执行的任务,通过信号量控制最大并发任务数量为5*/
for (int i = 0; i < 1000; i++) {
    /*任务i*/
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        /*耗时任务1,执行前等待信号使信号量减1*/
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"任务%d开始",i);
        [NSThread sleepForTimeInterval:10];
        NSLog(@"任务%d结束",i);
        /*任务i结束,发送信号释放一个资源*/
        dispatch_semaphore_signal(semaphore);
    });
}
[NSThread sleepForTimeInterval:1000];

打印结果为每次开启5个并发任务:

GCD多线程编程中什么时候会创建新线程

对于是否会开启新线程的情景主要有如下几种情况:串行队列中提交异步任务、串行队列中提交同步任务、并发队列中提交异步任务、并发队列中提交同步任务。(其中主队列是典型的串行队列,全局队列是典型的并发队列)

/*创建一个串行队列*/
dispatch_queue_t serialQueue = dispatch_queue_create("serial.queue", DISPATCH_QUEUE_SERIAL);
/*创建一个并发队列*/
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrent.queue", DISPATCH_QUEUE_CONCURRENT);

1)串行队列中提交同步任务:不会开启新线程,直接在当前线程同步地串行执行这些任务。

/*1 串行队列添加同步任务:没有开启新线程,全部在主线程串行执行*/
dispatch_sync(serialQueue, ^{
    NSLog(@"SERIAL_SYN_A %@",[NSThread currentThread]);
});
dispatch_sync(serialQueue, ^{
    NSLog(@"SERIAL_SYN_B %@",[NSThread currentThread]);
});
dispatch_sync(serialQueue, ^{
    NSLog(@"SERIAL_SYN_C %@",[NSThread currentThread]);
});

2)串行队列中提交异步任务:会开启一个新线程,在新子线程异步地串行执行这些任务。

/*2 串行队列添加异步任务:开启了一个新子线程并共用,串行执行*/
dispatch_async(serialQueue, ^{
    NSLog(@"SERIAL_ASYN_A %@",[NSThread currentThread]);
});
dispatch_async(serialQueue, ^{
    NSLog(@"SERIAL_ASYN_B %@",[NSThread currentThread]);
});
dispatch_async(serialQueue, ^{
    NSLog(@"SERIAL_ASYN_C %@",[NSThread currentThread]);
});

3)并发队列中提交同步任务:不会开启新线程,效果和“串行队列中提交同步任务”一样,直接在当前线程同步地串行执行这些任务。

/*3 并发队列添加同步任务:没有开启新线程,全部在主线程串行执行*/
dispatch_async(concurrentQueue, ^{
    NSLog(@"CONCURRENT_SYN_A %@",[NSThread currentThread]);
});
dispatch_async(concurrentQueue, ^{
    NSLog(@"CONCURRENT_SYN_B %@",[NSThread currentThread]);
});
dispatch_async(concurrentQueue, ^{
    NSLog(@"CONCURRENT_SYN_C %@",[NSThread currentThread]);
});

4)并发队列中提交异步任务:会开启多个子线程,在子线程异步地并发执行这些任务。

dispatch_async(concurrentQueue, ^{
    NSLog(@"CONCURRENT_ASYN_A %@",[NSThread currentThread]);
});
dispatch_async(concurrentQueue, ^{
    NSLog(@"CONCURRENT_ASYN_B %@",[NSThread currentThread]);
});
dispatch_async(concurrentQueue, ^{
    NSLog(@"CONCURRENT_ASYN_C %@",[NSThread currentThread]);
});

下图展示了上面例子中线程的执行顺序。

线程执行顺序

总结:

只有异步提交任务时才会开启新线程,异步提交到串行队列会开启一个新线程,异步提交到并发队列可能会开启多个线程。

同步提交任务无论提交到并发队列还是串行队列,都不会开启新线程,都会直接在当前线程依次同步执行。

注意,如果当前线程是主线程,那么不可在当前线程提交同步任务,否则会造成线程死锁而报错。

上一篇下一篇

猜你喜欢

热点阅读