iOS开发之多线程(3)—— GCD

版本

Xcode 11.5
Swift 5.2.2

简介

Grand Central Dispatch, 强大的中央调度器, 额… 我们还是叫GCD吧.

几个概念

1. 任务(Task) 和 队列(Queue)

  • 任务就是将要在线程中执行的代码(块), GCD中为block. 执行任务有两种方式: 同步执行和异步执行.
    dispatch_async(queue, ^{
        // 任务
    });
  • 队列不是线程, 把这两者区分开会比较容易驾驭本文. 队列是用于装载线程任务的队形结构, 遵循FIFO(先进先出)原则. 队列有两种: 串行队列和并发队列. 另外有两个系统提供的特殊队列: 主队列(串行队列) 和 全局队列(并发队列).
    dispatch_queue_t queue = dispatch_queue_create("我是标签", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        // 这里的任务(代码块)会被添加到队列queue中
    });

2. 同步(sync) 和 异步(async)

  • 同步执行会阻塞当前代码, 不具备开启线程的能力.
    但不代表同步执行就一定在当前线程执行, 例如在其他线程同步执行到主队列, 最终是在主线程执行的(因为主线程始终存在, 所以我们说没有开启新线程). 除了调用主队列, 同步执行的任务都是在当前线程完成.
    // 在其他线程同步执行到主队列
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"1 %@", [NSThread currentThread]);
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"2 %@", [NSThread currentThread]);
        });
    });
  
-----------------------------------------------------------------------------------------------------  
    log:
    1 <NSThread: 0x600000e78140>{number = 4, name = (null)}
    2 <NSThread: 0x600000e30180>{number = 1, name = main}
  • 异步执行不会阻塞当前线程, 具备开启新线程能力.
    但不是说异步执行就一定会开启新线程, 例如异步执行到主队列不会开启新线程, 又例如多个异步执行到相同队列也可能不会开启相应数量的线程.
    // 异步执行+全局队列(并发队列)
    for (int i=0; i<8; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"%d %@", i, [NSThread currentThread]);
        });
    }

-----------------------------------------------------------------------------------------------------
    log:
    0 <NSThread: 0x600000b76680>{number = 5, name = (null)}
    2 <NSThread: 0x600000b75700>{number = 6, name = (null)}
    1 <NSThread: 0x600000b75a40>{number = 4, name = (null)}
    3 <NSThread: 0x600000b76a00>{number = 3, name = (null)}
    4 <NSThread: 0x600000b76680>{number = 5, name = (null)}
    5 <NSThread: 0x600000b75700>{number = 6, name = (null)}
    6 <NSThread: 0x600000b76a00>{number = 3, name = (null)}
    7 <NSThread: 0x600000b75a40>{number = 4, name = (null)}

GCD会根据系统资源控制并行的数量, 所以如果任务很多, 它并不会让所有任务同时执行.

3. 串行(Serial) 和 并发(Concurrent)

队列有两种: 串行队列和并发队列. 这两个队列都是遵循FIFO(先进先出)原则的.

  • 串行队列里的任务是一个一个执行的.
    串行队列.png

  • 并发队列里的任务是可以多个同时执行的.
    虽然并发队列也是一个一个任务取出来(FIFO原则), 但是由于取出来很快(因为可以开启多个线程来执行任务), 我们认为这些已经取出来的任务是同步执行的.
    并发队列.png

注:

  1. 任务被添加到并发队列的顺序是任意的, 所以最终可能以任意顺序完成, 你不会知道何时开始运行下一个任务, 或者任意时刻有多少 Block 在运行. 这些完全取决于 GCD.
  2. GCD 会根据系统资源控制并行的数量, 所以如果任务很多, 它并不会让所有任务同时执行. 也就是说, 开不开启新线程(或者说开启多少条新线程)由GCD决定, 但会保证不会阻塞当前线程.

