给 iOS 开发者的 GCD 用户手册

本文讲的是给 iOS 开发者的 GCD 用户手册,


Grand Central Dispatch,或者GCD,是一个极其强大的工具。它给你一些底层的组件,像队列和信号量,让你可以通过一些有趣的方式来获得有用的多线程效果。可惜的是,这个基于C的API是一个有点神秘,它不会明显的告诉你如何使用这个底层组件来实现更高层次的方法。在这篇文章中,我希望描述那些你可以通过GCD提供给你的底层组件来实现的一些用法。

后台工作

也许最简单的用法,GCD让你在后台线程上做一些工作,然后回到主线程继续处理,因为像那些属于 UIKit 的组件只能(主要)在主线程中使用。

在本指南中,我将使用 doSomeExpensiveWork() 方法来表示一些长时间运行的有返回值的任务。

这种模式可以像这样建立起来:

let defaultPriority = DISPATCH_QUEUE_PRIORITY_DEFAULT
let backgroundQueue = dispatch_get_global_queue(defaultPriority, 0)
dispatch_async(backgroundQueue, {
	let result = doSomeExpensiveWork()
	dispatch_async(dispatch_get_main_queue(), {
		//use `result` somehow
	})
})

在实践中,我从不使用任何队列优先级除了 DISPATCH_QUEUE_PRIORITY_DEFAULT 。这返回一个队列,它可以支持数百个线程的执行。如果你的耗性能的工作总是在一个特定的后台队列中发生,你也可用通过 dispatch_queue_create 方法来创建自己的队列。 dispatch_queue_create 可以创建一个任意名称的队列,无论它是串行的还是并行的。

注意每一个调用使用 dispatch_async ,不使用 dispatch_sync 。dispatch_async 在 block 执行前返回,而dispatch_sync 会等到 block 执行完毕才返回。内部的调用可以使用 dispatch_sync(因为不管它什么时候返回),但外部必须调用 dispatch_async (否则,主线程会被阻塞)。

创建单例

dispatch_once 是一个可以被用来创建单例的API。在 Swift 中它不再是必要的,因为 Swift 中有一个更简单的方法来创建单例。为了以后,当然,我把它写在这里(用 Objective-C )。

+ (instancetype) sharedInstance {
	static dispatch_once_t onceToken;
	static id sharedInstance;
	dispatch_once(&onceToken, ^{
		sharedInstance = [[self alloc] init];
	});
	return sharedInstance;
}

扁平化一个完整的block

现在 GCD 开始变得有趣了。使用一个信号量,我们可以让一个线程暂停任意时间,直到另一个线程向它发送一个信号。这个信号量,就像 GCD 其余部分一样,是线程安全的,并且他们可以从任何地方被触发。

当你需要去同步一个你不能修改的异步API时,你可以使用信号量解决问题。

// on a background queue
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0)
doSomeExpensiveWorkAsynchronously(completionBlock: {
    dispatch_semaphore_signal(semaphore)
})
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
//the expensive asynchronous work is now done

dispatch_semaphore_wait 会阻塞线程直到 dispatch_semaphore_signal 被调用。这就意味着 signal 一定要在另外一个线程中被调用,因为当前线程被完全阻塞。此外,你不应该在在主线程中调用 wait ,只能在后台线程。

在调用 dispatch_semaphore_wait 时你可以选择任意的超时时间,但是我倾向于一直使用 DISPATCH_TIME_FOREVER 。

这可能不是完全显而易见的,为什么你要把已有的一个完整的 block 代码变为扁平化,但它确实很方便。我最近使用的一种情况是,执行一系列必须连续发生的异步任务。这个使用这种方式的简单抽象被称作 AsyncSerialWorker :

typealias DoneBlock = () -> ()
typealias WorkBlock = (DoneBlock) -> ()

class AsyncSerialWorker {
    private let serialQueue = dispatch_queue_create("com.khanlou.serial.queue", DISPATCH_QUEUE_SERIAL)

    func enqueueWork(work: WorkBlock) {
        dispatch_async(serialQueue) {
            let semaphore = dispatch_semaphore_create(0)
            work({
                dispatch_semaphore_signal(semaphore)
            })
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
        }
    }
}

这一小类可以创建一个串行队列,并允许你将工作添加到 block 中。当你的工作完成后, WorkBlock 会调用 DoneBlock ,开启信号量,并允许串行队列继续。

限制并发 block 的数量。

