为什么要用GCD-Swift

为什么要用GCD-Swift

当今世界,多核已然普及。但是APP却不见得很好的跟上了这个趋势。APP

想要利用好多核就必须可以保证任务能有效的分配。并行执行可以让APP同时执行很多的任务。这个其实很难,但是有了GCD一切都变得简单了很多。

你并不是一定要写一个大并发的APP才需要用GCD。使用GCD可以让你的APP更快的响应用户的操作,不用要等到你的UI或者服务等到执行完成。一般来说你会把各种任务都分配给其他核心去执行,而你的主线程(UI线程)可以随时处理用户的操作。

GCD可以让这些变得简单。

线程

传统的多任务分发方式是使用线程。在一个多核的设备上,每一个新的线程都可以被分配在一个CPU核心上,与其他的线程并行执行。

单核心的CPU麻烦一些,系统会不停地在几个线程之间切换以保证每个线程都有机会执行。
这样做的效果是看起来像是并行执行的,但是其实多个线程的不同任务之间是顺序执行的。

但是使用线程也会遇到一个很大的问题。数据在线程之间的正确传递就是一个很大的难题了。线程之间的同步和互斥又会变得很诡异难以调试。而且,为了保证APP的高效快速运行,开辟多少线程也是一个需要思考的问题。因为,创建和销毁线程也是有很大的资源消耗的。于是很多的系统都提供了一个叫做线程池的概念来解决这个问题。所有的线程创建后都放在这个池子里统一管理。

同步

当你有多个线程在执行的时候,你一般都会遇到一个问题Race Condition,实际的运算结果取决于哪个线程先获得共享数据。一个经典的例子就是:银行账户问题。


class BankAccount {

  var balance: Double?

  //...

}

// 创建一个账号,给这个账号存100块
var account = BankAccount()
account.balance = 100

// 第一个线程:取10块
func withdraw() {

  let balance = account.balance
  account.balance = balance! - 10

}

// 第二个线程:增加10%的利息
func accrue() {

  let balance = account.balance
  account.balance = balance! * 1.10

}

// 那么最后:account.balance = ?

最后的结果取决于这两个线程哪一个先执行。在并行的条件下执行的先后顺序是不定的。但是执行顺序不同最后的balance值就是不同的。

上面的代码会有多少个可能的不同结果?balance都会是什么值?
Race Condition即使在单核设备下也会发生。

对于这些问题,传统的解决方法如下:

  • 信号量(semaphore)- 用来控制一组有限资源的消费。线程等待,知道资源可用。

  • 互斥(Mutex)- 一次只允许一个线程执行。当一个线程持有mutex的时候其他线程等待。

  • 条件变量(Condvar)- 线程等待直到某些条件为真。

队列

GCD把线程的创建、回收以及线程的同步等进一步抽象为队列(Queue)统一管理。简单而言,队列就是一个可以让数据按照先进先出的顺序执行。一个APP可以创建多个队列,并且多个队列之间可以并行的处理各自的任务,每个队列内部的任务顺序执行。

队列比线程有很多的优势。

  1. GCD库屏蔽了线程管理的繁琐部分。队列会在需要的时候自动创建线程并且在不需要的时候回收。
  2. GCD库会根据系统的CPU核心数创建最佳数量的线程。最后,队列只会在需要的时候创建线程,所以资源利用会得到优化。

总之,队列给你了你线程能给的,但是又不用考虑具体线程的操作。

GCD有三种队列:

  • 顺序队列(Serial)- 每次执行一个任务,按照任务加入队列的顺序。一个任务执行完成后执行下一个。

  • 并发(Concurrent)- 按照任务加入队列的顺序开始,但是后面的任务不用等到前面的任务执行完成就可以开始。

  • 主线程(Main)- 一个预先开启的序列线程。这个队列中包含一个NSRunLoop实例。你的APP总是在这个队列中运行。

系统提供了几种并发队列。这些队列有自己专属的QoS(服务质量种类)。这个服务质量种类是用来表示你提交的任务的意图是什么,这样GCD可以有针对性的优化。

  • QOS_CLASS_USER_INTERACTIVE 这个用户交互(user interactive)表示任务需要立即执行,以便APP给用户一个良好的用户体验。一般用于更新UI、处理事件和小延迟的处理。在你的整个APP中,这以种类的任务应该保持一个较低的总量。

  • QOS_CLASS_USER_INITIATED 用户初始化(user initiated)表示任务是在UI初始化的,并且可以异步执行。这个一般用于处理用户等待的需要理解给出运行结果,或者任务需要理解完成用户交互的情况。

  • QOS_CLASS_UTILITY 通用(utility)表示长期执行的任务,一般来说用户可以见到任务执行的比例。一般用来处理大的计算、I/O、网络以及不简单的数据提交等类似任务。这一种类做了电量优化。

  • QOS_CLASS_BACKGROUND 后台运行(background)表示用户并不知道任务在运行中。这个一般用于数据的提前加载、维护,以及其他无需用户交互和任务完成时间无明显限制的情况。

