美文网首页
信号量Semaphore的典型应用场景

信号量Semaphore的典型应用场景

作者: 夜月饮酒 | 来源:发表于2018-06-05 14:56 被阅读0次

    在iOS多线程开发环境中,我们往往会用信号量Semaphore解决一些特别的问题,它不仅高效而且也易于理解。这里我总结了加锁、异步返回、控制线程并发数这三个用途,下面通过一些例子进行解释。

    一、加锁

    代码形式:

    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    for (int i = 0; i < 10000; i++) {
        dispatch_async(queue, ^{
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    
            //临界区,即待加锁的代码区域
    
            dispatch_semaphore_signal(semaphore);
        });
    }
    

    在进行多线程任务之前,首先创建一个计数为1的信号量,这样可以保证同一时刻只有一个线程在访问临界区,dispatch_semaphore_create() 会为我们完成。

    在要访问临界区之前,通过 dispatch_semaphore_wait() 函数,我们可以在信号量为 0 时,让临界区外的线程进入等待状态。

    在这里,当第一条线程访问临界区时,信号量计数为初始值1,

    dispatch_semaphore_wait() 函数判断到计数大于0,于是将计数减1,从而线程允许访问临界区。其它线程因为信号量等于0,就在临界区外等待。

    在第一条线程访问完临界区后,这条线程需要发出一个信号,来表明我已经用完临界区的资源了,下个正在等待的线程可以去访问了。

    dispatch_semaphore_signal()会将信号量计数加1,就好像发出了一个信号一样,下个在临界区前等待的线程会去接收它。接收到了信号的线程判断到信号量计数大于零了,于是访问临界区。

    通过重复这个过程,所有线程都会安全地访问一遍临界区。

    贴一段YYKit中的简单的加锁代码:

    - (instancetype)init {
        self = [super init];
        _lock = dispatch_semaphore_create(1);
        return self;
    }
    
    - (NSURL *)imageURL {
        dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER);
        NSURL *imageURL = _imageURL;
        dispatch_semaphore_signal(_lock);
        return imageURL;
    }
    

    二、异步任务,同步返回

    - (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) {
            //task赋值,代码有点长,就不贴了
            dispatch_semaphore_signal(semaphore);
        }];
    
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    
        return tasks;
    }
    

    上面是 AFNetworking 的一段代码,我且称之为异步返回。这段代码的功能是通过异步的请求取得键路径为 keyPath 的任务数组 tasks,然后返回它。这个方法虽然是异步的,但是执行时间较短。

    碰到这种情况,我们肯定最先想到的是用代码块 block 或者代理 delegate 来实现,然后我们就得去声明一个代理,写一个协议方法,或者写一个带有一个参数的代码块,这里AFNetworking巧妙地通过信号量解决了。

    我们跟之前的加锁对比,可以发现,信号量在创建时计数是0,
    dispatch_semaphore_signal() 函数在 dispatch_semaphore_wait() 函数之前。

    AFNetworking 把 dispatch_semaphore_wait() 函数放在返回语句之前,同时信号量计数初始为0,是为了让线程在 tasks 有值之前一直等待。获取 tasks 的异步操作结束之后,这时候 tasks 赋值好了,于是通过 dispatch_semaphore_signal() 函数发出信号,外面的线程就知道不用等待,可以返回 tasks 了。

    其实信号量进行了隐式的线程间通信,仔细想想,信号量本身是否线程安全呢?

    三、控制线程并发数

    在 GCD 中,dispatch_async() 异步操作可以产生新的线程,但是方法本身没办法限制线程的最大并发数,线程的创建和销毁是由 GCD 底层管理的。
    了解 NSOperationQueue 的同学肯定知道,通过 maxConcurrentOperationCount 属性可以设置它的最大并发数。那么在GCD中,对应的解决方法就是使用信号量。

    dispatch_semaphore_t semaphore = dispatch_semaphore_create(5);
    for (int i = 0; i < 1000; ++i) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            
            //多线程代码
            
            dispatch_semaphore_signal(semaphore);
        });
    }
    

    其实跟加锁代码非常相似,区别在于,在初始化信号量时,将计数赋值为最大并发数。在应用场景上,限制线程并发数是为了性能考虑,而加锁是为了安全而考虑。

    相关文章

      网友评论

          本文标题:信号量Semaphore的典型应用场景

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