iOS GCD之dispatch_semaphore(信号量)

前言

在看AFNetworking3.0源码时,注意到在 AFURLSessionManager.m 里面的 tasksForKeyPath: 方法中使用到dispatch_semaphore,对dispatch_semaphore11不甚理解,经查原来是通过引入信号量dispatch_semaphore``的方式把NSURLSession的异步方法 getTasksWithCompletionHandler: 变成了同步方法。

- (NSArray *)tasksForKeyPath:(NSString *)keyPath {
    __block NSArray *tasks = nil;
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
        if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
            tasks = dataTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
            tasks = uploadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
            tasks = downloadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
            tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
        }

        dispatch_semaphore_signal(semaphore);
    }];

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    return tasks;
}

这里是把本来异步的getTasksWithCompletionHandler方法变成了同步的方式了,通过引入信号量的方式,等待异步方法获取到tasks,然后再返回。


1. dispatch_semaphore介绍

  • 信号量是基于计数器的一种多线程同步机制,用来管理对资源的并发访问
  • 信号量是基于计数器的一种多线程同步机制,用来管理对资源的并发访问。
  • 信号量就是一种可用来控制访问资源的数量的标识,设定了一个信号量,在线程访问之前,加上信号量的处理,则可告知系统按照我们指定的信号量数量来执行多个线程。
  • 其实,这有点类似锁机制了,只不过信号量都是系统帮助我们处理了,我们只需要在执行线程之前,设定一个信号量值,并且在使用时,加上信号量处理方法就行了。

简单来讲 信号量为0则阻塞线程,大于0则不会阻塞。则我们通过改变信号量的值,来控制是否阻塞线程,从而达到线程同步。

1.1 dispatch_semaphore相关的3个函数
  • dispatch_semaphore_create
// 创建信号量,参数:信号量的初值,如果小于0则会返回NULL
dispatch_semaphore_t dispatch_semaphore_create(long value);
  • dispatch_semaphore_wait
// 等待降低信号量,接收一个信号和时间值(多为DISPATCH_TIME_FOREVER)
// 若信号的信号量为0,则会阻塞当前线程,直到信号量大于0或者经过输入的时间值;
// 若信号量大于0,则会使信号量减1并返回,程序继续住下执行
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
  • dispatch_semaphore_signal
// 提高信号量, 使信号量加1并返回
long dispatch_semaphore_signal(dispatch_semaphore_t dsema);

dispatch_semaphore_waitdispatch_semaphore_signal这两个函数中间的执行代码,每次只会允许限定数量的线程进入,这样就有效的保证了在多线程环境下,只能有限定数量的线程进入。

可用于处理在多个线程访问共有资源时候,会因为多线程的特性而引发数据出错的问题。

1.2 线程同步方法
1.2.1 使用NSoperation下可以直接设置并发数
1.2.2 使用GCD让线程同步方法
  • dispatch_group
  • dispatch_barrier
  • dispatch_semaphore

2.应用场景

2.1 保持线程同步,将异步操作转换为同步操作
// 保持线程同步
- (void)semaphore {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
    __block int j = 0;
    dispatch_async(queue, ^{
        j = 100;
        dispatch_semaphore_signal(semaphore);
    });
    
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"finish j = %zd", j);
}

输出结果

image.png

如果注掉dispatch_semaphore_wait这一行,则 j = 0

  • 注释:block块异步执行添加到了全局并发队列里,所以程序在主线程会跳过block块(同时开辟子线程异步执行block块),执行块外的代码dispatch_semaphore_wait,因为semaphore信号量为0,且时间为DISPATCH_TIME_FOREVER,所以会阻塞当前线程(主线程),进而只执行子线程的block块,直到执行块内部的dispatch_semaphore_signal使得信号量+1。正在被阻塞的线程(主线程)会恢复继续执行。这样保证了线程之间的同步。
2.2 为线程加锁
// 给线程加锁
- (void)addLock {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    
    for (int i = 0; i < 10; i++) {
        dispatch_async(queue, ^{
            // 相当于加锁
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            NSLog(@"i = %zd semaphore = %@", i, semaphore);
            // 相当于解锁
            dispatch_semaphore_signal(semaphore);
        });
    }
}

输出结果:

iOS GCD之dispatch_semaphore(信号量)_第1张图片
image.png
  • 注释:当线程1执行到dispatch_semaphore_wait这一行时,semaphore的信号量为1,所以使信号量-1变为0,并且线程1继续往下执行;如果当在线程1 NSLog这一行代码还没执行完的时候,又有线程2来访问,执行dispatch_semaphore_wait时由于此时信号量为0,且时间为DISPATCH_TIME_FOREVER,所以会一直阻塞线程2(此时线程2处于等待状态),直到线程1执行完NSLog并执行完dispatch_semaphore_signal使信号量为1后,线程2才能解除阻塞继续住下执行。以上可以保证同时只有一个线程执行NSLog这一行代码。
2.3 获取通讯录
  • 做通讯录的时候需要判断权限,才能获取通讯录