4. 主队列(Main Queue) 和 全局队列(Global Queue)

  • 主队列也是串行队列, 但是主队列中的所有任务都会被系统放到主线程中执行. 主线程用来处理UI相关操作, 所以不要把耗时操作放到主线程中执行(不要把耗时任务放到主队列中).
  • 全局队列是并发队列, 系统提供了四个枚举优先级常量(background、low、default 以及 high). 注意, 全局队列并不是后台线程, 队列和线程是不同的东西, 全局队列中的任务放到哪个线程去执行由执行方式(同步或异步)以及GCD根据资源分配来决定.

注: Apple 的 API 也会使用这些全局队列, 所以你添加的任何任务都不会是这些队列中唯一的任务.

GCD的基本使用

通过了解前面的概念, 我们知道执行方式(同步和异步)和队列(串行和并发)组合起来有四种写法, 但由于主队列是特殊的队列(主队列中的任务都会被系统放入主线程中去执行), 因此我们还得讨论主队列和两种执行方式的组合. 而全局队列也是并发队列, 故不需要单独拿出来讨论.
综上, 我们将讨论以下六种写法:

  • 同步执行 + 串行队列
  • 同步执行 + 并发队列
  • 异步执行 + 串行队列
  • 异步执行 + 并发队列
  • 同步执行 + 主队列
  • 异步执行 + 主队列

先列表总结一下吧:

串行队列 并发队列 主队列
同步 阻塞 / 不开新线程 / 按顺序 阻塞 / 不开新线程 / 按顺序 阻塞 / 不开新线程(在主线程) / 按顺序
异步 不阻塞 / 不开或只开一条 / 按顺序 不阻塞 / 至少开一条 / 乱序 不阻塞 / 不开新线程(在主线程) / 按顺序

什么时候阻塞?
只要是同步执行肯定会阻塞; 只要是异步执行肯定不阻塞.

什么时候开启新线程(不包括主线程)?
tag1: 需要同时执行两个或两个以上任务(block)时, 才会开启新线程 (因为一个线程同一时间只能执行一个任务).
!!! 前方高能, 请戴好口罩. !!!
首先, 同步执行肯定不会开启新线程. 因为同步执行的目的是为了阻塞当前线程, 等执行完了block, 才会继续往下执行. 也就是说, 同步执行在同一时刻只有一个任务, 所以不会开启新线程.
异步执行可分四种情况讨论:

  1. 当前线程对应的队列是串行队列(串行1), 异步执行+当前队列(串行1)不会开启新线程. 因为当前只有一个队列(串行1), 而这个队列里的任务是一个一个顺序执行的, 没有必要开启新线程.
  2. 当前线程对应的队列是串行队列(串行1), 异步执行+其他串行队列(串行2)会开启一条新线程. 因为异步执行不阻塞串行1当前的任务, 而串行2里面的任务需要同时执行, 所以只能开新线程. 又因为串行2里任务是一个个顺序执行的, 所以只会新开一个线程.
  3. 当前线程对应的队列是并发队列(并发1), 异步执行+串行队列(串行1)会开启一条新线程. 因为并发1当前的任务和串行1里的任务都要同时执行, 所以需要开启新线程. 又因为串行1是串行的, 所以只会开一条线程.
  4. 当前线程对应的队列是并发队列(并发1), 异步执行+并发队列(不管是不是当前队列, 并发2)都会开启新线程. 因为并发1当前的任务和并发2里的任务需要同时执行, 所以只能开启新线程. 又因为并发2是并发的(多个任务同时执行), 所以会开启多条线程. 线程数量由GCD根据当前资源(内存使用状况, 线程池中线程数等因素)决定.

总结: 请回头看tag1.

1. 同步执行 + 串行队列

  • 阻塞当前线程
  • 不开启新线程
  • 任务按顺序依次执行

OC

