参考资料:
Semaphore信号量主要用途有两个:
- 用于多线程对共享资源的访问控制,保证线程安全,为线程加锁。
- 保持线程同步,将异步任务转换为同步任务。
Tip : 共享资源可以是一个变量,一个从url下载图片的任务,读取数据库的任务等等。
一些理论知识(A Bit of Theory)
信号量是由一个线程队列和一个计数器(Int类型)组成的。
线程队列(Threads Queue) : 采用FIFO先进先出机制用来追踪等待线程(等待访问共享资源的线程)。
计数器(Counter Value) : 用来决定一个线程是否可以访问共享资源。(wait
和signal
方法都将改变计数器的值)
那么,我们什么时候调用wait
和signal
方法呢?
- 访问共享资源之前调用
wait
方法。wait
方法可以理解为当前线程在请求对共享资源的访问,如果资源可用(未被其他线程占用)则允许访问,否则等待(阻塞线程)。 - 访问共享资源结束之后调用
signal
方法。结束访问时,要通知信号量,我用完了资源,让别人(等待着的线程)来访问吧。
调用wait
方法时,行为规则如下:
- 计数器-1;
- 如果-1之后,当前计数器小于0,则线程被阻塞;
- 如果-1之后,当前计数器大于等于0,则线程被放行,无需等待;
调用signal
方法时,行为规则如下:
- 计数器+1;
- 如果+1之前,计数器小于0,此方法将从线程队列中按照FIFO规则提取第一个等待中的线程并唤醒。
- 如果+1之前,计数器大于等于0,说明线程队列是空的,没有正在等待的线程。
上面所述流程可用图示表示如下:
具体的例子可以查看上面文章中的例子(文章需要翻墙!!)
这里重点总结几点实际使用过程中的小Tip:
-
wait
方法有一个等待超时时间参数(尤其注意参数是.distantFuture(无限期等待下去)情况下不要在主线程中调用wait
方法,可能将阻塞主线程。) - 调用
wait
计数器即刻-1,等待结果为success
时,计数器保持不变,由signal
负责回归计数器,但是如果等待结果是timedOut
,计数器将自动+1回归,因此,应只在结果是success
时才执行signal
操作。 - 构造信号量时传入的初始计数器数值,首先不允许为负值,其次为0时一般用于将异步任务转成同步任务,数值大于0时代表最多允许多少个线程同时访问共享资源或者同时执行多少个任务。
代码示例:
-
异步任务转同步任务
var value = 10 let semaphore = DispatchSemaphore(value: 0) var waitResult: DispatchTimeoutResult = .success DispatchQueue.global().async { value = 100 Thread.sleep(forTimeInterval: 3.0) if waitResult == .success { semaphore.signal() } } // 试着将这里的超时时间参数改改查看效果 waitResult = semaphore.wait(timeout: DispatchTime.distantFuture) switch waitResult { case .success: print("success") case .timedOut: print("timedOut") }
查看不羁阁:『GCD』详尽总结的文章末尾,这里讲到了信号量及其应用示例。
了解一些概念:
优先级反转问题及解决方法