在前面的例子中,信号量作为一个简单的标志,但它也可以被用来作为一种有限的资源计数器。如果你想在一个特定资源上打开特定数量的连接,你可以使用下面的代码:

class LimitedWorker {
    private let concurrentQueue = dispatch_queue_create("com.khanlou.concurrent.queue", DISPATCH_QUEUE_CONCURRENT)
    private let semaphore: dispatch_semaphore_t

    init(limit: Int) {
    	semaphore = dispatch_semaphore_create(limit)
    }

    func enqueueWork(work: () -> ()) {
        dispatch_async(concurrentQueue) {
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
            work()
            dispatch_semaphore_signal(semaphore)
        }
    }
}

这个例子从苹果的Concurrency Programming Guide拿来的。他们可以更好的解释在这里发生了什么:

当你创建一个信号量时,你可以指定你的可用资源的数量。这个值是信号量的初始计数变量。你每一次等待信号量发送信号时,这个 dispatch_semaphore_wait 方法使计数变量递减1。如果产生的值是负的,则函数告诉内核来阻止你的线程。在另一端,这个 dispatch_semaphore_signal 函数递增count变量用1表示资源已被释放。如果有任务阻塞和等待资源,其中一个随即被放行并进行它的工作。

其效果类似于 maxConcurrentOperationCount 在 NSOperationQueue 。如果你使用原 GCD队 列而不是 NSOperationQueue,你可以使用信号庄主来限制同时执行的 block 数量。

一个值得注意的就是,每次你调用 enqueueWork ,如果你打开信号量的限制,就会启动一个新线程。如果你有一个低限并且大量工作的队列,您可以创建数百个线程。一如既往,先配置文件,然后更改代码。

等待许多并发任务来完成

如果你有多 block 工作来执行,并且在他们集体完成时你需要发一个通知,你可以使用 group 。dispatch_group_async 允许你在队列中添加工作(在 block 里面的工作应该是同步的),并且记录添加了多少了项目。注意,在同一个 dispatch group 中可以将工作添加到不同的队列中,并且可以跟踪它们。当所有跟踪的工作完成,这个 block 开始运行 dispatch_group_notify,就像是一个完整的 block 。

