概述

早上起床,你先打开洗衣机,然后用热水把泡面泡上,接着打开电脑开启一天的码农生活。其中“洗衣服”、“泡泡面”和“码代码”3个任务(线程)同时进行,这就是多线程。网上有许多关于多线程的经典解释,此处就不再菜鸟弄斧了,以免贻笑大方。当今流行于世的系统基本都会提供多线程这项基本功能,iOS也不例外。其中Swift提供了3种可选方案:NSThread,GCD和NSOperation,接下来我们将对3种方案进行运用和分析。

NSThread

NSThread是Objective-C给Swift留下的众多遗产之一,并且Swift给其定义了一个简体名称Thread。

 class ViewController: UIViewController {

     @IBOutlet weak var testLabel: UILabel!
var testThread:Thread? = nil
var count = override func viewDidLoad() {
super.viewDidLoad()
testLabel.text = "Charpter9" testThread = Thread.init(target: self, selector: #selector(threadFunc), object: "菜鸟先飞")
testThread!.start()
} override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
} @objc func threadFunc(p: String) { while(true) {
sleep()
count +=
print("\(p): \(count)")
}
}
}

创建线程使用Thread.init(target:Any, selector: Selector, object: Any?)。

其中target表示selector函数所属的类,selector表示线程的函数名,object表示函数参数。

如上代码所示,当我们调用testFunc!.start()时,系统就会创建线程并执行threadFunc函数。

(注意gif右下角打印变化)

提示:针对target参数,网上有个千篇一律的说法是:“selector消息发送的对象”。针对这个解释,我只能说“非常忠于原文”,基本就是直译apple的文档。target其实就是selector函数所在的类实例,为什么要说的这么复杂呢?这是因为selector本身并非函数入口地址,而是1个字符串。线程启动时,selector字符串被交给了target,target调用和selector字符串同名的方法,进而启动线程,所以才有之前的那个晦涩的说法。

NSThread是1个轻量级线程调用(相对GCD和NSOperation而言),不带任何“赠品”。如果你要做数据同步,那你得自己加同步锁或信号量(NSLock和NSCondition);线程不用了要记得cancel掉,不然会内存泄漏。总之你得留个心眼儿好好管着NSThread这个熊孩子。

“什么?!内存泄漏?!NSThread太可怕了,又不好管,有没有安全听话一点的东东啊?”

GCD(Grand Central Dispatch)

这是iOS开发中出镜率最高的一种线程机制。如下图所示,

GCD涉及1个先入先出(FIFO)队列,任务依次入队,再依次出队交给线程执行。整个过程中,除了创建队列、新任务和加入任务到队列的动作外,其他均由系统自己处理。FIFO队列和线程的生老病死养老送终都由系统负责解决。真是名副其实的“大中央调度”。接下来我们看看GCD有哪些特性以及如何使用。

 import UIKit

 class ViewController: UIViewController {

     var queue: DispatchQueue?

     override func viewDidLoad() {
super.viewDidLoad()
/* create serial queue */
queue = DispatchQueue.init(label: "com.ansersion.charpter9.queue.serial") print("start test sync...")
for i in ... {
print("sync start \(i)")
queue?.sync {
sleep()
let df = DateFormatter()
df.dateFormat = "HH:mm:ss"
print("sync executing:\(Thread.current),[\(df.string(from: Date()))]")
}
print("sync end \(i)")
}
print("start test async...")
for i in ... {
print("async start \(i)")
queue?.async {
sleep()
let df = DateFormatter()
df.dateFormat = "HH:mm:ss"
print("async executing:\(Thread.current),[\(df.string(from: Date()))]")
}
print("async end \(i)")
} } override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
} }

首先我们先创建1个队列,label为队列的唯一标识:

queue = DispatchQueue.init(label: "com.ansersion.charpter9.queue.serial")

DispatchQueue有2个将任务加入队列的函数:sync(同步)和async(异步)。这两者有什么区别呢?

在以上代码中,先是循环入队3个任务,并同步执行,然后再循环入队3个任务,并异步执行,每个任务先sleep一秒钟,然后打印线程和时间信息。

我们不妨看看以上代码的运行结果。

我们可以发现“sync”是阻塞的,相当于把任务中的代码原地执行一边,和直接调用1个函数没多大区别。但是“async”就不一样了,它将任务入队就立刻返回,无需等待任务执行完毕。再看看打印信息,我们可以发现“sync”执行时,直接使用的是主程序所在的main线程,而“async”则重开了1个新线程。

GCD有2个重要特性,第一个就是“同步”和“异步”。

同步:任务入队后在当前线程下阻塞执行,不开启新线程。

异步:任务入队后不在当前线程下执行,而是开启新线程,将任务出队到新线程中执行。