// 同步执行 + 串行队列
- (void)syncSerial {
    
    NSLog(@"start, %@", [NSThread currentThread]);
    
    dispatch_queue_t queue = dispatch_queue_create("com.KKThreadsDemo.syncSerialQueue", DISPATCH_QUEUE_SERIAL);
    for (int i=0; i<5; i++) {
        dispatch_sync(queue, ^{
            NSLog(@"%d, %@", i, [NSThread currentThread]);
        });
    }
    
    NSLog(@"end, %@", [NSThread currentThread]);
}

-----------------------------------------------------------------------------------------------------
log:
start, <NSThread: 0x600003000240>{number = 1, name = main}
0, <NSThread: 0x600003000240>{number = 1, name = main}
1, <NSThread: 0x600003000240>{number = 1, name = main}
2, <NSThread: 0x600003000240>{number = 1, name = main}
3, <NSThread: 0x600003000240>{number = 1, name = main}
4, <NSThread: 0x600003000240>{number = 1, name = main}
end, <NSThread: 0x600003000240>{number = 1, name = main}

Swift

// 同步执行 + 串行队列
@objc func syncSerial() {
    
    print("start, \(Thread.current)")
    
    let queue = DispatchQueue(label: "com.KKThreadsDemo.syncSerialQueue")
    for i in 0..<5 {
        queue.sync {
            print("\(i), \(Thread.current)")
        }
    }
    
    print("end, \(Thread.current)")
}

2. 同步执行 + 并发队列

  • 阻塞当前线程
  • 不开启新线程
  • 任务按顺序执行

OC

// 同步执行 + 并发队列
- (void)syncConcurrent {
    
    NSLog(@"start, %@", [NSThread currentThread]);
    
    dispatch_queue_t queue = dispatch_queue_create("com.KKThreadsDemo.syncConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    for (int i=0; i<5; i++) {
        dispatch_sync(queue, ^{
            NSLog(@"%d, %@", i, [NSThread currentThread]);
        });
    }
    
    NSLog(@"end, %@", [NSThread currentThread]);
}

-----------------------------------------------------------------------------------------------------
log:
start, <NSThread: 0x600000354280>{number = 1, name = main}
0, <NSThread: 0x600000354280>{number = 1, name = main}
1, <NSThread: 0x600000354280>{number = 1, name = main}
2, <NSThread: 0x600000354280>{number = 1, name = main}
3, <NSThread: 0x600000354280>{number = 1, name = main}
4, <NSThread: 0x600000354280>{number = 1, name = main}
end, <NSThread: 0x600000354280>{number = 1, name = main}

Swift

// 同步执行 + 并发队列
@objc func syncConcurrent() {
    
    print("start, \(Thread.current)")
    
    // 二选一
    let queue = DispatchQueue(label: "com.KKThreadsDemo.syncConcurrentQueue", attributes: .concurrent)
    let queue = DispatchQueue(label: "com.KKThreadsDemo.syncConcurrentQueue", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: .none)
    for i in 0..<5 {
        queue.sync {
            print("\(i), \(Thread.current)")
        }
    }
    
    print("end, \(Thread.current)")
}

3. 异步执行 + 串行队列

  • 不阻塞当前线程
  • 不开或只开一条新线程
  • 任务按顺序执行

OC

// 异步执行 + 串行队列
- (void)asyncSerial {
    
#if 1
    NSLog(@"start, %@", [NSThread currentThread]);

    dispatch_queue_t queue = dispatch_queue_create("com.KKThreadsDemo.asyncSerialQueue", DISPATCH_QUEUE_SERIAL);
    for (int i=0; i<5; i++) {
        dispatch_async(queue, ^{
            NSLog(@"%d, %@", i, [NSThread currentThread]);
        });
    }

    NSLog(@"end, %@", [NSThread currentThread]);
    
#else
    
    /* ------ 如果添加到当前线程对应的队列, 则不开启新线程 ------ */
    dispatch_queue_t queue = dispatch_queue_create("com.KKThreadsDemo.asyncSerialQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        
        NSLog(@"start, %@", [NSThread currentThread]);
        
        for (int i=0; i<5; i++) {
            dispatch_async(queue, ^{
                NSLog(@"%d, %@", i, [NSThread currentThread]);
            });
        }
        
        NSLog(@"end, %@", [NSThread currentThread]);
    });
