iOS 多线程相关

概念

在开始多线程之前,我们先来了解几个比较容易混淆的概念。

线程与进程

一个进程,可以拥有一个或多个线程。

runloop与线程

https://www.jianshu.com/p/9a46e6762fca

并发与并行

并发指多个任务交替占用CPU,并行指多个CPU同时执行多个任务。好比在火车站买票,并发是指一个窗口有多人排队买票,并行是指多个窗口有多人排队买票。

同步和异步

同步指在执行一个函数时,如果这个函数没有执行完毕,那么下一个函数便不能执行。异步指在执行一个函数时,不必等到这个函数执行完毕,便可开始执行下一个函数。

四种方式

iOS目前有四种多线程方式:

  • Pthreads
  • NSThread
  • GCD
  • NSOperation & NSOperationQueue

Pthreads

这个方法不必多说,大家了解一下就好,按照百度百科里的解释:

POSIX线程(POSIX threads),简称Pthreads,是线程的POSIX标准。该标准定义了创建和操纵线程的一整套API。在类Unix操作系统(Unix、Linux、Mac OS X等)中,都使用Pthreads作为操作系统的线程。

简单的说,这是一套在很多操作系统上都适用的多线程API,所以移植性很强(然并卵)。虽然它在iOS系统中是适用的,但它是基于c语言的框架,用起来是相当的酸爽(需要程序员自己管理线程的生命周期),下面可以来体验一下:

- (void)threads {
    // 定义一个pthread_t类型的变量
    pthread_t thread;
    // 创建一个线程,并自动运行
    pthread_create(&thread, NULL, run, NULL);
    // 设置该线程的状态为detached,该线程结束后会自动释放所有资源
    pthread_detach(thread);
}
void *run(void *data) { // 新线程调用的方法,里面有需要执行的任务
    NSLog(@"%@", [NSThread currentThread]);
    return NULL;
}

打印结果:

2019-05-30 22:25:55.516777+0800 studyDemo[46180:3419195] {number = 3, name = (null)}

NSThread

这套方案是经过苹果封装后的,并且完全面向对象的。所以可以直接操控线程对象,非常直观和方便。但是它的生命周期还是需要手动管理,所以使用较少。比如[NSThread currentThread],它可以获取当前线程类,从而可以知道当前线程的各种属性,用于调试非常方便。下面来看代码:

- (void)threads_NSThread {
    // 创建
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadLog) object:nil];
    // 启动
    [thread start];

    // 创建并启动
    [NSThread detachNewThreadSelector:@selector(threadLog) toTarget:self withObject:nil];
}
- (void)threadLog {
    NSLog(@"%@", [NSThread currentThread]);
}

打印结果:

2019-05-30 22:39:37.926337+0800 studyDemo[46421:3427904] {number = 3, name = (null)}

GCD

同步

let queue = DispatchQueue(label: "com.ffib.blog")

queue.sync {
    for i in 0..<5 {
        print(i)
    }
}

for i in 10..<15 {
    print(i)
}

output: 
0
1
2
3
4
10
11
12
13
14

从结果可以看出队列同步操作时,当程序在进行队列任务时,主线程的操作并不会被执行,这是由于当程序在执行同步操作时,会阻塞线程,所以需要等待队列任务执行完毕,程序才可以继续执行。

异步

let queue = DispatchQueue(label: "com.ffib.blog")

queue.async {
    for i in 0..<5 {
        print(i)
    }
}

for i in 10..<15 {
    print(i)
}

output:
10
11
12
13
14
0
1
2
3
4

从结果可以看出队列异步操作时,当程序在执行队列任务时,不必等待队列任务开始执行,便可执行主线程的操作。与同步执行相比,异步队列并不会阻塞主线程,当主线程空闲时,便可执行别的任务。

QoS 优先级

在实际开发中,我们需要对任务分类,比如UI的显示和交互操作等,属于优先级比较高的,有些不着急操作的,比如缓存操作、用户习惯收集等,相对来说优先级比较低。
在GCD中,我们使用队列和优先级划分任务,以达到更好的用户体验,选择合适的优先级,可以更好的分配CPU的资源。
GCD内采用DispatchQoS结构体,如果没有指定QoS,会使用default。
以下等级由高到低。