这里需要注意一点,苹果的API也会用到这些全局分发的队列。所以,在这些队列里并不是只有你的任务在执行。当然,如前文所述你也可以创建你自己的队列。你可以选择4种全局队列,主队列,还有两种自定义队列可以选择。

Closure

队列的任务使用closure封装。

你也可以使用方法加入队列,不过这里只使用block。

这里有一个closure的例子,让你对closure有一个大概的印象。swift的closure就和Objective-C的block差不多。


func makeIncrementer() -> ((Int) -> Int) {
  func addOne(number: Int) -> Int {
    return 1 + number
  }

  return addOne
}

var increment = makeIncrementer()
increment(9)

上面的例子makeIncrementer返回一个closure,这个closure需要一个整型参数。

Hello dispatch world!

这里需要注意一点:Objective-C的block和Swift的closure。

Swift的closure和Objective-C的block是兼容的。所以你完全可以把Swift的closure传入Objective-C中需要block参数的方法中。Swift的closure和方法是同一类型的,所以你甚至可以把swift的方法名传递过去。

Closure和block的上下文处理语法基本一样。唯一不同的是变量在closure中直接就是可变的。也就是说Objective-C中的__block关键字在Swift的closure中是默认行为。

dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), {
  print("hello dispatch:- user interactive")
})

dispatch_async(dispatch_get_main_queue(), {
  print("hello dispatch:- main queue")
})

调用dispatch_async(),GCD就会给队列添加一个closure任务,然后继续代码的执行完全不会等待closure的结束。在我们的例子中,第一次dispatch_async是给QOS_CLASS_USER_INTERACTIVE的类型的全局队列添加了一个任务。第二次是给dispatch_get_main_queue,也就是主线程添加了一个任务。

下面看一个自创队列的例子:


@IBAction func concurrentAction(sender: AnyObject) {

  let concurrentQueue = dispatch_queue_create("concurrent.test.queue", 
                                      DISPATCH_QUEUE_CONCURRENT) //1

  for i in 0...1000 {
    dispatch_async(concurrentQueue){ //2
    NSThread.sleepForTimeInterval(1) //3
    print("print concurrent queue:- \(i)") //4
  }
}

这里一步一步的介绍一下:

  1. 创建一个名称为concurrent.test.queue的并发队列。

  2. 做一个1001次的循环,每个循环给这个队列添加一个closure。以上的写法只是一个简写,其实是这样的:


dispatch_async(concurrentQueue,{ //2
  NSThread.sleepForTimeInterval(1) //3
  print("print concurrent queue:- \(i)")
})
  1. 当前线程休眠一秒

Barriers

很多人看到这里就会想,并发队列在哪儿体现出来队列的概念了呢?这分明就是一个把一堆closure扔进去分开执行的或者Set(集合)而已嘛。

目前来看是的,但是当你遇到barrier的时候对列就真的变成队列了。你可以使用dispatch_barrier_syncdispatch_barrier_async两个方法把closure加入并发队列中。这个时候就很有意思了。扔进去的closure并不会立刻执行,而是要等。等到在这个closure之前扔进队列的全部closure都执行完成之后才开始执行。然后,在这个barrier的closure执行完成之后,在它后面扔进队列的closure才会被执行。可以把barrier的closure认为是一个特殊的点,在这个点的从前到后都是顺序执行的。除此之外的点,还是并行执行。

我们来看看具体的例子:

let concurrentQueue = dispatch_queue_create("concurrent.test.queue", 
                                    DISPATCH_QUEUE_CONCURRENT)

var count: Int = 0

for _ in 0...100 {
  dispatch_async(concurrentQueue){
  NSThread.sleepForTimeInterval(1)
  print("print concurrent queue:- \(count++)")
}

dispatch_barrier_async(concurrentQueue, {
  print("##ASYNC in barrier, concurrent queue - START")

  for _ in 1...10 {
    NSThread.sleepForTimeInterval(0.5)
  }

  print("##ASYNC in barrier, concurrent queue - END")
})

for _ in 0...100 {
  dispatch_async(concurrentQueue) {
    NSThread.sleepForTimeInterval(1)
    print("print concurrent queue:- \(count++)")
  }
}

dispatch_barrier_sync(concurrentQueue, {
  print("##SYNC in barrier, concurrent queue - START")

  for _ in 1...10 {
    NSThread.sleepForTimeInterval(0.5)
  }

  print("##SYNC in barrier, concurrent queue - END")
})

for _ in 0...100 {
  dispatch_async(concurrentQueue){
  NSThread.sleepForTimeInterval(1)
  print("print concurrent queue:- \(count++)")
}

注意barrier最好是用在自己创建的并发队列上。另外,dispatch_barrier_async的效果就和dispatch_async一样,而dispatch_barrier_sync就和dispatch_sync一样。dispatch_barrier_sync要分配的队列是当前队列的话会造成死锁。

读写锁

先看一个新闻累的单例的例子。这个例子的最初形态中不是一个线程安全的单例:

class NewsFeed {
  static let sharedInstance = NewsFeed() //1

  private init() {} //2
  private var _news: [String] = []

  var news: [String] {
    return _news
  }
  
  func addNews(n: String) {
    _news.append(n)
  }
}

有这么一个新闻的类。

  1. 实现一个单例。Swift的单例明显要比Objective-C简单了很多。是的,Swift的static属性自动内置了dispatch_once。所以,这么一样就实现了Swift的单例。

  2. init方法只能给类的内部调用,但是不能给外部在调用,否则就不是单例了。用户可以调用news属性来读取全部的新闻,也可以通过调用方法addNews来添加新闻。

当多个线程都可以访问addNews方法的时候,那么news属性读出来的新闻列表就有很大的可能是错的。我们现在使用barrier来确保这个功能可以正确的执行。解决办法就是任何的线程要添加新闻,那么就必须通过barrier这一道关口。在添加新闻的时候只有一个closure执行。其他的添加closure等待。

为了保证线程的安全,读取新闻的操作也只能在concurrentQueue上执行。但是这次我们是需要立即返回结果的没法使用dispatch_async。这个时候使用dispatch_sync就是最好的选择了。使用dispatch_sync可以等到方法执行完毕,并返回我们需要的结果。dispatch sync可以知道dispatch barrier的进度如何,添加了几条新闻。也可以让closure执行完成并返回。但是使用dispatch sync需要很小心。如前所述,如果你给dispatch sync分配的队列是当前正在运行的队列会造成死锁。因为调用会等待这个closure结束,这个closure甚至可能都没法开始。因为这个closure需要等到它前面运行的closure结束之后才能开始。

class NewsFeed {
  private var _news: [String] = []

  var news: [String] {
    var newsCopy: [String]!

    dispatch_sync(concurrentQueue){ //1
      newsCopy = self._news //2
    }

    return newsCopy
  }
}

下面逐一解释:

  1. dispatch sync同步分配到concurrentQueue上执行读取操作。
  2. 保存一份新闻的拷贝给newsCopy并返回。

现在这个新闻单例就是线程安全的了。下面我们看一下完整的代码:


import Foundation

class NewsFeed {

  static let sharedInstance = NewsFeed()

  private let concurrentQueue = dispatch_queue_create("newsfeed.queue.concurrent",
                      DISPATCH_QUEUE_CONCURRENT)

  private init() {}

  private var _news: [String] = []

  var news: [String] {
    var newsCopy: [String]!

    dispatch_sync(concurrentQueue){
        newsCopy = self._news
    }

    return newsCopy
  }

  func addNews(n: String) {
    dispatch_barrier_async(concurrentQueue){
      self._news.append(n)
      dispatch_async(dispatch_get_main_queue()){
        self.newsAddedNotification()
      }
    }
  }

  func newsAddedNotification() {
    // post notification
  }
}

to be continued...

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容

  • 尽管 Grand Central Dispatch (GCD)已经存在一段时间了,但并非每个人都知道怎么使用它。这...
    coderFamer阅读 7,360评论 1 16
  • 学习多线程,转载两篇大神的帖子,留着以后回顾!第一篇:关于iOS多线程,你看我就够了 第二篇:GCD使用经验与技巧...
    John_LS阅读 604评论 0 3
  • 荣辱不惊,看庭前花开花落;去留无意,任天上云卷云舒。 2017年4月14日 星期五 天气26度 深圳 晴天 23...
    日记女王阅读 496评论 0 0
  • 华灯初上,车厢里灯盏点起,爵士慵懒漪叠,塑料座椅都还反射着灯光。人声很弱,晚高峰结束公交车就很有失意老人的落寞。 ...
    雪山湖泊阅读 269评论 0 3
  • 自卑,将自己“深藏”于子袭布帘子之后,试图把可能的嘲笑和攻击挡在外面;殊不知,他的快乐也被这袭帘子所阻隔。直到帘子...
    性淡如菊阅读 321评论 0 6