如果我们再仔细思考思考“async”执行时打印的信息,我们会发现3个任务是1个接着1个执行的(根据打印时间)。我们不禁会想,系统咋就这么抠门,异步执行只开了1个线程。假如我现在有急事,想让它们同时执行要怎么做呢?

我们只需要将

queue = DispatchQueue.init(label: "com.ansersion.charpter9.queue.serial") //创建串行队列

改为

queue = DispatchQueue.init(label: "com.ansersion.charpter9.queue.concurrent", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil) // 创建并发队列

就可以了。

假如我想让3个任务一起执行要怎么做呢?

我们再来看看运行结果

根据打印时间,我可以发现3个同步执行的任务和之前没有多大区别,但是3个异步执行的任务是同时执行的,而且系统开了3个线程。

GCD的第二个重要特性是:串行队列和并发队列。

串行队列:只绑定了1个线程,前一个任务执行完毕后,下一个任务才能出队给绑定线程执行。

并发队列:根据需要可以绑定多个线程,不管前一个任务是否执行完毕,只要当前有空闲线程,就将任务出队给空闲线程执行。

关于DispatchQueue.init(label: "com.ansersion.charpter9.queue.concurrent", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit,target: nil)

这是一个比较新的init接口,往后会继续传承还是遗弃尘封都还无定数,截至博主发文日期,apple官网上关于这个接口的描述还是个空页面。但是我们要记住attributes这个参数“.concurrent”:表示并发队列。

那么并发队列最多能异步并发多少个线程呢?

stackoverflow上有人做了个实验,发现最多可以实现66个线程并发,这位同学真的很6。然而apple并未给出官方说法,66权且作为1个参考。

现在我们总结一下:

其实我们大可不必自己创建队列,系统本身就为app创建了2个队列:1个是主线程串行队列,1个是全局并发队列。

我们来讲讲主线程队列,app的所有UI更新都是在主线程中进行的。以上我们的实验之所以只靠打印来查看效果是因为其他线程无法更新UI,否则就崩溃伺候。

如果我想在主线程以外的其他线程里更新UI要怎么办呢?

这时候就要靠主线程串行队列了,因为它绑定的是主线程,所以更新UI的任务交给它执行就不会出问题啦。

我们在之前的代码中,再后缀以下内容

queue?.async {

sleep(5)

DispatchQueue.main.async {

self.navigationItem.title = "update UI"

}

}

程序启动5秒钟之后,你会发现导航栏的标题改变了。

注意:不能在主线程中使用主线程队列调用sync,否则直接死锁,因为你已经在执行主线程了,主线程并不空闲,你不能再同步调用它了(不容易理解,需细细体会)。

另外,GCD还可以延时执行任务,分组执行(挨组执行,每组可以有多个任务),这些可以使用DispatchWorkItem作为任务单元或者为async添加参数实现。

此处仅以抛砖引玉,不作细说。

NSOperation

NSOperation本身是一个抽象类,我们通过使用其现成的子类(BlockOperation)或继承它自定义子类来实现1个操作(或任务),然后我们就可以直接运行这个操作,或者将其放入操作队列里执行。

也许你已经发现,其操作和操作队列的概念和GCD的任务和任务队列的概念很像。NSOperation官文文档提到:

An operation queue executes its operations either directly, by running them on secondary threads, or indirectly using the libdispatch library (also known as Grand Central Dispatch)

简言之,NSOperation是对GCD的封装。

既然是一脉相承,那么NSOperation是个长江后浪推前浪的勇进后生呢,还是说只是个既生瑜何生亮的花瓶?

实现真正的同步并发

如前文所述,在GCD中,无论是串行队列还是并发队列,当我们调用同步运行(sync)时,都只相当于原地调用线程的代码。并不存在什么并发之说。现在我们来看看NSOperation是怎么实现同步并发的。

 import UIKit

 class ViewController: UIViewController {

     override func viewDidLoad() {
super.viewDidLoad()
let blockOperation = BlockOperation(); for i in ... {
print("block \(i) added")
blockOperation.addExecutionBlock {
sleep()
let df = DateFormatter()
df.dateFormat = "HH:mm:ss"
print("BlockOperation addExecutionBlock executing:\(Thread.current),[\(df.string(from: Date()))]")
}
} print("start BlockOperation")
blockOperation.start()
print("end BlockOperation")
} override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}

此处我们使用NSOperation的子类BlockOperation,通过调用其接口addExecutionBlock添加了3个操作,每个操作先sleep一秒钟,然后打印线程和时间信息。通过打印,我们看到3个任务是使用了3个线程同时执行的,所以是并发的。因为print("end BlockOperation")是在3个线程打印结束后才执行的,所以是同步的。