public struct DispatchQoS : Equatable {

     public static let userInteractive: DispatchQoS //用户交互级别,需要在极快时间内完成的,例如UI的显示
     
     public static let userInitiated: DispatchQoS  //用户发起,需要在很快时间内完成的,例如用户的点击事件、以及用户的手势
     。
     public static let `default`: DispatchQoS  //系统默认的优先级,与主线程优先级一致
     
     public static let utility: DispatchQoS   //实用级别,不需要很快完成的任务
     
     public static let background: DispatchQoS  //用户无法感知,比较耗时的一些操作

     public static let unspecified: DispatchQoS
}

以下通过两个例子来具体看一下优先级的使用。
相同优先级

let queue1 = DispatchQueue(label: "com.ffib.blog.queue1", qos: .utility)
let queue2 = DispatchQueue(label: "com.ffib.blog.queue2", qos: .utility)

queue1.async {
    for i in 5..<10 {
        print(i)
    }
}

queue2.async {
    for i in 0..<5 {
        print(i)
    }
}
 output:
 0
 5
 1
 6
 2
 7
 3
 8
 4
 9

从结果可见,优先级相同时,两个队列是交替执行的。

不同优先级

let queue1 = DispatchQueue(label: "com.ffib.blog.queue1", qos: .default)
let queue2 = DispatchQueue(label: "com.ffib.blog.queue2", qos: .utility)

queue1.async {
    for i in 0..<5 {
        print(i)
    }
}

queue2.async {
    for i in 5..<10 {
        print(i)
    }
}

output:
0
1
2
3
4
5
6
7
8
9

从结果可见,CPU会把更多的资源优先分配给优先级高的队列,等到CPU空闲之后才会分配资源给优先级低的队列。

主队列默认使用拥有最高优先级,即userInteractive,所以慎用这一优先级,否则极有可能会影响用户体验。
一些不需要用户感知的操作,例如缓存等,使用utility即可

串行队列

在创建队列时,不指定队列类型时,默认为串行队列。

let queue = DispatchQueue(label: "com.ffib.blog.initiallyInactive.queue", qos: .utility)

queue.async {
    for i in 0..<5 {
        print(i)
    }
}

queue.async {
    for i in 5..<10 {
        print(i)
    }
}

queue.async {
    for i in 10..<15 {
        print(i)
    }
}
output: 
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14

从结果可见队列执行结果,是按任务添加的顺序,依次执行。

并行队列

let queue = DispatchQueue(label: "com.ffib.blog.concurrent.queue", qos: .utility, attributes: .concurrent)

queue.async {
    for i in 0..<5 {
        print(i)
    }
}

queue.async {
    for i in 5..<10 {
        print(i)
    }
}

queue.async {
    for i in 10..<15 {
        print(i)
    }
}
output:
5
0
10
1
2
3
11
4
6
12
7
13
8
14
9

从结果可见,所有任务是以并行的状态执行的。另外在设置attributes参数时,参数还有另一个枚举值initiallyInactive,表示的任务不会自动执行,需要程序员去手动触发(queue.activate())。如果不设置,默认是添加完任务后,自动执行。
添加initiallyInactive这一属性带来了,更多的灵活性,可以自由的决定执行的时机。

再来看看并行队列如何设置这一枚举值。

let queue = DispatchQueue.init(label: "Qos5", qos: .utility, attributes: [.concurrent, .initiallyInactive])

延迟执行

GCD提供了任务延时执行的方法,通过对已创建的队列,调用延时任务的函数即可。其中时间以DispatchTimeInterval设置,GCD内跟时间参数有关系的参数都是通过这一枚举来设置。

public enum DispatchTimeInterval : Equatable {

    case seconds(Int)     //秒

    case milliseconds(Int) //毫秒

    case microseconds(Int) //微妙

    case nanoseconds(Int)  //纳秒