dispatch_group_t group = dispatch_group_create()
for item in someArray {
	dispatch_group_async(group, backgroundQueue, {
		performExpensiveWork(item: item)
	})
}
dispatch_group_notify(group, dispatch_get_main_queue(), {
	// all the work is complete
}

拥有一个完整的block,对于扁平化一个功能来说是一个很好的案例。 dispatch group 认为,当它返回时,这个 block 应该完成了,所以你需要这个 block 等待直到其他工作已经完成。

有更多的手动方式来使用 dispatch groups ,特别是如果你耗性能的工作已经是异步的:

// must be on a background thread
dispatch_group_t group = dispatch_group_create()
for item in someArray {
	dispatch_group_enter(group)
	performExpensiveAsyncWork(item: item, completionBlock: {
		dispatch_group_leave(group)
	})
}

dispatch_group_wait(group, DISPATCH_TIME_FOREVER)

// all the work is complete

这段代码是比较复杂的,但通过一行一行的阅读可以帮助理解它。就像信号量,groups 也还保持线程安全,是一个你可以操作的内部计数器。您可以使用此计数器来确保在执行完成 block 之前,多个长的运行任务都已完成。使用 “enter” 递增计数器,并用 “leave” 递减计数器。 dispatch_group_async 为你处理所有的这些细节,所以我愿意尽可能的使用它。

在这段代码的最后一点是 wait 方法:它会阻塞线程,并等待计数器为0后,继续执行。注意,即使你使用了enter/leaveAPI,你也可以在在队列中添加一个 dispatch_group_notify block.反过来也是对的:当你使用 dispatch_group_async API时你也可以使用 dispatch_group_wait 。

dispatch_group_wait,就像dispatch_semaphore_wait一样,可以设置超时。再一次声明,DISPATCH_TIME_FOREVER 已非常足够使用, 我从未觉得需要使用其他的来设置超时。当然就像 dispatch_semaphore_wait 一样,永远不要在主线程使用dispatch_group_wait 。

两者之间最大的区别是,使用 notify 可以完全从主线程调用,而使用 wait,必须发生在后台队列(至少 wait 的部分,因为它会完全阻塞当前队列)。

隔离队列

Swift 语言的 Dictionary (和 Array )类型都是值类型。 当他们被改变时, 他们的引用会完全被新的结构给替代。当然,因为更新实例变量的 Swift 对象不是原子性的,它们不是线程安全的。双线程可以在同一时间更新一个字典(例如,增加一个值),并且两个尝试写在同一块内存,这可能导致内存损坏。我们可以使用隔离队列来实现线程安全。 让我们创建一个 [identity map] (http://martinfowler.com/eaaCatalog/identityMap.html)。 identity map 是一个字典,将项目从其ID 属性映射到模型对象。

class IdentityMap<T: Identifiable> {
	var dictionary = Dictionary<String, T>()

	func object(forID ID: String) -> T? {
		return dictionary[ID] as T?
	}

	func addObject(object: T) {
		dictionary[object.ID] = object
	}
}

这个对象基本上是一个字典的包装器。如果我们的方法 addObject 同一时间被多个线程所调用,它可能会损害内存,因为这些线程对对同一个引用进行处理。这被称之为 readers-writers problem。总之,我们可以同时有多个读者阅读,但是只有一个线程可以在任何给定的时间写。 幸运的是,GCD 给了我们很好的工具去处理这样的情况。我们可以使用以下四种 API :

  • dispatch_sync
  • dispatch_async
  • dispatch_barrier_sync
  • dispatch_barrier_async

我们理想的情况是,读同步,同时,而写可以异步,当引用该对象时必须是唯一的。 GCD 的 barrier API集可以做一些特别的事情:他们执行 block 之前必须等到队列完全空了。使用 barrier API去进行字典写入的操作将会被限制,这样确保我们永远不会有任何写入发生在同一时间,无论是读取或是写入。

class IdentityMap<T: Identifiable> {
	var dictionary = Dictionary<String, T>()
	let accessQueue = dispatch_queue_create("com.khanlou.isolation.queue", DISPATCH_QUEUE_CONCURRENT)

	func object(withID ID: String) -> T? {
		var result: T? = nil
		dispatch_sync(accessQueue, {
			result = dictionary[ID] as T?
		})
		return result
	}

	func addObject(object: T) {
		dispatch_barrier_async(accessQueue, {
			dictionary[object.ID] = object
		})
	}
}

dispatch_sync 将 block 添加到我们的隔离队列,然后等待它在返回之前执行。这样,我们就会有我们的同步阅读的结果。(如果我们没有做到同步,我们的 getter 方法可能需要一个完成的 block 。)因为 accessQueue 是并发的,这些同步读取就能同时发生。 dispatch_barrier_async 将 block 添加到隔离队列。这个 async 部分意味着它将实际执行的 block 之前返回(执行写入操作)。这对我们的表现有好处,但也有一个缺点是,在 “write” 操作后立即执行 “read” 操作可能会导致获取改变之前的旧数据。 这个 dispatch_barrier_async 的 barrier 部分,意味着它将等待直到当前运行队列中的每个 block 执行完毕后才执行。其他 block 将在它后面排队,当barrier调度完成时执行。

总结

Grand Central Dispatch 是一个有很多底层语言的框架。使用它们,这个是我能建立的比较高级的技术。如果有其他一些你使用的GCD的高级用法而我没有罗列在这里,我喜欢听到它们并将它们添加到列表中。






原文发布时间为:2016年06月12日


本文来自合作伙伴掘金,了解相关信息可以关注掘金网站。

时间: 2024-09-10 05:52:55

给 iOS 开发者的 GCD 用户手册的相关文章

iOS开发者证书的那些事

苹果官方文档 Certificates 证书 我们从开发者中心了解到,开发者证书分为两种类型:Development Certificate(开发证书)和Production Certificate(发布证书).两种证书都有对应的附属证书,包括推送证书.Apple Pay证书.Pass Type ID证书等等一系列附属证书. 需要注意的是,当我们发布应用到 AppStore 时,发布的电脑必须具备两个条件: 安装了创建这个发布证书的电脑导出的p12文件 从开发者账号下载了发布证书 证书的作用 当

适用于iOS开发者的Android开发技巧

  我曾经从事过五年的iOS应用开发工作,那段时间我一直在尽量避免同Android打交道--不过现在情况不同了.不管大家是否相信,Android开发其实乐趣满满.而且与iOS开发相比也不像大家想象的那样差异巨大. 我在Android平台上开发出这款"七分钟锻炼"应用,并借此学到了很多宝贵的知识.我希望这篇文章分享的一些小技巧也能帮助大家解决实际问题.请注意,我接下来进行比较的内容并不一定完全匹配,而且本文的重点也不在于完整地叙述Android开发;当然,我一定会提到自己在开发这款简单应

Flash开发如何使用iOS开发者授权以及如何申请证书

  在缴费成功后会收到一份来自Apple Developer Suport的确认邮件,如下图. 开发如何使用iOS开发者授权以及如何申请证书-微信第三方授权开发者"> 在邮件里有一个Login按钮,点击后可以进入Apple Developer会员中心.点击图中的iOS Provisioning Portal,进入开发者授权设置系统. 在设置界面中,可以选择点击页面下方的助手来运行向导. 或者直接从左边的导航来设置,我在这篇文章会主要介绍如何通过导航设置开发者授权以及证书.不过在此之前,我要

Flash如何成为一个合法的iOS开发者

  iOS开发者的申请流程 如果你是一个开发团队,在你打算掏腰包购买iOS开发者授权之前,最好先问一下你的同事,是否已经有人获得了开发许可,因为一个开发许可一年内最多可以授权给111个设备来开发测试.如果你没有授权许可可以借用,或者你打算最终在iTunes Store中以自己的名义发布你的应用,那么就准备一张国际信用卡,准备支付99美元来申请一个iOS开发者授权. 点击下面链接进入Apple Developer Center http://developer.apple.com/ 点击页面左边中

10大iOS开发者最喜爱的类库

该10大iOS开发者最喜爱的库由"iOS辅导团队"成员Marcelo Fabri组织投票选举而得,参与者包括开发者团队,iOS辅导团队以及行业嘉宾.每个团队都要根据以下规则选出五个最好的库:1)不能投自己写的库:2)排除大的架构,比如游戏类架构:3)排除不在设备上运行的库,例如CocoaPods或者Rack::CoreData也排除在外. 最终评选出了如下10大iOS开发者最喜爱的库: 1)SSToolkit Sam Soffes简直就是一个Objective-C天才,SSToolki