实现操作依赖

假使现在我们要开发某款APP,该APP有5个模块,每个模块都有1个线程,现在我们将各个模块分配给5个工程师去开发。

正当我们在为自己优秀的项目管理和分工能力而沾沾自喜的时候。开发A模块的工程师告诉你:“我要等到B模块的运行结果后才能开始启动,否则#¥**&@%^...”。你感觉没什么难度,于是爽快的修改了一下主程序再加了一些线程通信的内容,把A放在B之前运行……这虽然只是你主程序修改的一小步,但却是你悲剧命运的一大步。时过境迁,沧海桑田,你的APP从5个模块变成了50个模块,当这时再有某个工程师找到你,敢问你爽直的豪情是否依旧?

终于有一天,你受不了大喊一声:“你们能不能自己折腾,别来找我?!”

"操作依赖,你值得拥有”

 class ViewController: UIViewController {

     override func viewDidLoad() {
super.viewDidLoad()
let queue = OperationQueue.init()
let df = DateFormatter()
df.dateFormat = "HH:mm:ss" let blockOperationSlow = BlockOperation.init(block: {
let df = DateFormatter()
df.dateFormat = "HH:mm:ss"
print("slow start:\(Thread.current),[\(df.string(from: Date()))]")
sleep()
print("slow end:\(Thread.current),[\(df.string(from: Date()))]")
})
let blockOperationQuick = BlockOperation.init(block: {
let df = DateFormatter()
df.dateFormat = "HH:mm:ss"
print("quick start:\(Thread.current),[\(df.string(from: Date()))]")
sleep()
print("quick end:\(Thread.current),[\(df.string(from: Date()))]")
}) blockOperationQuick.addDependency(blockOperationSlow) print("add queue start")
queue.addOperation(blockOperationQuick)
queue.addOperation(blockOperationSlow)
print("add queue end") } override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}

首先我们创建了一个操作队列(OperationQueue),操作队列都是并发异步执行的,但是可以设置最大并发数(maxConcurrentOperationCount),如果设置为1就等于是串行异步执行了。(注:系统提供了一个默认串行异步的操作队列:主操作队列OperationQueue.main,修改其maxConcurrentOperationCount没有任何作用。由于它使用的是主线程,也就是说可以通过它修改UI。)

我们创建了1个slow操作和1个quick操作,并且让quick操作依赖slow操作。通过打印我们发现,2个操作是运行在不同的线程中的,即便quick操作只需要执行1秒,而slow操作需要执行3秒,但是quick操作还是等到slow操作执行完之后才启动的。

那么假使两个操作相互依赖会怎么样呢?

回答是:都无法执行。

唉,码代码永远是走在一条追求完美而不得的不归路上。

支持继承

这并非什么新功能,但却能帮助我们实现良好的代码结构。闲话不多说,代码见分明。

 import UIKit

 class MyOperation: Operation {
override func main() {
print("do MyOperation")
}
} class ViewController: UIViewController { override func viewDidLoad() {
super.viewDidLoad()
let blockOperation = MyOperation();
blockOperation.start()
} override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}

线程安全

线程安全是一个伴随多线程一生的话题。其核心问题就是如何保证对共享资源的串行访问,多线程的一大利器就是并发,而共享资源却是排斥并发的。并发利器和共享资源的矛盾遍布中外贯穿古今。惯用的做法就是对共享资源加锁,有锁的线程访问共享资源,其他线程继续等待(想象一下排队蹲茅厕的场景)。

Swift提供了两种锁,NSLock和NSRecursiveLock,前者是不可递归锁,锁了一次后必须解开后才能再次上锁;后者是可递归锁,比NSLock多了一种功能:在同一个线程中,允许连续n次上锁(相应的也得有n次解锁)。

线程安全是个高深而细分的主题,此处点到为止。

源码下载(NSThread):https://pan.baidu.com/s/1BfVxj9yzkLw22qIk8IAiBg

源码下载(GCD):https://pan.baidu.com/s/1oiBENGLszz6lb1dJRVnulQ

源码下载(NSOperation同步并发):https://pan.baidu.com/s/1TLLtuIP1fjluDwjaIWa8Rg

源码下载(NSOperation操作依赖):https://pan.baidu.com/s/1yAmh1ZLAb2j4zUL2lsU6-Q

上一节           回目录          下一节