    case never
}

在设置调用函数时,asyncAfter有两个及其相同的方法,不同的地方在于参数名有所不同,参照Stack Overflow的解释。

wallDeadline 和 deadline,当系统睡眠后,wallDeadline会继续,但是deadline会被挂起。例如:设置参数为60分钟,当系统睡眠50分钟,wallDeadline会在系统醒来之后10分钟执行,而deadline会在系统醒来之后60分钟执行。

let queue = DispatchQueue(label: "com.ffib.blog.after.queue")

let time = DispatchTimeInterval.seconds(5)

queue.asyncAfter(wallDeadline: .now() + time) {
    print("wall dead line done")
}

queue.asyncAfter(deadline: .now() + time) {
    print("dead line done")
}

DispatchQueue.main.asyncAfter(deadline: .now() + Double(NSEC_PER_SEC * 1) / Double(NSEC_PER_SEC)) {
    print("主线程 dead line done")
}

dispatch_group (组)

如果想等到所有的队列的任务执行完毕再进行某些操作时,可以使用DispatchGroup来完成。

let group = DispatchGroup()
let queue1 = DispatchQueue(label: "com.ffib.blog.queue1", qos: .utility)
let queue2 = DispatchQueue(label: "com.ffib.blog.queue2", qos: .utility)
queue1.async(group: group) {
    for i in 0..<5 {
        print(i)
    }
}
queue2.async(group: group) {
    for i in 6..<10 {
        print(i)
    }
}

//group内所有线程的任务执行完毕
group.notify(queue: DispatchQueue.main) {
    print("done")
}

output: 
5
0
6
1
7
2
8
3
9
4
done

如果想等待某一队列先执行完毕再执行其他队列可以使用wait

group.wait()
// 上方的output会变为:0~9 done,因为wait会同步等待先前提交的工作完成。(类似同步执行)

为防止队列执行任务时出现阻塞,导致线程锁死,可以设置超时时间。

