美文网首页iOS
iOS多线程之GCD、GCD处理多任务的网络请求、多读单写

iOS多线程之GCD、GCD处理多任务的网络请求、多读单写

作者: 流云_henry | 来源:发表于2020-07-14 18:18 被阅读0次

    在软件开发中使用多线程可以大大地提高用户体验,提高效率。Grand Central Dispatch(CGD)则是C语言的一套多线程开发框架,相比NSThread和NSOperation,GCD更加高效,并且线程由系统管理,会自动运行多核运算。因为这些优势,GCD是Apple推荐给开发者使用的首选多线程解决方案。

    1、GCD的调度机制

    GCD框架中一个很重要的概念是调度队列,我们对线程的操作实际上是由调度队列完成的。我们只需要将要执行的任务添加到合适的队列中即可。在GCD框架中,有如下三种类型的调度队列。

    1.1主队列

    其中的任务在主线程中执行,因为其会阻塞主线程,所以是一个串行的队列。可以通过下面的方法得到:

    dispatch_get_main_queue();
    

    1.2全局并行队列

    队列中任务的执行严格按照先进先出的模式进行。如果是串行的队列,则当一个任务结束后,才会开启另一个任务,如果是并行队列,则任务的开启顺序和添加顺序是一致的。系统为iOS应用自动创建了4个全局共享的并发队列。使用下面的函数获得:

    dispatch_get_global_queue(<#long identifier#>, <#unsigned long flags#>);
    

    上面函数的第一个参数是这个队列的ID,系统的4个全局队列默认的优先级不同,这个参数可填写的定义如下:

    #define DISPATCH_QUEUE_PRIORITY_HIGH 2 //优先级别最高的全局队列
    #define DISPATCH_QUEUE_PRIORITY_DEFAULT 0//优先级别中等的全局队列
    #define DISPATCH_QUEUE_PRIORITY_LOW (-2)//优先级别较低的全局队列
    #define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN//后台的全局队列,优先级别最低
    

    这个函数的第二个参数是一个预留参数,我们可以传NULL.

    1.3自定义队列

    上面的两种队列都是系统为我们创建好的,我们只需要获取到他们,添加任务即可。当然我们也可以创建自己的队列,包含串行和并行的。使用如下方法来创建:

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

    其中第一个参数是这个队列的名字,第二个参数决定创建的是串行还是并行队列。填写DISPATCH_QUEUE_SERIAL或NULL创建串行队列,填写DISPATCH_QUEUE_CONCURRENT创建并行队列。

    2、添加任务到调度队列中

    使用dispatch_sync(<#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>)函数或者dispatch_async(<#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>)函数来同步或异步的执行任务。示例如下:

    - (void)creatGCDQueue {
        //创建一个串行的队列
        dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
        //向队列中添加同步任务1
        dispatch_sync(queue, ^{
            NSLog(@"%@:task1",[NSThread currentThread]);
        });
        //向队列中添加异步任务2
        dispatch_async(queue, ^{
            NSLog(@"%@:task2",[NSThread currentThread]);
        });
        
    }
    

    //打印信息:


    image.png

    上面的代码创建了一个串行的自定义队列,并且向队列中添加了一个同步的任务和一个异步的任务。需要注意,这里的同步和异步指的是针对当前代码运行所在的线程而言的。
    从打印信息可以看出,同步的任务是在主线程中执行,异步的任务是在单独的线程中执行,由于我们创建的调度队列是串行的,因此先开启了任务1,后开启了任务2.

    只有当调度队列是并行,而且向队列中添加的任务也是异步的时候,多任务才会实现并行异步执行。

    实现如下:

    - (void)creatGCDQueue {
        //创建一个并行的队列
        dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
        //向队列中添加异步任务1
        dispatch_async(queue, ^{
            for (int i = 0; i < 15; i ++) {
                NSLog(@"%@ = %d:task1",[NSThread currentThread],i);
            }
        });
        //向队列中添加异步任务2
        dispatch_async(queue, ^{
            for (int i = 0; i < 15; i ++) {
                NSLog(@"%@ = %d:task2",[NSThread currentThread],i);
            }    });
        
    }
    

    3、使用队列组

    通过前面的学习,我们现在已经可以运用队列多线程执行任务了,但是GCD的强大之处远远不止如此。看下面的例子。
    如果有3个任务A、B、C,其中A与B是没有关系的,他们可以并行执行,C必须是A、B都结束之后才能执行,当然,实现这样的逻辑并不困难,使用KVO就可以实现,但是如果使用队列处理这样的逻辑,则代码会更加清晰简单。
    可以使用dispatch_group_create()创建一个队列组,使用如下函数将队列添加到队列组中:

    void dispatch_group_async(dispatch_group_t group,
        dispatch_queue_t queue,
        dispatch_block_t block);
    

    队列中的队列是异步执行的,示例如下:

    - (void)creatGCDGroup {
        //创建一个队列组
        dispatch_group_t group = dispatch_group_create();
        //创建一个异步队列
        dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
        //添加任务
        dispatch_group_async(group, queue, ^{
            for (int i = 0; i < 10; i ++) {
                NSLog(@"%@ = %d:task1",[NSThread currentThread],i);
            }
        });
        
        //添加任务
        dispatch_group_async(group, queue, ^{
            for (int i = 0; i < 10; i ++) {
                NSLog(@"%@ = %d:task2",[NSThread currentThread],i);
            }
        });
        
        //阻塞线程,直到前面的队列任务执行完成
        dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
        for (int i = 0; i < 10; i ++) {
            NSLog(@"%@ = %d:over",[NSThread currentThread],i);
        }
    }
    

    打印结果如下:


    image.png

    以上代码完美的实现了我们的任务依赖需求,可以看出GCD的强大了吧,复杂的任务逻辑关系因为GCD变得十分清晰简单。

    4、GCD对循环任务的处理

    说到循环,除了常规的while循环,for循环外,for-in也是开发中常用的一种循环方式。for-in循环通常来进行数组或字典的遍历,这种遍历通常不关心循环执行的顺序。使用GCD,配合设备的多核运算技术,我们可以将这种循环遍历的性能提升到极致,示例如下:

    - (void)creatGCDApply {
        dispatch_apply(20, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t i) {
            NSLog(@"%@:%zu",[NSThread currentThread],i);
        });
    }
    

    打印信息如下:


    image.png

    从打印信息可以看出,循环是由多个不同的线程完成的,比如我们的设备是8核的CPU。因此每个线程单独在一个核执行,这将循环的运行效率提升到了极致。大大提高了运行速率。

    5、GCD中的消息与信号

    5.1Dispatch Source

    在GCD框架中提供了dispatch_source_t类型的对象,dispatch_source_t类型的对象可以用来传递和接收某个消息。在任一线程上调用它的一个函数 dispatch_source_merge_data 后,会执行 Dispatch Source 事先定义好的句柄(可以把句柄简单理解为一个 block )。
    这个过程叫 Custom event ,用户事件。是 dispatch source 支持处理的一种事件。简单地说,这种事件是由你调用 dispatch_source_merge_data 函数来向自己发出的信号。
    示例如下:

    - (void)creatGCDSource {
        //创建一个数据对象,DISPATCH_SOURCE_TYPE_DATA_ADD的含义表示当数据变化时相加
        dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
        //设置响应分派源事件的block,在分派源指定的队列上运行
        dispatch_source_set_event_handler(source, ^{
            
            NSLog(@"%lu:sec",dispatch_source_get_data(source));//得到分派源的数据
            dispatch_async(dispatch_get_main_queue(), ^{
                //更新UI
            });
            
        });
        //启动
        dispatch_resume(source);
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
           
            //网络请求
            //向分派源发送事件,需要注意的是,不可以传递0值(事件不会被触发),同样也不可以传递负数。
            dispatch_source_merge_data(source, 1);
        });
    
    }
    

    注意:DISPATCH_SOURCE_TYPE_DATA_ADD是将所有触发结果相加,最后统一执行响应,但是加入sleepForTimeInterval后,如果interval的时间越长,则每次触发都会响应,但是如果interval的时间很短,则会将触发后的结果相加后统一触发。这在更新UI时很有用,比如更新进度条时,没必要每次触发都响应,因为更新时还有其他的用户操作(用户输入,触碰等),所以可以统一触发

    比如我们写一个进度条的示例:

    - (void)creatGCDSource {
        //1、指定DISPATCH_SOURCE_TYPE_DATA_ADD,做成Dispatch Source(分派源)。设定Main Dispatch Queue 为追加处理的Dispatch Queue
           dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
           
           __block NSUInteger totalComplete = 0;
           
           dispatch_source_set_event_handler(source, ^{
               
               //当处理事件被最终执行时,计算后的数据可以通过dispatch_source_get_data来获取。这个数据的值在每次响应事件执行后会被重置,所以totalComplete的值是最终累积的值。
               NSUInteger value = dispatch_source_get_data(source);
               
               totalComplete += value;
               
               NSLog(@"进度:%@", @((CGFloat)totalComplete/100));
               
               NSLog(@":large_blue_circle:线程号:%@", [NSThread currentThread]);
           });
           
           //分派源创建时默认处于暂停状态,在分派源分派处理程序之前必须先恢复。
           dispatch_resume(source);
           
           dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
           
           //2、恢复源后,就可以通过dispatch_source_merge_data向Dispatch Source(分派源)发送事件:
        
           dispatch_async(queue, ^{
           
               for (NSUInteger index = 0; index < 100; index++) {
           
                   dispatch_source_merge_data(source, 1);
           
                   NSLog(@":recycle:线程号:%@~~~~~~~~~~~~i = %ld", [NSThread currentThread], index);
           
                   sleep(0.1);
               }
           });
    
    }
    

    5.2、信号量 singer

    信号量是GCD中一个很重要的概念,他的用法与消息的传递有所类似,其本示例代码如下:

    - (void)creatGCDSinger {
        //创建一个信号,其中的参数是信号的初始值
        dispatch_semaphore_t singer = dispatch_semaphore_create(0);
        //发送信号,信号量+1
        dispatch_semaphore_signal(singer);
        //等待信号,当信号量大于0时,执行后面的代码,否则等待,第二个参数为等待的超时时长,下面设置的为一直等待
        dispatch_semaphore_wait(singer, DISPATCH_TIME_FOREVER);
        NSLog(@"singer");
    }
    

    注意,dispatch_semaphore_wait函数会阻塞当前线程,在主线程中要慎用。通过发送信号函数:dispatch_semaphore_signal(),可以使信号量+1,每次执行过等待信号后,信号量会-1,如此,我们可以很方便地控制不同队列中方法的执行流程。

    5.2.1限制线程的最大并发数
    - (void)creatGCDSinger {
        //创建一个信号,其中的参数是信号的初始值
        dispatch_semaphore_t singer = dispatch_semaphore_create(2);
        for (int i = 0; i < 15; i++) {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                //等待信号,当信号量大于0时,执行后面的代码,否则等待,第二个参数为等待的超时时长,下面设置的为一直等待
                dispatch_semaphore_wait(singer, DISPATCH_TIME_FOREVER);
                //doing
                sleep(1);
                //发送信号,信号量+1
                dispatch_semaphore_signal(singer);
            });
        }
    }
    

    如上述代码可知,总共异步执行15个任务,但是由于我们设置了值为2的信号量,每一次执行任务的时候信号量都会先-1,而在任务结束后使信号量加1,当信号量减到0的时候,说明正在执行的任务有2个,这个时候其它任务就会阻塞,直到有任务被完成时,这些任务才会执行。

    注意,信号量的正常的使用顺序是先降低(dispatch_semaphore_wait)然后再提高(dispatch_semaphore_signal),这两个函数通常成对使用。

    5.2.2阻塞发请求的线程

    有些时候,我们需要阻塞发送请求的线程,比如在多个请求回调后统一操作的需求,而这些请求之间并没有顺序关系,且这些接口都会另开线程进行网络请求的。一般地,这种多线程完成后进行统一操作的需求都会使用队列组(dispatch_group_t)来完成,但是由于是异步请求,没等其异步回调之后,请求的线程就结束了,为此,就需要使用信号量来阻塞住发请求的线程。实现代码如下:

    - (void)creatGCDSinger {
        //创建线程组
        dispatch_group_t group = dispatch_group_create();
        //获取队列
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        //任务1
        dispatch_group_async(group, queue, ^{
           //请求1
            [self request1];
        });
        
        //任务2
        dispatch_group_async(group, queue, ^{
           //请求2
            [self request2];
        });
        
        //任务3
        dispatch_group_async(group, queue, ^{
           //请求3
            [self request3];
        });
    
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
             NSLog(@"-------所有网络请求已请求完成-------");
         });
    }
    
    - (void)request1 {
        //创建信号量,并设置为0,信号量本质是资源数,为0表示用完,需要等待
        dispatch_semaphore_t sema = dispatch_semaphore_create(0);
        //模拟网络请求-异步
        //每次网络请求成功或失败后,都让信号量+1,表示释放当前资源,其他线程可以抢占了
        [[KNetRequestManager share] getSomeData:^{
            //网络请求成功,发送信号
          dispatch_semaphore_signal(sema);
        } errorBlock:^{
            //网络请求失败,发送信号
            dispatch_semaphore_signal(sema);
        }];
        //如果信号量为0,表示没有资源可用,便一直等待,不再往下执行.只有当网络请求成功或失败时,才会往下走
        dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
    }
    
    - (void)request2 {
        //创建信号量,并设置为0,信号量本质是资源数,为0表示用完,需要等待
        dispatch_semaphore_t sema = dispatch_semaphore_create(0);
        //模拟网络请求-异步
        //每次网络请求成功或失败后,都让信号量+1,表示释放当前资源,其他线程可以抢占了
        [[KNetRequestManager share] getSomeData:^{
            //网络请求成功,发送信号
          dispatch_semaphore_signal(sema);
        } errorBlock:^{
            //网络请求失败,发送信号
            dispatch_semaphore_signal(sema);
        }];
        //如果信号量为0,表示没有资源可用,便一直等待,不再往下执行
        dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
    }
    
    - (void)request3 {
        //创建信号量,并设置为0,信号量本质是资源数,为0表示用完,需要等待
        dispatch_semaphore_t sema = dispatch_semaphore_create(0);
        //模拟网络请求-异步
        //每次网络请求成功或失败后,都让信号量+1,表示释放当前资源,其他线程可以抢占了
        [[KNetRequestManager share] getSomeData:^{
            //网络请求成功,发送信号
          dispatch_semaphore_signal(sema);
        } errorBlock:^{
            //网络请求失败,发送信号
            dispatch_semaphore_signal(sema);
        }];
        //如果信号量为0,表示没有资源可用,便一直等待,不再往下执行
        dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
    }
    

    当然,我们也可以使用dispatch_group_enter和dispatch_group_leave来实现同样的功能:

    - (void)creatGCDSinger {
        //创建线程组
        dispatch_group_t group = dispatch_group_create();
        //创建一个并发队列
        dispatch_queue_t queue = dispatch_queue_create("group.queue", DISPATCH_QUEUE_CONCURRENT);
        //任务1
         dispatch_group_enter(group);
        dispatch_group_async(group, queue, ^{
           //请求1
            [self request1WithGroup:group];
        });
        
        //任务2
        dispatch_group_enter(group);
        dispatch_group_async(group, queue, ^{
           //请求2
            [self request2WithGroup:group];
        });
    
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
             NSLog(@"-------所有网络请求已请求完成-------");
         });
    }
    
    - (void)request1WithGroup:(dispatch_group_t)group {
        //模拟网络请求-异步
        [[KNetRequestManager share] getSomeData:^{
            //网络请求成功,调用level
          dispatch_group_leave(group);
        } errorBlock:^{
             //网络请求失败,调用level
            dispatch_group_leave(group);
        }];
    
    }
    
    - (void)request2WithGroup:(dispatch_group_t)group
        //模拟网络请求-异步
        [[KNetRequestManager share] getSomeData:^{
            //网络请求成功,调用level
          dispatch_group_leave(group);
        } errorBlock:^{
             //网络请求失败,调用level
            dispatch_group_leave(group);
        }];
    }
    
    5.2.3信号量控制网络请求顺序
    - (void)creatGCDSinger {
        //创建semp
        dispatch_semaphore_t semp = dispatch_semaphore_create(1);
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        //任务1
        dispatch_async(queue, ^{
            //信号量-1
            dispatch_semaphore_wait(semp, DISPATCH_TIME_FOREVER);
            //模拟网络请求
            //模拟网络请求-异步
            //每次网络请求成功或失败后,都让信号量+1,表示释放当前资源,其他线程可以抢占了
            [[KNetRequestManager share] getSomeData:^{
                //网络请求成功,发送信号
              dispatch_semaphore_signal(sema);
            } errorBlock:^{
                //网络请求失败,发送信号
                dispatch_semaphore_signal(sema);
            }];
        });
        //任务2
        dispatch_async(queue, ^{
            //信号量-1
            dispatch_semaphore_wait(semp, DISPATCH_TIME_FOREVER);
            //模拟网络请求
            //模拟网络请求-异步
            //每次网络请求成功或失败后,都让信号量+1,表示释放当前资源,其他线程可以抢占了
            [[KNetRequestManager share] getSomeData:^{
                //网络请求成功,发送信号
              dispatch_semaphore_signal(sema);
            } errorBlock:^{
                //网络请求失败,发送信号
                dispatch_semaphore_signal(sema);
            }];
        });
    }
    

    6、队列的挂起和开启

    在GCD框架中还提供了暂停与开始任务队列的方法,使用下面的函数可以将队列或队列组暂时挂起和开启:

    //挂起队列或队列组
    void dispatch_suspend(dispatch_object_t object);
    //开启队列或队列组
    void dispatch_resume(dispatch_object_t object);
    

    注意:在暂停队列时,队列中正在执行的任务并不会中断,未开启的任务会被挂起。

    7、数据存储的线程安全问题-多度单写

    在进行多线程编程时,或许总会遇到这一类问题:数据的竞争与线程的安全。这些问题如果通过程序手动来控制,则难度将会非常大。CGD同样为我们简单地解决了这样的问题。
    首先,如果只是在读取数据,而不对数据做任何修改,那么我们并不需要处理安全问题,可以让多个任务同时进行读取。可是如果要对数据进行写操作,那么在同一时间,我们就必须只能有一个任务在写,CGD中有一个方法帮我们完美地解决了这个问题,示例如下:

    - (void)creatCGDReadAndWriter {
        //创建一个队列
        dispatch_queue_t queue = dispatch_queue_create("oneQueue", DISPATCH_QUEUE_CONCURRENT);
        //多个任务同时执行读操作
        dispatch_async(queue, ^{
            for (int i = 0; i < 5; i++) {
                NSLog(@"read1:%d",i);
            }
        });
        dispatch_async(queue, ^{
            for (int i = 0; i < 5; i++) {
                NSLog(@"read2:%d",i);
            }
        });
        
        //执行写操作
        /*
         下面这个函数在加入队列时不会执行,会等待已经开始的异步执行全部完成后再执行,并且在执行时会阻塞其他任务
         当执行完成后,其他任务重新进入异步执行
         */
        dispatch_barrier_async(queue, ^{
            for (int i = 0; i < 5; i ++) {
                 NSLog(@"writer:%d",i);
            }
        });
        //绩效执行异步操作
        dispatch_async(queue, ^{
            for (int i = 0; i < 5; i++) {
                NSLog(@"read3:%d",i);
            }
        });
        dispatch_async(queue, ^{
            for (int i = 0; i < 5; i++) {
                NSLog(@"read4:%d",i);
            }
        });
    }
    

    打印信息:


    image.png

    从打印信息可以看出读操作是异步进行的,写操作是等待当前任务结束后阻塞任务队列独立进行的,当写操作结束后队列恢复异步执行读操作,这正是我们需要的效果。

    相关文章

      网友评论

        本文标题:iOS多线程之GCD、GCD处理多任务的网络请求、多读单写

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