#endif
}

-----------------------------------------------------------------------------------------------------
log if 1:
start, <NSThread: 0x600003ad00c0>{number = 1, name = main}
end, <NSThread: 0x600003ad00c0>{number = 1, name = main}
0, <NSThread: 0x600000a303c0>{number = 5, name = (null)}
1, <NSThread: 0x600000a303c0>{number = 5, name = (null)}
2, <NSThread: 0x600000a303c0>{number = 5, name = (null)}
3, <NSThread: 0x600000a303c0>{number = 5, name = (null)}
4, <NSThread: 0x600000a303c0>{number = 5, name = (null)}

-----------------------------------------------------------------------------------------------------
log if 0:
start, <NSThread: 0x6000012a7080>{number = 3, name = (null)}
end, <NSThread: 0x6000012a7080>{number = 3, name = (null)}
0, <NSThread: 0x6000012a7080>{number = 3, name = (null)}
1, <NSThread: 0x6000012a7080>{number = 3, name = (null)}
2, <NSThread: 0x6000012a7080>{number = 3, name = (null)}
3, <NSThread: 0x6000012a7080>{number = 3, name = (null)}
4, <NSThread: 0x6000012a7080>{number = 3, name = (null)}

Swift

// 异步执行 + 串行队列
@objc func asyncSerial() {
    
    print("start, \(Thread.current)")
            
    // 二选一
    let queue = DispatchQueue(label: "com.KKThreadsDemo.asyncSerialQueue")
    let queue = DispatchQueue(label: "com.KKThreadsDemo.asyncSerialQueue", attributes: .init(rawValue: 0))
    for i in 0..<5 {
        queue.async {
            print("\(i), \(Thread.current)")
        }
    }
    
    print("end, \(Thread.current)")
}

为什么先打印了任务0再打印end ?
因为新开了一个线程4来执行任务, 且没有阻塞当前线程, 线程4和当前线程形成并行关系, CPU在线程间来回切换运算, 不确定是先执行那一条线程.

4. 异步执行 + 并发队列

  • 不阻塞当前线程
  • 至少开启一条新线程 (数量由GCD根据当前资源决定)
  • 任务乱序执行

OC

// 异步执行 + 并发队列
- (void)asyncConcurrent {
    
    NSLog(@"start, %@", [NSThread currentThread]);

    dispatch_queue_t queue = dispatch_queue_create("com.KKThreadsDemo.asyncConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    for (int i=0; i<5; i++) {
        dispatch_async(queue, ^{
            NSLog(@"%d, %@", i, [NSThread currentThread]);
        });
    }

    NSLog(@"end, %@", [NSThread currentThread]);
}

-----------------------------------------------------------------------------------------------------
log:
start, <NSThread: 0x600000c1c100>{number = 1, name = main}
end, <NSThread: 0x600000c1c100>{number = 1, name = main}
0, <NSThread: 0x600001c02880>{number = 6, name = (null)}
3, <NSThread: 0x600001c023c0>{number = 3, name = (null)}
4, <NSThread: 0x600001c02880>{number = 6, name = (null)}
1, <NSThread: 0x600001c024c0>{number = 5, name = (null)}
2, <NSThread: 0x600001c75c00>{number = 4, name = (null)}

Swift

// 异步执行 + 并发队列
@objc func asyncConcurrent() {
    
    print("start, \(Thread.current)")
            
    // 二选一
    let queue = DispatchQueue(label: "com.KKThreadsDemo.asyncConcurrentQueue", attributes: .concurrent)
    let queue = DispatchQueue(label: "com.KKThreadsDemo.asyncConcurrentQueue", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: .none)
    for i in 0..<5 {
        queue.async {
            print("\(i), \(Thread.current)")
        }
    }
    
    print("end, \(Thread.current)")
}