// 同步等待先前提交的工作完成,如果在指定的超时时间之前工作未完成,则返回。
group.wait(timeout: <#T##DispatchTime#>) 
// 作用同上,DispatchTime为主板时间CPU时钟计时, DispatchWallTime为实际时间即系统时间
group.wait(wallTimeout: <#T##DispatchWallTime#>)

DispatchWorkItem

Swift3新增的api,可以通过此api设置队列执行的任务。先看看简单应用吧。通过DispatchWorkItem初始化闭包。

let workItem = DispatchWorkItem {
    for i in 0..<10 {
        print(i)
    }
}

调用一共分两种情况,第一种是通过调用perform(),自动响应闭包。

 DispatchQueue.global().async {
     workItem.perform()
 }

第二种是作为参数传给async方法。

 DispatchQueue.global().async(execute: workItem)

接下来我们来看看DispatchWorkItem的内部都有些什么方法和属性。

init(qos: DispatchQoS = default, flags: DispatchWorkItemFlags = default,
    block: @escaping () -> Void)

从初始化方法开始,DispatchWorkItem也可以设置优先级,另外还有个参数DispatchWorkItemFlags,来看看DispatchWorkItemFlags的内部组成。

public struct DispatchWorkItemFlags : OptionSet, RawRepresentable {

    public static let barrier: DispatchWorkItemFlags 

    public static let detached: DispatchWorkItemFlags

    public static let assignCurrentContext: DispatchWorkItemFlags

    public static let noQoS: DispatchWorkItemFlags

    public static let inheritQoS: DispatchWorkItemFlags

    public static let enforceQoS: DispatchWorkItemFlags
}

DispatchWorkItemFlags主要分为两部分:

  • 覆盖
    • noQoS 没有优先级
    • inheritQoS 继承Queue的优先级
    • enforceQoS 覆盖Queue的优先级
  • 执行情况
    • barrier
    • detached
    • assignCurrentContext

执行情况会在下文会具体描述,先在这留个坑。
先来看看设置优先级,会对任务执行有什么影响。

let queue1 = DispatchQueue(label: "com.ffib.blog.workItem1", qos: .utility)
let queue2 = DispatchQueue(label: "com.ffib.blog.workItem2", qos: .userInitiated)
let workItem1 = DispatchWorkItem(qos: .userInitiated) {
    for i in 0..<5 {
        print(i)
    }
}
let workItem2 = DispatchWorkItem(qos: .utility) {
    for i in 5..<10 {
        print(i)
    }
}
queue1.async(execute: workItem1)
queue2.async(execute: workItem2)

output:
5
6
7
8
9
0
1
2
3
4

由结果可见即使设置了DispatchWorkItem仅仅只设置了优先级并不会对任务执行顺序有任何影响。也就是仍然按照queue的优先级执行。
接下来,再来设置DispatchWorkItemFlags试试

let queue1 = DispatchQueue(label: "com.ffib.blog.workItem1", qos: .utility)
let queue2 = DispatchQueue(label: "com.ffib.blog.workItem2", qos: .userInitiated)

let workItem1 = DispatchWorkItem(qos: .userInitiated, flags: .enforceQoS) {
    for i in 0..<5 {
        print(i)
    }
}

let workItem2 = DispatchWorkItem {
    for i in 5..<10 {
        print(i)
    }
}

queue1.async(execute: workItem1)
queue2.async(execute: workItem2)
output:
5
0
6
1
7
2
8
3
9
4

设置enforceQoS,使优先级强制覆盖queue的优先级,所以两个队列呈交替执行状态,变为同一优先级。
DispatchWorkItem也有waitnotify方法,和DispatchGroup用法相同。wait会等待这个workItem执行完毕。会阻塞当前线程。也可以使用cancel()提前取消任务。

// 执行结束通过notify提示主队列
workItem.notify(queue: DispatchQueue.main) {
    print("value = ", value)
}

// wait会等待这个workItem执行完毕。会阻塞当前线程。workItem3会先执行完,之后再执行workItem2
queue12.async(execute: workItem4)
queue13.async(execute: workItem3)
workItem3.wait()

dispatch_once (单次)

一般用于单例

// swift
class Tool: NSObject {
    static let share = Tool()
}

// OC
// Tool.h
@interface Tool : NSObject
+ (instancetype)sharedInstance;
@end
// Tool.m
@implementation Tool
+ (instancetype)sharedInstance {    
    static Tool *instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}
@end

DispatchSemaphore(信号量)

如果你想同步执行一个异步队列任务,可以使用信号量。
wait()会使信号量减一,如果信号量大于1则会返回.success,否则返回timeout(超时),也可以设置超时时间。

func wait(wallTimeout: DispatchWallTime) -> DispatchTimeoutResult
func wait(timeout: DispatchTime) -> DispatchTimeoutResult

signal()会使信号量加一,返回当前信号量。

print("DispatchSemaphore: 开始")
let semaphore = DispatchSemaphore.init(value: 1)
let queue = DispatchQueue.init(label: "semaphore0", qos: .utility)

for i in 0 ..< 5 {
    print("wait: \(i)")
    if semaphore.wait(timeout: .distantFuture) == .success {
        queue.async {
            sleep(2)
            print("semaphore: \(semaphore.signal())、\(i)")
        }
    }
}
print("DispatchSemaphore: 结束")

output:
DispatchSemaphore: 开始
wait: 0
wait: 1
semaphore: 1、0
wait: 2
semaphore: 1、1
wait: 3
semaphore: 1、2
wait: 4
semaphore: 1、3
DispatchSemaphore: 结束
semaphore: 0、4

我们来看下for循环里都发生了什么。第一遍循环遇到wait时,此时信号量为1,大于0,所以if判断为true,进行sleep和打印操作;当第二遍循环遇到wait时,发现信号量为0,此时就会锁死线程,直到上一遍循环的操作完成,调用signal()方法,信号量加一,才会继续执行操作,循环以上操作。

DispatchSemaphore还有另外一个用法,可以限制队列的最大并发量,通过前面所说的wait()信号量减一,signal()信号量加一,来完成此操作,正如上文所述例子,其实达到的效果就是最大并发量为一。
如果使用过NSOperationQueue的同学,应该知道maxConcurrentOperationCount,效果是类似的。

DispatchWorkItemFlags

barrier可以理解为隔离,在读取时,可以异步访问,但是如果突然出现了异步写入操作,我们想要达到的效果是在进行写入操作的时候,使读取操作暂停,直到写入操作结束,再继续进行读取操作,以保证读取操作获取的是最新内容。
预期结果是:在写入操作之前,读取到的内容是a;在写入操作之后,读取到的内容是b(即写入的内容)。
先看看不使用barrier的结果。

print("DispatchWorkItemFlags: 开始")
var testStr = "a"
let queue1 = DispatchQueue.init(label: "flags", attributes: .concurrent)
let readWorkItem = DispatchWorkItem.init {
    sleep(1)
    print(testStr)
}
let writeWorkItem = DispatchWorkItem.init {
    sleep(3)
    testStr = "b"
    print("write")
}
queue1.async(execute: readWorkItem)
queue1.async(execute: writeWorkItem)
queue1.async(execute: readWorkItem)
print("DispatchWorkItemFlags: 结束")
output:
DispatchWorkItemFlags: 开始
DispatchWorkItemFlags: 结束
a
a
write

结果不是我们想要的。再来看看加了barrier之后的效果。

// 将上题writeWorkItem修改初始化方式
let writeWorkItem = DispatchWorkItem.init(flags: [.barrier]) {
    // 里面内容同上,不变
}
output:
DispatchWorkItemFlags: 开始
DispatchWorkItemFlags: 结束
a
write
b

结果符合预期的想法,barrier主要用于读写隔离,以保证写入的时候,不被读取。

dispatch_barrier_async (栅栏)

释义

void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);

dispatch_barrier_async一般叫做“栅栏函数”,它就好像栅栏一样可以将多个操作分隔开,在它前面追加的操作先执行,在它后面追加的操作后执行。
栅栏函数也可以执行队列上的操作(参数列表中有queue和block),也有对应的 dispatch_barrier_sync 函数。

示例:

- (void)testBarrierAsync
{
    //创建一个并行队列
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.gcd.barrier.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    
    //并行操作
    void (^blk1)() = ^{
        NSLog(@"1");
    };
    void (^blk2)() = ^{
        NSLog(@"2");
    };
    void (^blk3)() = ^{
        NSLog(@"3");
    };
    void (^blk4)() = ^{
        NSLog(@"4");
    };
    void (^blk5)() = ^{
        NSLog(@"5");
    };
    void (^blk6)() = ^{
        NSLog(@"6");
    };
    
    //栅栏函数执行操作
    void (^barrierBlk)() = ^{
        NSLog(@"Barrier!");
    };
    
    //执行所有操作
    dispatch_async(concurrentQueue, blk1);
    dispatch_async(concurrentQueue, blk2);
    dispatch_async(concurrentQueue, blk3);
    dispatch_barrier_async(concurrentQueue, barrierBlk);
    dispatch_async(concurrentQueue, blk4);
    dispatch_async(concurrentQueue, blk5);
    dispatch_async(concurrentQueue, blk6);
}

方法执行结果:

 2
 1
 3
 Barrier!
 5
 4
 6

分析:
栅栏函数之前和之后的操作执行顺序都不固定,但是前面三个必然先执行,然后再执行栅栏函数中的操作,最后执行后面的三个。
注意:
dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block)

栅栏函数中传入的参数队列必须是由 dispatch_queue_create 方法创建的队列,否则,与dispatch_async无异,起不到“栅栏”的作用了,对于dispatch_barrier_sync也是同理。

参考文献

  • https://www.jianshu.com/p/2d57c72016c6
  • https://www.cnblogs.com/graveliang/p/5693835.html
  • https://www.jianshu.com/p/d63c3100dd63
  • https://juejin.im/post/6844903545213288461#heading-12

你可能感兴趣的:(iOS 多线程相关)