Swift多线程:GCD进阶,单例、信号量、任务组
其实这个标题不知道怎么写了,都很碎,也没有想到特别合适的例子能够全部放在一起的。索性就这么平铺开吧。
image.png1. dispatch_once,以及Swift下的单例
使用dispatch_once函数能保证某段代码在程序运行过程中只被执行1次。所以在通常在OC时代,我们都会用它来写单例。
但是,但是,但是:这个函数在Swift3.0以后的时代已经被删除了。没错,被删除了,不用了。
原来自从Swift 1.x开始Swift就已经开始用dispatch_one机制在后台支持线程安全的全局lazy初始化和静态属性。static var背后已经在使用dispatch_once了,所以从Swift 3开始,就干脆把dispatch_once显式的取消了。
凸(艹皿艹 ),那Swift里面的单例怎么写呐?其实方法有很多种,有OC心Swift皮的写法、新瓶装老酒的写法,那既然咱们开始了Swift,就抛下过去那写沉重包袱吧。这里非典型技术宅只分享其中的一种。
final class SingleTon: NSObject {
static let shared = SingleTon()
private override init() {}
}
什么?你在搞事情吧,就这么点?是的,因为是全局变量,所以只会创建一次。
- 使用
final
,将这个单例类终止继承。 - 设置初始化方法为私有,避免外部对象通过访问init方法创建单例类的实例。
2. dispatch_after
在GCD中我们使用dispatch_after()函数来延迟执行队列中的任务。准确的理解是,等到指定的时间到了以后,才会开辟一个新的线程然后立即执行队列中的任务。
所以dispatch_after不会阻塞当前任务,并不是先把任务加到线程里面,等时间到了在执行。而是等时间了,才加入到线程中。
我们使用两种时间格式来看看。
方法一:使用相对时间,DispatchTime
@IBAction func delayProcessDispatchTime(_ sender: Any) {
//dispatch_time用于计算相对时间,当设备睡眠时,dispatch_time也就跟着睡眠了.
//Creates a `DispatchTime` relative to the system clock that ticks since boot.
let time = DispatchTimeInterval.seconds(3)
let delayTime: DispatchTime = DispatchTime.now() + time
DispatchQueue.global().asyncAfter(deadline: delayTime) {
Thread.current.name = "dispatch_time_Thread"
print("Thread Name: \(String(describing: Thread.current.name))\n dispatch_time: Deplay \(time) seconds.\n")
}
}
方法二:使用绝对时间,DispatchWallTime
@IBAction func delayProcessDispatchWallTime(_ sender: Any) {
//dispatch_walltime用于计算绝对时间。
let delaytimeInterval = Date().timeIntervalSinceNow + 2.0
let nowTimespec = timespec(tv_sec: __darwin_time_t(delaytimeInterval), tv_nsec: 0)
let delayWalltime = DispatchWallTime(timespec: nowTimespec)
//wallDeadline需要一个DispatchWallTime类型。创建DispatchWallTime类型,需要timespec的结构体。
DispatchQueue.global().asyncAfter(wallDeadline: delayWalltime) {
Thread.current.name = "dispatch_Wall_time_Thread"
print("Thread Name: \(String(describing: Thread.current.name))\n dispatchWalltime: Deplay \(delaytimeInterval) seconds.\n")
}
}
3. 队列的循环、挂起、恢复
3.1 dispatch_apply
dispatch_apply函数是用来循环来执行队列中的任务的。在Swift 3.0里面对这个做了一些优化,使用以下方法:
public class func concurrentPerform(iterations: Int, execute work: (Int) -> Swift.Void)
本来循环执行就是为了节约时间的嘛,所以默认就是用了并行队列。我们尝试一下用这个升级版的dispatch_apply
让它执行10次打印任务。
@IBAction func useDispatchApply(_ sender: Any) {
print("Begin to start a DispatchApply")
DispatchQueue.concurrentPerform(iterations: 10) { (index) in
print("Iteration times:\(index),Thread = \(Thread.current)")
}
print("Iteration have completed.")
}
运行结果如下:
image.png看,是不是所有的任务都是并行进行的?标红的地方,是非典型技术宅想提醒一下大家这里还是有一些任务是在主线程中进行的。它循环执行并行队列中的任务时,会开辟新的线程,不过有可能会在当前线程中执行一些任务。
如果需要循环的任务里面有特别耗时的操作,我们上一篇文章里面说是应该放在global里面的。如何避免在主线程操作这个呐???
来,给三秒时间想想。
看到调用这个方法的时候是不是就是在UI线程里面这么写下来的嘛?那就开启一个gloablQueue,让它来进行不就好了嘛!BINGO!
这位同学,你已经深得真谛,可以放学后到我家后花园来了。嘿嘿✧(≖ ◡ ≖✿)嘿嘿
3.2 队列的挂起与唤醒
如果一大堆任务执行着的时候,突然后面的任务不想执行的。那怎么办呐?我们可以让它暂时先挂起,等想好了再让它们运行起来。
不过挂起是不会暂停正在执行的队列的哈,只能是挂起还没执行的队列。
@IBAction func useDispatchSuspend(_ sender: Any) {
let queue = DispatchQueue(label: "new thread")
// 挂起
queue.suspend()
queue.async {
print("The queue is suspended. Now it has completed.\n The queue is \"\(queue.label)\". ")
}
print("The thread will sleep for 3 seconds' time")
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.seconds(3)) {
// 唤醒,开始执行
queue.resume()
}
}
image.png
我们也可以看一下控制条的打印结果。显然能看到代码并没有按照顺序执行,新建的queue里面的打印是在被唤醒之后才执行的。
4. 信号量(semaphore)
信号量这个东西在之前的文章里面有一个例子里面用到了,当时还有人专门问我semaphore
是什么东西。现在可以好好说一说这个了。
不要问我是哪个例子里面用到了,实在想不起来了呀,只能记得有人问过semaphore
这个。
有时候多个线程对一个数据进行操作的时候,会造成一些意想不到的效果。多个人同时对同一个数据进行操作,谁知道怎么搞啊!
为了保证同时只有一个线程来修改这个数据,这个时候我们就要用到信号量了。当信号量为0的时候,其他线程想要修改或者使用这个数据就必须要等待了,等待多久呐?DispatchTime.distantFuture
,要等待这么久。意思就是一直等待下去。。。。OC里面管这个叫做DISPATCH_TIME_FOREVER
。
如果给信号量设置成了0,其实就意味着这个资源没有人能够再能用了。所以,当用完了之后一定要把信号量设置成非0( ⊙ o ⊙ )!
//创建一个信号量,初始值为1
let semaphoreSignal = DispatchSemaphore(value: 1)
//表示信号量-1
semaphoreSignal.wait()
//表示信号量+1
semaphoreSignal.signal()
4.1 简单实用一下
我们简单的让globalQueue
这个全局队列按照1->5的顺序进行打印,打印一次休息1秒钟。
@IBAction func useSemaphore(_ sender: Any) {
let semaphoreSignal = DispatchSemaphore(value: 1)
for index in 1...5 {
DispatchQueue.global().async {
semaphoreSignal.wait()
print(Thread.current)
print("这是第\(index)次执行.\n")
semaphoreSignal.signal()
}
print("测试打印")
}
}
看一下打印结果:
image.pngglobalQueue
如果不加信号量,正常打印是什么样子的?如果不记得,请看上一篇文章。iOS多线程系列之三:使用GCD实现异步下载图片。
好奇宝宝们有没有想过,在创建信号量的时候初始值设置成2或者更大的数,例如50,会是什么效果? 自己敲敲代码试试喽,想想看。
4.2 多个线程之间进行任务协调
实际工作中,很多时候我们需要在多个任务之间进行协调,每个任务都是多线程的。
打个比方,我们在后台下载音乐、专辑的封面。等着两个都做完了,才通知用户可以去听音乐了。两个任务都是多线程,我们其实并不知道什么时候才能执行完毕。这个时候,就可以靠信号量,让大家互相等待。
为了更简化这个过程,例子里面模拟了一个在另外一个方法中需要耗时1秒的一个操作。当完成之后,才执行后续操作。
func semaphoreDemo() -> Void {
let sema = DispatchSemaphore.init(value: 0)
getListData { (result) in
if result == true {
sema.signal()
}
}
sema.wait()
print("我终于可以开始干活了")
}
private func getListData(isFinish:@escaping (Bool) -> ()) {
DispatchQueue.global().async {
Thread.sleep(forTimeInterval: 1)
print("global queue has completed!")
isFinish(true)
}
}
这个例子不是用group也可以做嘛?!是哒。也可以。
5. 任务组
GCD的任务组在开发中是经常被使用到,当需要一组任务结束后再执行一些操作时,就可以用它啦。
DispatchGroup的职责就是当队列中的所有任务都执行完毕后,会发出一个通知来告诉告诉大家,任务组中所执行的队列中的任务执行完毕了。
既然是组,里面就肯定有很多队列啦,不然怎么能叫做“组”呐。
队列和组关联有两种方式:手动、自动。
5.1 自动关联
肯定先从自动开始了,因为通常自动最省事啊。这还用问嘛。
@IBAction func useGroupQueue(_ sender: UIButton) {
let group = DispatchGroup()
//模拟循环建立几个全局队列
for index in 0...3 {
//创建队列的同时,加入到任务组中
DispatchQueue.global().async(group: group, execute: DispatchWorkItem.init(block: {
Thread.sleep(forTimeInterval: TimeInterval(arc4random_uniform(2) + 1))
print("任务\(index)执行完毕")
}))
}
//组中所有任务都执行完了会发送通知
group.notify(queue: DispatchQueue.main) {
print("任务组的任务都已经执行完毕啦!")
}
print("打印测试一下")
}
看看打印结果:
image.png5.2 手动关联
接下来我们将手动的管理任务组与队列中的关系。
enter()
,leave()
是一对儿。前者表示进入到任务组。后者表示离开任务组。
let manualGroup = DispatchGroup()
//模拟循环建立几个全局队列
for manualIndex in 0...3 {
//进入队列管理
manualGroup.enter()
DispatchQueue.global().async {
//让线程随机休息几秒钟
Thread.sleep(forTimeInterval: TimeInterval(arc4random_uniform(2) + 1))
print("-----手动任务\(manualIndex)执行完毕")
//配置完队列之后,离开队列管理
manualGroup.leave()
}
}
//发送通知
manualGroup.notify(queue: DispatchQueue.main) {
print("手动任务组的任务都已经执行完毕啦!")
}
image.png
利用任务组可以完成很多场景的工作。例如多任务执行完后,统一刷新UI。把刷新UI的操作放在notify
里面就好了。
还记得刷新UI用哪个queue嘛?hoho~
最后,所有的代码都放在这里了:gitHub 下载后给颗Star吧~ 么么哒~(~o ̄3 ̄)~ 爱你们~
iOS多线程系列之一:Operation基础操作,按优先级加载图片
iOS多线程系列之二:Operation实例,异步加载CollectionView图片
iOS多线程系列之三:使用GCD实现异步下载图片
iOS多线程系列之四:GCD进阶,单例、信号量、任务组
iOS多线程系列之五:使用Thread进行多线程间通讯,协调子线程任务