5. 同步执行 + 主队列

如在主线程中使用, 将会造成死锁, 系统报错. 关于死锁, 详见下小节.

在非主线程中使用:

  • 阻塞当前线程
  • 不开新线程, 在主线程中执行
  • 任务按顺序执行

OC

// 同步执行 + 主队列
- (void)syncMain {
    
    // 在非主线程中使用
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"start, %@", [NSThread currentThread]);

        for (int i=0; i<5; i++) {
            dispatch_sync(dispatch_get_main_queue(), ^{
                NSLog(@"%d, %@", i, [NSThread currentThread]);
            });
        }

        NSLog(@"end, %@", [NSThread currentThread]);
    });
}

-----------------------------------------------------------------------------------------------------
log:
start, <NSThread: 0x60000291a880>{number = 5, name = (null)}
0, <NSThread: 0x6000004e0cc0>{number = 1, name = main}
1, <NSThread: 0x6000004e0cc0>{number = 1, name = main}
2, <NSThread: 0x6000004e0cc0>{number = 1, name = main}
3, <NSThread: 0x6000004e0cc0>{number = 1, name = main}
4, <NSThread: 0x6000004e0cc0>{number = 1, name = main}
end, <NSThread: 0x60000291a880>{number = 5, name = (null)}

Swift

// 同步执行 + 主队列
@objc func syncMain() {
    
    // 在非主线程中使用
    DispatchQueue.global().async {
        
        print("start, \(Thread.current)")

        for i in 0..<5 {
            DispatchQueue.main.sync {
                print("\(i), \(Thread.current)")
            }
        }
        
        print("end, \(Thread.current)")
    }
}

6. 异步执行 + 主队列

  • 不阻塞当前线程
  • 不开新线程, 在主线程中执行
  • 任务按顺序执行 (主线程是串行线程)

OC

// 异步执行 + 主队列
- (void)asyncMain {
    
    // 可在任意线程中使用
//    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"start, %@", [NSThread currentThread]);

        for (int i=0; i<5; i++) {
            dispatch_async(dispatch_get_main_queue(), ^{
                NSLog(@"%d, %@", i, [NSThread currentThread]);
            });
        }

        NSLog(@"end, %@", [NSThread currentThread]);
//    });
}

-----------------------------------------------------------------------------------------------------
log:
start, <NSThread: 0x600001f94100>{number = 1, name = main}
end, <NSThread: 0x600001f94100>{number = 1, name = main}
0, <NSThread: 0x600001f94100>{number = 1, name = main}
1, <NSThread: 0x600001f94100>{number = 1, name = main}
2, <NSThread: 0x600001f94100>{number = 1, name = main}
3, <NSThread: 0x600001f94100>{number = 1, name = main}
4, <NSThread: 0x600001f94100>{number = 1, name = main}

Swift

// 异执行 + 主队列
@objc func asyncMain() {
    
    // 可在任意线程中使用
    DispatchQueue.global().async {
        
        print("start, \(Thread.current)")

        for i in 0..<5 {
            DispatchQueue.main.async {
                print("\(i), \(Thread.current)")
            }
        }
        
        print("end, \(Thread.current)")
    }
}

死锁

什么是死锁?

网友
两个线程卡住了, 彼此等待对方完成或执行其他操作.

苹果
You attempted to lock a system resource that would have resulted in a deadlock.
您试图锁定可能导致死锁的系统资源。

百度
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。

维基
In an operating system, a deadlock occurs when a process or thread enters a waiting state because a requested system resource is held by another waiting process, which in turn is waiting for another resource held by another waiting process. If a process is unable to change its state indefinitely because the resources requested by it are being used by another waiting process, then the system is said to be in a deadlock.
在操作系统中,当进程或线程进入等待状态时会发生死锁,因为所请求的系统资源由另一个等待进程持有,而该等待进程又正在等待另一个等待进程持有的另一个资源。如果某个进程由于另一个进程正在使用该进程所请求的资源而无法无限期更改其状态,则称该系统处于死锁状态

