Wrappres' Studio.

iOS 知识补齐-多线程

字数统计: 2.1k阅读时长: 8 min
2019/04/15 Share

概念解释

线程安全

线程安全是编程中的术语,指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。

线程同步

线程同步即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态,实现线程同步的方法有很多,临界区对象就是其中一种。

pthread

POSIX 线程(英语:POSIX Threads,常被缩写 为 Pthreads)是 POSIX 的线程标准,定义了创建和操纵线程的一套 API。
实现 POSIX 线程标准的库常被称作 Pthreads,一般用于 Unix-like POSIX 系统,如 Linux、Solaris。但是 Microsoft Windows 上的实现也存在,例如直接使用 Windows API 实现的第三方库 pthreads-w32;而利用 Windows 的 SFU/SUA 子系统,则可以使用微软提供的一部分原生 POSIX API。

NSThread

创建 启动线程

1
2
3
4
5
6
7
8
9
// 创建线程
let thread = Thread(target: self, selector: #selector(run), object: nil)
// 启动线程
thread.start()

// 线程调用的方法
@objc func run() {
print("\(Thread.current)")
}
1
2
3
4
5
6
7
8
9
10
11
// 创建并自动启动线程
Thread.detachNewThreadSelector(#selector(run), toTarget: self, with: nil)
// 或
Thread.detachNewThread {
print("\(Thread.current)")
}

// 线程调用的方法
@objc func run() {
print("\(Thread.current)")
}
1
2
3
4
5
6
7
 // 隐式创建并启动线程
self.performSelector(inBackground: #selector(run), with: nil)

// 线程调用的方法
@objc func run() {
print("\(Thread.current)")
}

通信

在主线程执行操作

1
2
3
open func performSelector(onMainThread aSelector: Selector, with arg: Any?, waitUntilDone wait: Bool, modes array: [String]?)

open func performSelector(onMainThread aSelector: Selector, with arg: Any?, waitUntilDone wait: Bool)

在指定线程执行操作

1
2
3
4
5
@available(iOS 2.0, *)
open func perform(_ aSelector: Selector, on thr: Thread, with arg: Any?, waitUntilDone wait: Bool, modes array: [String]?)

@available(iOS 2.0, *)
open func perform(_ aSelector: Selector, on thr: Thread, with arg: Any?, waitUntilDone wait: Bool)

在当前线程执行操作

1
2
3
4
5
func perform(_ aSelector: Selector!) -> Unmanaged<AnyObject>!

func perform(_ aSelector: Selector!, with object: Any!) -> Unmanaged<AnyObject>!

func perform(_ aSelector: Selector!, with object1: Any!, with object2: Any!) -> Unmanaged<AnyObject>!

线程安全与线程同步

线程安全当多个线程同时执行同一段代码,若每次运行结果与单线程运行的结果一样,则线程安全,否则为线程不安全
例如开始两个线程模拟两地的售票处:

1
2
3
4
5
6
let window1 = Thread(target: self, selector:#selector(saleTicketNotSafe), object: nil);
window1.name = "北京售票处"
let window2 = Thread(target: self, selector: #selector(saleTicketNotSafe), object: nil);
window2.name = "上海售票处"
window1.start()
window2.start()

普通调用时会发生同一张票被卖了两次的情况

1
2
3
4
5
6
7
8
9
10
11
12
func saleTicketNotSafe() {
while true {
if ticketSurplusCount > 0 {
ticketSurplusCount = ticketSurplusCount - 1
print("剩余\(ticketSurplusCount)张票,在\(Thread.current.name ?? "-")窗口")
Thread.sleep(forTimeInterval: 0.2)
} else {
print("票卖完了!!!!!!!!!!!!!!!")
break
}
}
}

正确的做法应该是利用 NSLock 保证同一段代码同时只能被一个线程调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func saleTicketNotSafe() {
while true {
lock.lock()
if ticketSurplusCount > 0 {
ticketSurplusCount = ticketSurplusCount - 1
print("剩余\(ticketSurplusCount)张票,在\(Thread.current.name ?? "-")窗口")
Thread.sleep(forTimeInterval: 0.2)
} else {
print("票卖完了!!!!!!!!!!!!!!!")
break
}
lock.unlock()
}
}

GCD

Grand Central Dispatch(GCD) 是 Apple 开发的一个多核编程的较新的解决方法。它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。它是一个在线程池模式的基础上执行的并发任务。在 Mac OS X 10.6 雪豹中首次推出,也可在 iOS 4 及以上版本使用。

任务和队列

任务是指执行某个操作,即代码段。
同时执行任务的方式有两种,分别是同步执行和异步执行。同步执行(sync):只能在当前线程执行任务,不具备开启线程的能力。异步执行(async):具备开启新线程的能力,可以在不同线程中执行任务。
队列是指执行任务的等待队列,即用来存放任务的队列。采用 FIFO 的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。
串行队列:只开启一个线程,串行执行。
并行队列:可开启多个线程,同时执行多个任务。

使用

队列创建

1
2
3
4
5
6
7
8
9
10
11
12
// 串行队列
let serialQueue = DispatchQueue(label: "serialQueue")
// 并行队列
let concurrentQueue = DispatchQueue.init(label: "concurrentQueue", attributes: .concurrent)
// 主队列
let mainQueue = DispatchQueue.main
// 全局队列
let globalQueue = DispatchQueue.global()
serialQueue.async {
}
serialQueue.sync {
}

区别 并行队列 串行队列 主队列
同步 sync 没有开启新线程,串行执行任务 没有开启新线程,串行执行任务 没有开启新线程,串行执行任务
异步 async 有开启新线程,并发执行任务 有开启新线程(1 条),串行执行任务 没有开启新线程,串行执行任务

通信

获取主队列,添加任务

其他方法

栅栏方法:dispatch_barrier_async

当需要前后执行两组异步操作,即第一个异步操作执行完毕,第二个才开始执行时,可以利用 barrier 来类似栅栏的作用,保证两个异步操作之间的顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let queue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent)
queue.async {
sleep(1)
print("1---\(Thread.current)")
}
queue.async(group: nil, qos: .default, flags: .barrier) {
sleep(2)
print("barrier---\(Thread.current)")
}
queue.async {
sleep(1)
print("2---\(Thread.current)")
}
queue.async {
sleep(1)
print("3---\(Thread.current)")
}

延时执行方法:dispatch_after

1
2
3
4
let queue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent)
queue.asyncAfter(deadline: .now() + 10) {
print("延迟执行")
}

一次性代码(只执行一次):dispatch_once

1
2
3
4
5
6
- (void)once {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行1次的代码(这里面默认是线程安全的)
});
}

快速迭代方法:dispatch_apply

可以利用异步队列同时遍历,而普通循环是逐一遍历。异步遍历的顺序具有随机性

1
2
3
4
5
6
7
8
9
- (void)apply {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

NSLog(@"apply---begin");
dispatch_apply(6, queue, ^(size_t index) {
NSLog(@"%zd---%@",index, [NSThread currentThread]);
});
NSLog(@"apply---end");
}

队列组:dispatch_group

利用队列组实现分别异步执行两个耗时任务,当这两个耗时任务都执行完毕时,再回到主线程执行任务。

notify

依赖任务,通知 group,之前的两个任务已经执行完毕。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let group = DispatchGroup()
let queue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent)
queue.async(group: group) {
for i in 0...10 {
print("测试\(i)")
}
}
queue.async(group: group) {
for i in 11...20 {
print("测试\(i)")
}
}
group.notify(queue: queue) {
print("结束了")
}