// 获取通讯录
- (void)getAddressBook {
    //这个变量用于记录授权是否成功,即用户是否允许我们访问通讯录
    __block int tip=0;
    
    //创建通讯簿的引用
    ABAddressBookRef addressBooks=ABAddressBookCreateWithOptions(NULL, NULL);
    //创建一个初始信号量为0的信号
    dispatch_semaphore_t sema=dispatch_semaphore_create(0);
    //申请访问权限
    ABAddressBookRequestAccessWithCompletion(addressBooks, ^(bool granted, CFErrorRef error)        {
        //granted为YES是表示用户允许,否则为不允许
        if (!granted) {
            tip=1;
        }
        //发送一次信号
        dispatch_semaphore_signal(sema);
    });
    //等待信号触发
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
    CFRelease(addressBooks);
}
2.4 使用 Dispatch Semaphore 控制并发线程数量
// 控制并发线程数量
- (void)dispatchAsyncLimit:(dispatch_queue_t)queue limitSemaphoreCount:(NSUInteger)limitSemaphoreCount bloc:(dispatch_block_t)block {
    //控制并发数的信号量
    static dispatch_semaphore_t limitSemaphore;
    
    //专门控制并发等待的线程
    static dispatch_queue_t receiverQueue;
    
    //使用 dispatch_once而非 lazy 模式,防止可能的多线程抢占问题
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        limitSemaphore = dispatch_semaphore_create(limitSemaphoreCount);
        receiverQueue = dispatch_queue_create("receiver", DISPATCH_QUEUE_SERIAL);
    });
    
    // 如不加 receiverQueue 放在主线程会阻塞主线程
    dispatch_async(receiverQueue, ^{
        //可用信号量后才能继续,否则等待
        dispatch_semaphore_wait(limitSemaphore, DISPATCH_TIME_FOREVER);
        dispatch_async(queue, ^{
            !block ? : block();
            //在该工作线程执行完成后释放信号量
            dispatch_semaphore_signal(limitSemaphore);
        });
    });
}
  • 注释:以上栗子有点像-[NSOperationQueue maxConcurrentOperationCount]。 在能保证灵活性的情况下,通常更好的做法是使用操作队列,而不是通过GCD和信号量来构建自己的解决方案。

信号量属于底层工具。它非常强大,但在多数需要使用它的场合,最好从设计角度重新考虑,看是否可以不用。应该优先考虑是否可以使用诸如操作队列这样的高级工具。通常可以通过增加一个分派队列dispatch_suspend,或者通过其他方式分解操作来避免使用信号量。信号量并非不好,只是它本身是锁,能不用锁就不要用。尽量用cocoa框架中的高级抽象,信号量非常接近底层。但有时候,例如需要把异步任务转换为同步任务时,信号量是最合适的工具。

2.5 同时下载多张图片

问题描述:
1.假设现在系统有两个空闲资源可以被利用,但同一时间却有三个线程要进行访问,这种情况下,该如何处理呢?

2.我们要下载很多图片,并发异步进行,每个下载都会开辟一个新线程,可是我们又担心太多线程肯定cpu吃不消,那么我们这里也可以用信号量控制一下最大开辟线程数。

代码如下

- (void)dispatchSignal {
    // crate的value表示,最多几个资源可访问
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
    // 队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //任务1
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"run task 1");
        sleep(1);
        NSLog(@"complete task 1");
        dispatch_semaphore_signal(semaphore);
    });
    //任务2
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"run task 2");
        sleep(1);
        NSLog(@"complete task 2");
        dispatch_semaphore_signal(semaphore);
    });
    //任务3
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"run task 3");
        sleep(1);
        NSLog(@"complete task 3");
        dispatch_semaphore_signal(semaphore);
    });
}

输出结果:

iOS GCD之dispatch_semaphore(信号量)_第2张图片
dispatchSignal.png
  • 总结:由于设定的信号值为2,先执行两个线程,等执行完一个,才会继续执行下一个,保证同一时间执行的线程数不超过2。

假设我们设定信号值=1

// crate的value表示,最多几个资源可访问
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

输出结果:

iOS GCD之dispatch_semaphore(信号量)_第3张图片
image.png

假设我们设定信号值=3,就是不限制线程执行了,因为一共才只有3个线程。

// crate的value表示,最多几个资源可访问
dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);

输出结果:

iOS GCD之dispatch_semaphore(信号量)_第4张图片
image.png

以上只是举的比较简单的例子,在一些特殊场景下,合理利用信号量去控制,能够方便的解决我们的难题。


本文参考
iOS GCD之dispatch_semaphore(信号量)
iOS GCD中级篇 - dispatch_semaphore(信号量)的理解及使用
非常感谢以上作者


iOS多线程详细总结系列文章
iOS GCD之dispatch_semaphore(信号量)
iOS 多线程-GCD 详细总结
iOS 多线程: [NSOperation NSOperationQueue] 详解
iOS 多线程:[pthread,NSThread]详细总结


项目源码链接地址

你可能感兴趣的:(iOS GCD之dispatch_semaphore(信号量))