iOS

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

2020-07-14  本文已影响0人  流云_henry

在软件开发中使用多线程可以大大地提高用户体验,提高效率。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

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

上一篇下一篇

猜你喜欢

热点阅读