wait

阻塞当前线程,等待指定 gruop 中的任务执行完成后,才继续执行。

1
2
3
4
5
6
7
8
9
let group = DispatchGroup()
let queue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent)
queue.async(group: group) {
for i in 0...10 {
print("测试\(i)")
}
}
group.wait()
print("结束")

enter & leave

enter:标志着一个任务追加到 gruop 中
leave:标志着一个任务完成了,从 gruop 中删除
ps:在调用时传入 gruop 与利用 enter 和 leave 来控制效果一致

1
2
3
4
5
6
7
8
9
10
11
let group = DispatchGroup()
let queue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent)
group.enter()
queue.async {
for i in 0...10 {
print("测试\(i)")
}
group.leave()
}
group.wait()
print("结束")

信号量 dispatch_semaphore

信号量是持有计数的信号。当计数为 0 时,会阻塞所在线程,当计数大于 0 时,可以正常执行。

线程同步:将异步执行任务转换成同步执行任务

1
2
3
4
5
6
7
8
9
10
let queue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent)
let semaphore = DispatchSemaphore(value: 0)
queue.async {
for i in 1...10 {
print("测试\(i)")
}
semaphore.signal()
}
semaphore.wait()
print("结束")

利用 semaphore 为线程加锁

主队列与主线程的关系

主队列上的任务一定在主线程中执行。
主线程可以执行主队列以外的其他队列的任务,这是为了避免频繁切换线程对性能的影响。

使用 GCD 方式从子线程回到主现场的方法代码

答:如何用 GCD 同步若干个异步调用?(如根据若干个 url 异步加载多张图片,然后在都下载完成后合成一张整图)

1
2
3
4
5
6
7
8
9
10
11
12
// 使用Dispatch Group追加block到Global Group Queue,这些block如果全部执行完毕,就会执行Main Dispatch Queue中的结束处理的block。
// 创建队列组
dispatch_group_t group = dispatch_group_create();
// 获取全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_async(group, queue, ^{ /*加载图片1 */ });
dispatch_group_async(group, queue, ^{ /*加载图片2 */ });
dispatch_group_async(group, queue, ^{ /*加载图片3 */ });
// 当并发队列组中的任务执行完毕后才会执行这里的代码
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 合并图片
});

CATALOG
  1. 1. 概念解释
    1. 1.1. 线程安全
    2. 1.2. 线程同步
  2. 2. pthread
  3. 3. NSThread
    1. 3.1. 创建 启动线程
    2. 3.2. 通信
    3. 3.3. 线程安全与线程同步
  4. 4. GCD
    1. 4.1. 任务和队列
    2. 4.2. 使用
    3. 4.3. 通信
    4. 4.4. 其他方法
      1. 4.4.1. 栅栏方法:dispatch_barrier_async
      2. 4.4.2. 延时执行方法:dispatch_after
      3. 4.4.3. 一次性代码(只执行一次):dispatch_once
      4. 4.4.4. 快速迭代方法:dispatch_apply
      5. 4.4.5. 队列组:dispatch_group
        1. 4.4.5.1. notify
        2. 4.4.5.2. wait
        3. 4.4.5.3. enter & leave
        4. 4.4.5.4. 信号量 dispatch_semaphore
    5. 4.5. 主队列与主线程的关系
    6. 4.6. 使用 GCD 方式从子线程回到主现场的方法代码