iOS 开发者一定要知道的 14 个知识点

本文讲的是iOS 开发者一定要知道的 14 个知识点, 作为一个 iOS 开发者(现在对 Swift 中毒颇深 ).我从零开始创建应用.维护应用,并且在很多团队待过.在我的职业生涯中,一句话一直响彻耳边:"如果你不能解释一件事情,那你根本就不理解它." 所以为了充分的理解我每天的日常,我创建了一个清单,在我看来,它适合任何 iOS 开发者.我会试着清晰的解释每一个观点.[请随时纠正我,提出你的意见,或者干脆也来一发你觉得应该在列表上的"必须知道"的知识] Topic

Bob,我要怎样才能成为一名 iOS 开发者

本文讲的是Bob,我要怎样才能成为一名 iOS 开发者, iOS 开发虽不易,但别怕尽管上就是了. 然而这并不是我的桌面 自我驱动 我经常收到类似的邮件跟私信, "Bob,我怎样才能成为一个超酷的开发者?" "Bob,我想转行了.我好喜欢你的文章跟视频.我要怎样才能成为一个 iOS 开发者呢?" "Bob,我不知道应该如何开始学.而且我之前也从来没写过代码,你能帮帮我吗?" 好啦我知道啦.但我会实话实说.我尽量去回答这些一般问题.我叫这种问题叫做

iOS开发者必备:App Store营销广告指南

很重要:需要征得苹果同意 所有印制和视频格式的材料,无论是营销还是广告,都必须在发表或发布之前提交给苹果并通过苹果的审核.请按照以下指导通过电子邮件提交你的构想.情节串联图板或者最终的创意.回复至少需要5个工作日.苹果可以检查尚未完成的材料:然而,在发布之前所有材料都必须经过苹果的审核. 所有包含苹果产品的定制影像或视频材料在发布或发表以前都必须提交给苹果,并通过苹果的审核. 营销审核:提交给苹果进行审核的营销材料应该发送至appstoremarketing@apple.com.营销包括了任何不

iOS开发者账号总结

之前写过一篇<iOS开发者账号总结>的文章,主要总结了不同开发者账号之间的区别,以及一些账号管理和选择上的建议. 但是,之前的文章因为没有开启Markdown,而且当时写得有点潦草,现在看起来感觉太乱了.这两天打算把这篇文章重写一下,内容也会进行更新和添加,以保证文章质量.  简述 iOS开发在国内已经发展好几年了,在进行iOS开发之前必不可少的一件事,就是申请开发者账号. 申请开发者账号后,就可以进入开发者中心下载Xcode编译器,并且配置开发者证书进行真机测试来调试项目.在程序开发完成后,