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

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

GCD中有哪几种队列


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


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


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


2466108-3de9e8f1d630f7e6.webp.jpg


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


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结束后紧接着执行的。打印结果如下:


2466108-d8acaa571f4705d5.webp.jpg


引申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个并发任务:


2466108-e0b05652d69a524b.webp.jpg


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]);
});


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


2466108-66619e444a4f127c.webp.jpg


线程执行顺序


总结:


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


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


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


目录
相关文章
|
21天前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
116 2
|
2月前
|
设计模式 Java 开发者
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####
|
1月前
|
缓存 Java 调度
多线程编程核心:上下文切换深度解析
在现代计算机系统中,多线程编程已成为提高程序性能和响应速度的关键技术。然而,多线程编程中一个不可避免的概念就是上下文切换(Context Switching)。本文将深入探讨上下文切换的概念、原因、影响以及优化策略,帮助你在工作和学习中深入理解这一技术干货。
51 10
|
2月前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
1月前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
1月前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
59 3
|
1月前
|
算法 调度 开发者
多线程编程核心:上下文切换深度解析
在多线程编程中,上下文切换是一个至关重要的概念,它直接影响到程序的性能和响应速度。本文将深入探讨上下文切换的含义、原因、影响以及如何优化,帮助你在工作和学习中更好地理解和应用多线程技术。
45 4
|
19天前
|
安全 Java API
【JavaEE】多线程编程引入——认识Thread类
Thread类,Thread中的run方法,在编程中怎么调度多线程
|
2月前
|
安全 Java 开发者
Java中的多线程编程:从基础到实践
本文深入探讨了Java多线程编程的核心概念和实践技巧,旨在帮助读者理解多线程的工作原理,掌握线程的创建、管理和同步机制。通过具体示例和最佳实践,本文展示了如何在Java应用中有效地利用多线程技术,提高程序性能和响应速度。
73 1
|
2月前
|
API Android开发 iOS开发
深入探索Android与iOS的多线程编程差异
在移动应用开发领域,多线程编程是提高应用性能和响应性的关键。本文将对比分析Android和iOS两大平台在多线程处理上的不同实现机制,探讨它们各自的优势与局限性,并通过实例展示如何在这两个平台上进行有效的多线程编程。通过深入了解这些差异,开发者可以更好地选择适合自己项目需求的技术和策略,从而优化应用的性能和用户体验。