美文网首页
信号量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