九、使用多线程——NSThread,GCD和NSOperation的更多相关文章

  1. iOS多线程 NSThread/GCD/NSOperationQueue

    无论是GCD,NSOperationQueue或是NSThread, 都没有线程安全 在需要同步的时候需要使用NSLock或者它的子类进行加锁同步 "] UTF8String], DISPA ...

  2. 多线程 NSThread GCD

    ios多线程实现种类 NSThread NSOperationQueue NSObject GCD *************** 1.NSThread //线程 第一种 NSThread *thre ...

  3. 多线程:GCD

    多线程是程序开发中非常基础的一个概念,大家在开发过程中应该或多或少用过相关的东西.同时这恰恰又是一个比较棘手的概念,一切跟多线程挂钩的东西都会变得复杂.如果使用过程中对多线程不够熟悉,很可能会埋下一些 ...

  4. iOS中的多线程NSThread/GCD/NSOperation & NSOperationQueue

    iOS多线程有四套多线程方案: Pthreads NSThread GCD NSOperation & NSOperationQueue 接下来我来一个一个介绍他们 Pthreads 在类Un ...

  5. iOS中的几种锁的总结,三种开启多线程的方式(GCD、NSOperation、NSThread)

    学习内容 欢迎关注我的iOS学习总结--每天学一点iOS:https://github.com/practiceqian/one-day-one-iOS-summary OC中的几种锁 为什么要引入锁 ...

  6. iOS多线程——GCD与NSOperation总结

    很长时间以来,我个人(可能还有很多同学),对多线程编程都存在一些误解.一个很明显的表现是,很多人有这样的看法: 新开一个线程,能提高速度,避免阻塞主线程 毕竟多线程嘛,几个线程一起跑任务,速度快,还不 ...

  7. [iOS]多线程和GCD

    新博客wossoneri.com 进程和线程 进程 是指在系统中正在运行的一个应用程序. 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内. 比如同时打开QQ.Xcode,系统就会分别 ...

  8. iOS 开发多线程篇—GCD的常见用法

    iOS开发多线程篇—GCD的常见用法 一.延迟执行 1.介绍 iOS常见的延时执行有2种方式 (1)调用NSObject的方法 [self performSelector:@selector(run) ...

  9. iOS开发多线程篇—GCD介绍

    iOS开发多线程篇—GCD介绍 一.简单介绍 1.什么是GCD? 全称是Grand Central Dispatch,可译为“牛逼的中枢调度器” 纯C语言,提供了非常多强大的函数 2.GCD的优势 G ...

随机推荐

  1. 严选 | Elasticsearch史上最全最常用工具清单【转】

    1.题记 工欲善其事必先利其器,ELK Stack的学习和实战更是如此,特将工作中用到的“高效”工具分享给大家. 希望能借助“工具”提高开发.运维效率! 2.工具分类概览 2.1 基础类工具 1.He ...

  2. iScroll的使用

    CDN: <script src="//ossweb-img.qq.com/images/js/iscroll_library/iscroll-5.2.0.js">&l ...

  3. ShiftRows方法简介

    ShiftRows 是HSSFSheet工作薄的方法 ShiftRows(int startRow,int endRow,int n)参数介绍:startRow:开始行endRow:末尾行n:移动n行 ...

  4. JDK 自带的观察者模式源码分析以及和自定义实现的取舍

    前言 总的结论就是:不推荐使用JDK自带的观察者API,而是自定义实现,但是可以借鉴其好的思想. java.util.Observer 接口源码分析 该接口十分简单,是各个观察者需要实现的接口 pac ...

  5. Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.0:compile (default-compile) on project demo: Fatal error com piling: 无效的标记: -parameters

    背景:本项目使用JDK1.8 编译maven工程的时候出现如下错误: Failed to execute goal org.apache.maven.plugins:maven-compiler-pl ...

  6. 项目中 2个或者多个EF模型 表名称相同会导致生成的实体类 覆盖的解决方法

    场景:  2个数据库, 一个新,一个旧,  把旧的 数据库中的数据,导入到新的数据库中,  使用到了2个 EF实体模型, 新数据库 和 旧数据库中的表,有的名称是相同的 (但是结构是不同的) 旧的数据 ...

  7. 推荐一本写给IT项目经理的好书

    原文地址:http://www.cnblogs.com/cbook/archive/2011/01/19/1939060.html (防止原文作者删除.只能拷贝一份了) 推荐一本写给IT项目经理的好书 ...

  8. 四、Sql Server 基础培训《进度4-插入数据(实际操作)》

    知识点: 假设有订单表 CREATE TABLE Order ( ID int identity(1,1) not null primary key, --内码 BillNo varchar(100) ...

  9. hdu 2899

    mxy终于学会求函数极值了. 先写一道板子. #include <bits/stdc++.h> using namespace std; typedef double db; ; cons ...

  10. ruby 知识点随笔

    print .puts 和 p 方法的区别."" 与 ''  的区别. 处理控制台编码问题 >ruby -E utf-8 脚本文件名称 # 执行脚本 >irb -E u ...