Swift多线程:GCD进阶,单例、信号量、任务组1. dispatch_once,以及Swift下的单例2. dispatch_after3. 队列的循环、挂起、恢复4. 信号量(semaphore

其实这个标题不知道怎么写了,都很碎,也没有想到特别合适的例子能够全部放在一起的。索性就这么平铺开吧。

image.png

1. 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.png

globalQueue 如果不加信号量,正常打印是什么样子的?如果不记得,请看上一篇文章。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.png

5.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 ̄)~ 爱你们~


本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏大内老A

WCF如何克服HTTP传输协议的局限提供对不同消息传输模式的实现

WCF采用消息作为通信的唯一手段,它支持不同的消息交换模式(MEP:Message Exchange Pattern),比较典型的有以下三种MEP:One-Wa...

1666
来自专栏吴伟祥

Redis分布式锁的正确实现方式(Java版)

分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。本篇博客将介绍第二种方式,基于Redis...

512
来自专栏服务端技术杂谈

JAVA NIO内存泄漏

前言 写NIO程序时,经常使用ByteBuffer来读取写入数据,那使用ByteBuffer.allocate()还是ByteBuffer.allocateDi...

2778
来自专栏Golang语言社区

golang实现的反向代理

翻阅golang包手册的时候看到net/http/httputil有一个type ReverseProxy,这个不是反向代理吗!golang自带反向代理功能?好...

4987
来自专栏Java编程技术

使用线程池时候当程序结束时候记得调用shutdown关闭线程池

日常开发中为了便于线程的有效复用,线程池是经常会被用的工具,然而线程池使用完后如果不调用shutdown会导致线程池资源一直不会被释放。下面通过简单例子来说明该...

1002
来自专栏Java架构

一线互联网常见的 14 个 Java 面试题,你颤抖了吗程序员

跳槽不算频繁,但参加过不少面试(电话面试、face to face 面试),面过大 / 小公司、互联网 / 传统软件公司,面糊过(眼高手低,缺乏实战经验,挂掉)...

814
来自专栏码代码的陈同学

Java内存模型

Java内存模型(简称JMM)指定了JVM如何利用计算机内存(RAM)进行工作。JMM与整个计算机的模型类似,这个模型自然也包含内存模型,即Java内存模型(A...

1176
来自专栏Golang语言社区

morestack与goroutine pool

o语言的goroutine初始栈大小只有2K,如果运行过程中调用链比较长,超过的这个大小的时候,栈会自动地扩张。这个时候会调用到一个函数runtime.more...

4646
来自专栏JAVA高级架构

JAVA后端面试100 Q&A之第一篇

891
来自专栏吴伟祥

Java 的 I/O 类库的基本架构 转

I/O 问题是任何编程语言都无法回避的问题,可以说 I/O 问题是整个人机交互的核心问题,因为 I/O 是机器获取和交换信息的主要渠道。在当今这个数据大爆炸时代...

380

扫码关注云+社区