以上定义, 我都不满意.


在GCD中, 当线程进入等待状态时, 该线程中的一个任务和另一个任务形成循环依赖 (即每个任务都要等待另一个任务执行完, 自己才能开始或继续), 这种陷入僵局的无限等待状态称之为死锁状态, 也即死锁.

🍺🍺🍺

造成死锁的原因?
两个任务(block)相互等待.

为什么会等待?
举例说明:

    dispatch_queue_t queue = dispatch_queue_create("com.KKThreadsDemo.deadlockQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{

        // block1

        dispatch_sync(queue, ^{
            // block2
        });
    });

例子中, 在block1中同步提交了一个任务block2到队列queue, 这时queue里面就同时有了两个任务: block1和block2. 一方面, block1被阻塞, 需要等block2执行完返回后才算执行结束; 另一方面, 由于这queue是串行的, 只能一个个任务顺序执行 (前一个任务执行完了后一个才能开始), 也就是说block1不执行完block2是没办法从队列中取出来开始执行的, 没开始何谈结束? 你以为谈恋爱啊😂😂 就这样, 死锁了.

举个反例吧, 上面例子中DISPATCH_QUEUE_SERIAL改为DISPATCH_QUEUE_CONCURRENT就不会死锁了:

dispatch_queue_t queue = dispatch_queue_create("com.KKThreadsDemo.deadlockQueue", DISPATCH_QUEUE_CONCURRENT);

解析
queue变成了并发队列, 并发队列里的任务是可以多个同时执行的, 或者说, 不用等前面的任务执行完就可以立马取出下一个任务来执行. 例子中, 虽然此时线程只有一个(因为同步执行), 猜测是block1被保存了上下文, 中断去执行block2, block2返回后再根据上下文取出block1继续执行.

结论: 同步提交一个任务到一个串行队列, 并且这个队列与执行当前代码的队列相同, 则一定会导致死锁.

以下苹果文档可作为佐证:

苹果文档
If the queue you pass as a parameter to the function is a serial queue and is the same one executing the current code, calling these functions will deadlock the queue.

事实上, 这个结论是我见到过的所有GCD中死锁的情况了. 如果有人有不同见解, 还望不因赐教. 😁😁

几种常见的死锁情形 (都符合上文结论):

// 死锁 (主线程里调用)
- (void)deadlock {
    
    // 死锁1
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"%@", [NSThread currentThread]);
    });
    

    // 死锁2
    dispatch_queue_t queue2 = dispatch_queue_create("com.KKThreadsDemo.deadlockQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue2, ^{

        NSLog(@"start, %@", [NSThread currentThread]);

        dispatch_sync(queue2, ^{
            NSLog(@"1, %@", [NSThread currentThread]);
        });

        NSLog(@"end, %@", [NSThread currentThread]);
    });
    
    
    // 死锁3
    dispatch_queue_t queue3 = dispatch_queue_create("com.KKThreadsDemo.deadlockQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue3, ^{

        NSLog(@"start, %@", [NSThread currentThread]);

        dispatch_sync(dispatch_get_main_queue(), ^{

            NSLog(@"1, %@", [NSThread currentThread]);

            dispatch_sync(queue3, ^{
                NSLog(@"2, %@", [NSThread currentThread]);
            });
        });

        NSLog(@"end, %@", [NSThread currentThread]);
    });
    
    
    // 死锁4
    dispatch_queue_t queue4 = dispatch_queue_create("com.KKThreadsDemo.deadlockQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue4, ^{

        NSLog(@"start, %@", [NSThread currentThread]);

        dispatch_barrier_sync(queue4, ^{
            NSLog(@"1 %@", [NSThread currentThread]);
        });
        
        NSLog(@"end %@", [NSThread currentThread]);
    });
}

解决办法有二:

  1. 使用async
  2. 提交任务到另一个串行/并发队列 (只要不是当前正在执行的串行队列就行)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章