美文网首页Swift
Swift进阶 - Concurrency之GCD

Swift进阶 - Concurrency之GCD

作者: Joshua666 | 来源:发表于2019-10-17 12:05 被阅读0次

如果看完了之前写的Swift初学中的文章,应该对swift的基础有了一定的掌握。现在我们讨论一下进阶一些的知识,当你写一个比较复杂的app时,需要经常进行网络请求,就需要好好的架构你的app,不要让你的app因为长时间的请求出现卡顿的情况。这时候就需要考虑app的Concurrency。

Swift提供了两个apis:GCDOperations,其中Operation是以GCD为基础实现的。

一、GCD - Grand Central Dispatch

let queue = DispatchQueue(label: "com.test.test")
queue.async {
    let a = 5 + 5
    print(a)
    
    DispatchQueue.main.async {
        print("main\(a)")
    }
}

gcd就是把你写在closure里的代码放到队列中,这个队列根据你给的参数,可以是serial(串行)也可以是concurrent(并发)的。serial queue只有一个thread,所以这里面的任务每次执行一个,这一个结束了才会执行下一个;concurrent queue就会根据资源分配多个threads,可以同时执行多个任务。注意以下几点:

1、GCD中的所有queue都是按照先进先出FIFO(first-in, first-out)的顺序,但是这指的是先进来的先执行,并不是先进来先完成,因为每个task的所需时间是不一样的;

2、当你执行queue.async的时候,你的代码也不一定会并发进行!如果你的queue是一个serial queue,只有一个thread,你在怎么async,它也只能一个一个执行。

3、label得是一个unique的string,你可以用“com.your-domain.xxx”的形式。

4、当你的app开始运行时,系统会自动创建一个主queue,就是例子中的DispatchQueue.main,这是serial queue,主要是用来呈现UI的。不要把一个sync任务推给main,会卡UI的

5、DispatchQueue默认会创建一个serial queue,你要这样定义来获取concurrent queue

let queue = DispatchQueue(label: "com.test.test", attributes: .concurrent)

6、Global concurrent queues

当你需要concurrent queue时,你可以用上面的语句来定义一个你自己的,但是通常情况下,用系统本来就提供的global concurrent queue就行了:

let queue = DispatchQueue.global(qos: .utility)

qos(Quality of service)是指你这个queue中的任务的优先级,一共有6中:

  • .userInteractive - 这个任务与UI相关的时候,比如动画或者更新UI所需的逻辑运算等可以用这个,我们可不想因为复杂的运算影响UI的流畅性!
  • .userInitiated - 这个是当用户触发了一个事件,我们需要立即执行相关逻辑任务,但这个任务又可以并发进行的时候,比如用户点了一个button
  • .utility - 当你需要用一些progress bar或hud来显示加载中来执行此任务时,一般就是用这个了
  • .background - 当这个任务用户完全不需要知道
  • .default和.unspecified - 一般用不着,不讲了

简单例子

假设没有像SDWebImage的第三方库,你要写一个加载网络图片的uiCollectionView怎么写?这时候肯定要用到并发执行任务,不然肯定卡死(maybe not with 5g, but I don't care)

func loadImage(indexPath: IndexPath) {
    let queue = DispatchQueue.global(qos: .utility) // a
    queue.async {
      [weak self] in // b
      guard let self = self else { // c
        return
      }
      if let data = try? Data(contentsOf: self.urls[indexPath.row]), 
        let image = UIImage(data: data) {
        DispatchQueue.main.async {
          if let cell = self.collectionView.cellForItem(at: indexPath) 
                    as? PhotoCell { // d
            cell.display(image: image)
          }
        }
      }
    }
  }

a) 不知道图片都多大,所以也不知道加载一张图片需要多长时间,为了性能和电池,我们要用.utility
b) 这里在gcd的async直接用self也不会出现retain cycle,因为closure在执行完之后会释放内存,但self会被延长“寿命”。
c) 需要检查self是不是nil,万一加载出来之前self已经dismiss了。按照b里所说的,如果没用weak self,在加载出来前就不会dismiss
d) 在async里,我们不能直接传入cell,因为当async里的代码执行时,你并不知道这个cell的状态,有可能已经没了,也有可能被换了,所以我们要传入indexpath,然后获取实时的cell

7、Dispatch Group

假设你需要同时向服务器进行多次请求,只有这几个请求都完成时,你才能更新UI,这时候你就用DispatchGroup来解决:

let group = DispatchGroup()
let queue = DispatchQueue.global(qos: .utility)
queue.async(group: group) {
    print(2^5)
}
queue.async(group: group) {
    print(2^32)
}
group.notify(queue: DispatchQueue.main) {
    print("finished")

finished只有在两个数都算完的时候才会被print。除了notify,group还有一个方法是wait - 如果用了group.wait(),它会把当前的queue给block,直到运行完group里的任务。wait还可以加一个timeout参数, 多少秒后,如果group中的任务还没执行完,那么正常继续执行当前queue后面的任务。举个例子就明白了

let group = DispatchGroup()
let queue = DispatchQueue.global(qos: .utility)
queue.async(group: group) {
    Thread.sleep(until: Date().addingTimeInterval(2))
    print("AAAAA")
}
queue.async(group: group) {
    Thread.sleep(until: Date().addingTimeInterval(5))
    print("BBBBBB")
}
if group.wait(timeout: .now()+3) == .timedOut {
        print("不等了")
}
print("排队中...")
// AAAAA
// 不等了
// 排队中...
// BBBBBB

注意print的顺序!不要在main queue用group.wait()

8、@escaping

当func的其中一个参数是closure的时候,有两种情况:
a) 当func执行时会把closure的代码也执行,完成时,closure就不存在了

func sum(_ arr: [Int], handler: (Int) -> ()) {
    print("doing sum...")
    let total = arr.reduce(0, +)
    handler(total)
    print("finish")
}

sum([1, 2, 3]) {
    total in
    print(total)
}
// doing sum...
// 6
// finish

b) 当func执行完时,closure中的代码还没有执行完,这时候得让系统知道这个closure你要给我保存,不要在func执行完的时候把它从内存中毁掉,这时候就要加@escaping

func sum(_ arr: [Int], handler: @escaping (Int) -> ()) {
    print("doing sum...")
    let total = arr.reduce(0, +)
    queue.async {
        handler(total)
    }
    print("finish")
}

sum([1, 2, 3]) {
    total in
    Thread.sleep(until: Date().addingTimeInterval(1))
    print(total)
}
// doing sum...
// finish
// 6

9、group.enter()和group.leave()

看完第7、8条后,再看看下面的代码,你觉得print出的顺序是怎样的?

func sum(_ arr: [Int], handler: @escaping (Int) -> ()) {
    print("doing sum...")
    let total = arr.reduce(0, +)
    queue.async {
        handler(total)
    }
    print("finish")
}

queue.async(group: group) {
    sum(Array(1...2^32)) {
        total in
        Thread.sleep(until: Date().addingTimeInterval(2))
        print("total: \(total)")
    }
}

group.notify(queue: DispatchQueue.main) {
    print("都完成了")
}

我们在第7条中知道group.notify会在所有在group中的queue里的任务都完成之后执行。但是上面这个例子会以一下顺序print

doing sum...
finish
都完成了
total: 595

因为queue执行的sum中还有一个async call,在这个async call还没完成的时候,notify就执行了!如果想解决这个问题,那么我们就可以用到group的两个方法:enter()leave()

func sum(_ arr: [Int], handler: @escaping (Int) -> ()) {
    print("doing sum...")
    let total = arr.reduce(0, +)
    group.enter()
    queue.async {
        defer { group.leave() }
        handler(total)
    }
    print("finish")
}
// doing sum...
// finish
// total: 595
// 都完成了

在执行sum里的async之前,call了enter()告诉group我还有代码没完成
a) 你要是call了enter()就要记得call leave()
b) 因为有时候你还需要handle error在async的结果中,所以用了defer为了不漏写leave

10、Semaphore

如果资源有限,想限制可以并发进行的任务,可以用semaphore,用法如下:

let semaphore = DispatchSemaphore(value: 2)
for i in 1...5 {
    semaphore.wait()
    queue.async(group: group) {
        defer { semaphore.signal() }
        print("start\(i)")
        Thread.sleep(until: Date().addingTimeInterval(1))
        print("end\(i)")
    }
}

group.notify(queue: DispatchQueue.main) {
    print("all finished")
}

// print结果
start1
start2
end1
end2
start3
start4
end4
end3
start5
end5
all finished

相关文章

网友评论

    本文标题:Swift进阶 - Concurrency之GCD

    本文链接:https://www.haomeiwen.com/subject/rzrmmctx.html