引言
在现代应用开发中,处理并发任务已经成了不可避免的挑战。在这种情况下,如何有效地管理多个线程对共享资源的访问,避免资源竞争和数据不一致的问题,成为了我们必须面对的难题。在NSOperation&NSOperationQueue中系统为我们提供了控制最大并发数,设置操作依赖,和线程等待等方法。GCD也同样提供了一系列强大的工具来帮助我们解决这些问题,在前面的博客中我们已经介绍了一个栅栏,本篇博客我们就来介绍另外一个关键的的工具——信号量(Semaphore)。
信号量的概念
信号量(Semaphore)是并发编程中的关键工具,用于控制多个线程对共享资源的访问。它通过维护一个计数来限制同时访问某个资源的线程数量,确保资源不会被过渡占用。信号量的工作方式有点类似于NSOperationQueue的最大并发数属性,但不同的是,信号量的技术增加和减少需要开发者手动管理。
在Objective-C中信号量是dispatch_semaphore_t类型,而在Swift中,信号量是DispatchSemaphore类的实例。初始化时,信号量包含一个计数值,这个值表示可以同时访问资源的线程数。开发者可以通过增加或减少技术来手动管理资源的占用和释放,确保线程按照预期的方式等待资源的可用性。
以Swift为例,信号量有两个核心的方法wait()和signal()。
- wait()方法用于减少信号量的计数,通常用于控制对有限资源的访问。
- signal()方法用于增加信号量的计数,通常在资源使用完毕后调用。
信号量的用法
信号量使用很简单,因为它只有三个方法:
- 创建信号量:DispatchSemaphore(value: 1)
- 减少信号量计数:wait()
- 增加信号量计数:signal()
但是知道在什么场景来灵活使用它却并不容易,最简单的用法就是通过初始化一个计数值为1的信号量,将异步操作变为同步操作来保证线程安全。这种方式适用于需要确保只有一个线程能访问共享资源的场景,比如读写文件或更新数据库记录的操作。
代码如下:
func semaphore() {
let semaphore = DispatchSemaphore(value: 1)
let concurrentQueue = DispatchQueue(label: "com.example.concurrentQueue", attributes: .concurrent)
concurrentQueue.async {
semaphore.wait()
print("Task 1")
sleep(1)
semaphore.signal()
}
concurrentQueue.async {
semaphore.wait()
print("Task 2")
sleep(1)
semaphore.signal()
}
}
上面的代码虽然是并发异步,但由于信号量的存在Task 2 仍然会在Task 1执行完1秒后才执行。
但是如果我们将初始化计数改为2,则Task 1和Task 2会同时执行。
实际应用场景
在应用的实际开发中有很多场景都需要使用到信号量来保证线程安全,提升程序性能,下面我们就来列举几个典型的场景。
多个窗口售票问题
我们首先来列举一个比较典型的多个窗口同时售票的场景。
假设有3个窗口同时出售同一场电影的门票,门票总数为100张,如果我们不使用信号量来保证线程安全,实现代码如下:
/// 总票数
private var ticketCount = 100
//MARK: 开始售票
func startSaleTicket() {
print("开始售票 当前线程:\(Thread.current)")
ticketCount = 100
let queue1 = DispatchQueue(label: "com.example.queue1")
let queue2 = DispatchQueue(label: "com.example.queue2")
let queue3 = DispatchQueue(label: "com.example.queue3")
queue1.async {
self.saleTicket()
}
queue2.async {
self.saleTicket()
}
queue3.async {
self.saleTicket()
}
}
//MARK: 模拟售票
private func saleTicket() {
while true {
if ticketCount <= 0 {
print("所有票已售完")
break
}
ticketCount = ticketCount - 1
print("剩余票数:\(ticketCount) 窗口:\(Thread.current)")
}
}
执行结果如下:
开始售票 当前线程:<_NSMainThread: 0x600000b60500>{number = 1, name = main}
剩余票数:98 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:97 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:96 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:94 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:93 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:92 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:91 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:90 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:89 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:88 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:87 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:86 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:85 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:84 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:93 窗口:<NSThread: 0x600000b2c100>{number = 10, name = (null)}
剩余票数:99 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:81 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:82 窗口:<NSThread: 0x600000b2c100>{number = 10, name = (null)}
剩余票数:83 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:78 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:79 窗口:<NSThread: 0x600000b2c100>{number = 10, name = (null)}
剩余票数:80 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:76 窗口:<NSThread: 0x600000b2c100>{number = 10, name = (null)}
剩余票数:77 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:74 窗口:<NSThread: 0x600000b2c100>{number = 10, name = (null)}
剩余票数:72 窗口:<NSThread: 0x600000b2c100>{number = 10, name = (null)}
剩余票数:73 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:71 窗口:<NSThread: 0x600000b2c100>{number = 10, name = (null)}
剩余票数:69 窗口:<NSThread: 0x600000b2c100>{number = 10, name = (null)}
剩余票数:70 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:75 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:67 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:66 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:68 窗口:<NSThread: 0x600000b2c100>{number = 10, name = (null)}
剩余票数:65 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:64 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:63 窗口:<NSThread: 0x600000b2c100>{number = 10, name = (null)}
剩余票数:62 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:61 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:59 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:57 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:58 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:56 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:55 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:60 窗口:<NSThread: 0x600000b2c100>{number = 10, name = (null)}
剩余票数:53 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:52 窗口:<NSThread: 0x600000b2c100>{number = 10, name = (null)}
剩余票数:54 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:50 窗口:<NSThread: 0x600000b2c100>{number = 10, name = (null)}
剩余票数:51 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:49 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:48 窗口:<NSThread: 0x600000b2c100>{number = 10, name = (null)}
剩余票数:47 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:45 窗口:<NSThread: 0x600000b2c100>{number = 10, name = (null)}
剩余票数:44 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:43 窗口:<NSThread: 0x600000b2c100>{number = 10, name = (null)}
剩余票数:42 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:41 窗口:<NSThread: 0x600000b2c100>{number = 10, name = (null)}
剩余票数:46 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:40 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:38 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:39 窗口:<NSThread: 0x600000b2c100>{number = 10, name = (null)}
剩余票数:37 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:36 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:34 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:32 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:35 窗口:<NSThread: 0x600000b2c100>{number = 10, name = (null)}
剩余票数:33 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:31 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:30 窗口:<NSThread: 0x600000b2c100>{number = 10, name = (null)}
剩余票数:28 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:29 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:27 窗口:<NSThread: 0x600000b2c100>{number = 10, name = (null)}
剩余票数:26 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:25 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:24 窗口:<NSThread: 0x600000b2c100>{number = 10, name = (null)}
剩余票数:23 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:22 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:20 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:21 窗口:<NSThread: 0x600000b2c100>{number = 10, name = (null)}
剩余票数:18 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:17 窗口:<NSThread: 0x600000b2c100>{number = 10, name = (null)}
剩余票数:16 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:14 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:19 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:13 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:15 窗口:<NSThread: 0x600000b2c100>{number = 10, name = (null)}
剩余票数:11 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:10 窗口:<NSThread: 0x600000b2c100>{number = 10, name = (null)}
剩余票数:12 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:9 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:8 窗口:<NSThread: 0x600000b2c100>{number = 10, name = (null)}
剩余票数:7 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:6 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:5 窗口:<NSThread: 0x600000b2c100>{number = 10, name = (null)}
剩余票数:4 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
剩余票数:3 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
剩余票数:2 窗口:<NSThread: 0x600000b2c100>{number = 10, name = (null)}
所有票已售完
剩余票数:0 窗口:<NSThread: 0x600000b3c6c0>{number = 8, name = (null)}
所有票已售完
剩余票数:1 窗口:<NSThread: 0x600000b39c00>{number = 9, name = (null)}
所有票已售完
仔细看结果就会发现在剩余84张票之后,又突然剩余93张了,93张之后又突然剩余99张了,这明显是不符合正常销售场景的,因为票只会越来越少才对。而在最后有的窗口已经表示没有剩余票数了,有的窗口却又卖出去一张,这显然是不符合正常售票流程的。
下面我们就用信号量加锁来改善这个问题,代码如下:
/// 总票数
private var ticketCount = 100
/// 信号量
private var semaphore = DispatchSemaphore(value: 1)
//MARK: 开始售票
func startSaleTicket() {
print("开始售票 当前线程:\(Thread.current)")
ticketCount = 100
let queue1 = DispatchQueue(label: "com.example.queue1")
let queue2 = DispatchQueue(label: "com.example.queue2")
let queue3 = DispatchQueue(label: "com.example.queue3")
queue1.async {
self.saleTicket()
}
queue2.async {
self.saleTicket()
}
queue3.async {
self.saleTicket()
}
}
//MARK: 模拟售票
private func saleTicket() {
while true {
semaphore.wait()
if ticketCount <= 0 {
print("所有票已售完")
semaphore.signal()
break
}
ticketCount = ticketCount - 1
print("剩余票数:\(ticketCount) 窗口:\(Thread.current)")
semaphore.signal()
}
}
结果如下:
开始售票 当前线程:<_NSMainThread: 0x600000bf01c0>{number = 1, name = main}
剩余票数:99 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:98 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:97 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:96 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:95 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:94 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:93 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:92 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:91 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:90 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:89 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:88 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:87 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:86 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:85 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:84 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:83 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:82 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:81 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:80 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:79 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:78 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:77 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:76 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:75 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:74 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:73 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:72 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:71 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:70 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:69 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:68 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:67 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:66 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:65 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:64 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:63 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:62 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:61 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:60 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:59 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:58 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:57 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:56 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:55 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:54 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:53 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:52 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:51 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:50 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:49 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:48 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:47 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:46 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:45 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:44 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:43 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:42 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:41 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:40 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:39 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:38 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:37 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:36 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:35 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:34 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:33 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:32 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:31 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:30 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:29 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:28 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:27 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:26 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:25 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:24 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:23 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:22 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:21 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:20 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:19 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:18 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:17 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:16 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:15 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:14 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:13 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:12 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:11 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:10 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:9 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:8 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:7 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:6 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:5 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:4 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:3 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
剩余票数:2 窗口:<NSThread: 0x600000ba0080>{number = 9, name = (null)}
剩余票数:1 窗口:<NSThread: 0x600000ba6c00>{number = 10, name = (null)}
剩余票数:0 窗口:<NSThread: 0x600000ba6b40>{number = 8, name = (null)}
所有票已售完
所有票已售完
所有票已售完
这次我们发现,虽然三个线程同时售票,但票的数量没有任何错乱问题。
线程安全的队列
在项目的实际开发中,一个线程安全的队列使用频率还是非常高的,当有过多的消息或者是动画需要处理时,用于性能原因或者其他方面考虑,我们并不会一次性让所有内容都展示出来,而是通过让它们进入一个队列,再从队列内一个一个取出来处理或者是播放。但我们并不能保证每个消息或者是动画资源都是从同一个线程来的,因此我们需要一定的手段来保证队列的线程安全。
import Foundation
class SafeQueue<T> {
private var queue: [T] = []
private let semaphore = DispatchSemaphore(value: 1) // 信号量初始值为1,表示只有一个线程可以访问队列
// 入队操作
func enqueue(_ element: T) {
semaphore.wait() // 请求访问权限
queue.append(element)
semaphore.signal() // 释放访问权限
}
// 出队操作
func dequeue() -> T? {
semaphore.wait() // 请求访问权限
let element = queue.isEmpty ? nil : queue.removeFirst()
semaphore.signal() // 释放访问权限
return element
}
// 获取队列的大小
func size() -> Int {
semaphore.wait() // 请求访问权限
let size = queue.count
semaphore.signal() // 释放访问权限
return size
}
}
我们使用DispatchSemaphore(value: 1)来创建一个初始计数为1的信号量,这意味着队列的访问是互斥的,任何时刻只有一个线程能够执行enqueue或者dequeue操作。
enqueue方法,在添加元素到队列之前,线程会调用semaphore.wait()来请求访问权限。操作完成后,调用semaphore.signal()来释放权限,允许其他线程访问队列。
dequeue方法,在从队列移除元素之前,线程会调用semaphore.wait(),操作完成后,调用semaphore.signal()释放权限。
size方法,为了获取队列的大小,也使用了信号量来确保线程安全。
控制最大并发数
这个功能多用于数据下载和处理的场景中,假设我们有一个包含多个音频文件URL的数组,但要求最多同时进行3个下载任务,这时候使用信号量就可以很好的控制这一点。
let semaphore = DispatchSemaphore(value: 3) // 信号量初始值为3,表示最多允许3个并发下载任务
let queue = DispatchQueue.global()
let audioURLs = ["https://example.com/audio1.mp3", "https://example.com/audio2.mp3", "https://example.com/audio3.mp3", /* 其他URL */]
for url in audioURLs {
queue.async {
semaphore.wait() // 请求一个下载任务的名额
downloadAudio(from: url) {
print("Downloaded: \(url)")
semaphore.signal() // 释放下载任务的名额
}
}
}
func downloadAudio(from url: String, completion: @escaping () -> Void) {
// 模拟下载任务
sleep(2) // 模拟下载时间
completion()
}
信号量初始化时设置计数值为3,这意味着同时最多有3个线程可以访问资源(即开始下载)。
wait()方法,当一个线程要开始下载任务时,它需要调用wait()来减少信号量的计数。如果计数为0,表示已经有3个任务在运行,该线程会被阻塞,直到某个任务完成并调用signal()方法。
signal()方法,下载任务完成后,调用signal()增加信号量的计数,允许下一个被阻塞的线程开始下载。
注意事项
信号量作为一种强大的同步工具,可以帮助我们有效地管理并发任务和控制对共享资源的访问。然而,在使用信号量时,仍需注意以下几点:
避免死锁
在使用信号量时,必须确保wait()和signal()调用配对正确,如果一个线程在调用wait()后未能在适当的时候调用signal(),或者连续调用两次wait(),可能会导致其他线程永久阻塞,从而造成死锁。
合理设置信号量初始值
初始化信号量时,需要根据实际需求设定合理的初始计数值。如果初始值设置过高,可能导致资源被过度占用,设置过低则可能限制了并发性能。
性能考虑
虽然信号量时一种有效的同步机制,但在高并发场景下,频繁的信号量操作可能会带来性能开销。在某些情况下,考虑使用其他同步机制(如NSLock、DispatchGroup等)可能会更加合适。
结语
在本篇博客中,我们详细讨论了GCD信号量的工作原理及其应用场景。通过合理的使用GCD信号量,可以有效地管理并发任务,提升程序的性能和稳定性。然而,我们在使用信号量时需要注意其潜在的挑战,以避免引发性能问题或同步错误。