iOS多线程Swift GCD 一:Dispatch Queue

2021-03-29  本文已影响0人  Trigger_o

前言:

Dispatch(Grand Central Dispatch)(超级中二的命名
与pthread和Thread不同的是,GCD增加了两个很重要的概念,任务和队列,它是iOS多线程的核心框架.

不管是主线程还是其他线程,一般都不会直接操作,需要关注的是任务本身,而管理任务的则是队列,同步还是异步是任务的执行策略,而执行策略决定会不会开启线程.

一:基本使用

本篇讲的是swift的Dispatch库,大多数东西和OC的Dispatch相差不大,但是也优化了一些东西.

1.主线程(主队列)
主线程是唯一能够更新UI的线程,通常说的主线程其实指的是主队列(DispatchQueue.main),它是一个串行队列,里面的任务都会在主线程中串行执行;
GCD使用线程池来管理线程, 除了主队列只会在主线程进行之外(一开始就创建好了),任何任务都不能确定在哪个线程上进行;当然可以在运行的过程中查看当前线程.
就像前面说的,同步和异步决定是否开启线程,同步任务会在当前线程等待别的任务完成,如果添加一个同步的任务,并且指定在主队列中,会造成主队列的死锁.

文档中强调不能在主队列中添加同步任务
但是这个现象不是特例,这个过程可以简化成这样:
      DispatchQueue.global().async {
            let queue = DispatchQueue.init(label: "test")
            queue.sync {//A
                print("aaa")
                queue.sync {//B
                    print("bbb")
                }
            }//c
        }

为了和主线程区分开,这段代码先创建了一个线程,假设叫T,里面的代码不在主线程运行;
然后创建了一个串行队列queue,添加了一个同步的任务A,于是A会在线程T中执行,在A中又添加了一个同步任务B到同一个队列中,B会在A执行完后再执行,也就是走完c的位置,但是c的位置依赖于B的代码走完,于是就堵在了这里;简单来说就是A执行了一半把B塞了进去,他俩在同一个线程中谁都不能先完成.
再重新思考主队列的死锁,如果同步任务的代码写在viewDidLoad里,那么主队列的任务要在viewDidLoad至少走完才会结束,添加在viewDidLoad中间的同步任务和viewDidLoad本身互相等待造成死锁.



DispatchQueue.global().async {
            let queue = DispatchQueue.init(label: "test")
            queue.sync {//A
                print("aaa")
            }//c
           queue.sync {//B
               print("bbb")
            }     
 }

如果把任务B放在c外面,就没有问题



DispatchQueue.global().async {
            let queue = DispatchQueue.init(label: "test")
            queue.sync {//A
                print(Thread.current)
                let queue2 = DispatchQueue.init(label: "test")
                queue2.sync {//B
                    print(Thread.current)
                }
            }//c
        }

如果把B放在另一个队列queue2,也不会有问题,A和B仍然都在线程T中执行(两个print输出的结果一样),会按照添加的顺序执行.



DispatchQueue.global().async {
            let queue = DispatchQueue.init(label: "test")
            queue.sync {//A
                print(Thread.current)
                queue.async{//B
                    print(Thread.current)
                }
            }//c
        }

再或者把B异步添加到队列queue,也能正常执行,这时gcd会开启新线程,两个print结果不一样.



        DispatchQueue.global().async {
            print("0.\(Thread.current)")
            let queue = DispatchQueue.init(label: "test", qos: .background, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil)
            queue.sync {//A
                print("1.\(Thread.current)")
                queue.sync{//B
                    print("2.\(Thread.current)")
                }
            }//c
        }

最后一种,把queue换成并发队列,同步执行不会开启线程,0,1,2都是同一个线程,但是不会死锁,并发队列可以看出多轨道,但是线程只能只能执行一个任务,如果只有一个线程,就只好一个个的执行,但是不会造成第一个例子的那种阻塞.
总结一下就是,同步串行再加上任务重叠,就会造成死锁



      DispatchQueue.init(label: "q1").async {
            print("aaa -- \(Thread.current)")
        }
        DispatchQueue.main.async {
            print("bbb -- \(Thread.current)")
        }
        DispatchQueue.init(label: "q2").async {
            print("ccc -- \(Thread.current)")
        }
image.png

aaa和ccc会开启新的线程,而bbb不会,并且子线程的执行速度还更快,因为main.async里的任务要等viewDidLoad走完.



       DispatchQueue.main.async {
            print("1:\(Thread.current)")
            let queue = DispatchQueue.init(label: "q2")
            queue.sync {
                print("2:\(Thread.current)")
            }
        }
        print("3:\(Thread.current)")
image.png

可以看到3先输出,也就是viewDidLoad先走完了,之后1和2相继执行.



2.全局队列
GCD队列,系统队列编号有11个,1为主队列,2为管理队列,3保留;4-11为8个全局队列,有四种优先级(quality-of-service) ,对应文档里从上到下是优先级从高到低,不过还有一个default和一个暂不指定unspecified
这些队列是系统提供的,初始化其实是获取而非创建,而且可能会有系统的任务在里面,因此开发者的代码不是其中唯一的任务.

DispatchQueue.global(),可以获取一个默认的全局队列.
DispatchQueue.global(qos:),获取一个指定优先级的全局队列

      let queue2 = DispatchQueue.global(qos: DispatchQoS.QoSClass.background)
        queue2.async {
            print("2.\(Thread.current)")
        }
        
        let queue = DispatchQueue.global(qos:DispatchQoS.QoSClass.userInteractive)
        queue.async {
            print("1.\(Thread.current)")
        }

不同优先级的全局队列,cpu调度的优先度不同,即便1写在后面,也是先执行


image.png

3.自定义队列
事实上,自定义的队列最终都会指向系统创建的队列,虽然是init,但其实是获取已经存在的或者系统在需要的时候创建的队列,之所以这么做,是为了方便管理任务,或者说给系统队列取个别名.

let queue = DispatchQueue.init(label: "test")

自定义一个串行队列

let queue = DispatchQueue.init(label: "test", qos: .background, attributes: .concurrent, autoreleaseFrequency: . workItem, target: nil)

自定义一个并发队列

4.任务运行在哪个线程
前言提到了队列是开发者关注的重点,队列的属性是串行或者并发;
同步还是异步是任务的执行策略,这其中还好包含优先级等属性,在下一篇会说到;

        let queue = DispatchQueue.init(label: "aaa")
        for i in 0 ..< 100{
            if i % 2 == 0{
                queue.async {
                    sleep(2)
                    print("\(i): even -- async -- \(Thread.current)")
                }
            }else{
                queue.sync {
                    sleep(1)
                    print("\(i): uneven -- sync -- \(Thread.current)")
                }
            }
        }
image.png

这个例子创建一个串行队列,循环100次,偶数时向队列添加异步任务,奇数添加同步任务,看一下控制台输出
可以看到一定是按照顺序输出的,串行队列规定后添加的任务要在前面的任务完成后才开始,同步和异步只决定是否开启新的线程,并且可以看到同步的任务当前线程执行(这里是主线程),异步的在其他线程,具体是哪一个线程,由GCD决定,每一次的运行不一定会相同.



 override func viewDidLoad() {
        super.viewDidLoad()

        let queue = DispatchQueue.init(label: "aaa")
        queue.sync {
            print(Thread.current)
        }
    }

image.png

所谓当前的线程,主要看任务如何被添加到队列,任务(代码)一定是写在函数里的,至少要在函数里被调用,例如上面这段代码,直接写viewDidLoad(),viewDidLoad运行在主线程,如果这时添加一个同步任务,不管是添加到那个队列里去(主队列自然不行),代码运行到这,所在的线程就是主线程;



         DispatchQueue.global().async {
            print("1:\(Thread.current)")
            let queue = DispatchQueue.init(label: "q2")
            queue.sync {
                print("2:\(Thread.current)")
            }
        }
        print("3:\(Thread.current)")
image.png

同样的,上面是一个异步的代码块,在其中添加了一个同步任务,因此1和2是同一个线程,3又回到了主线程



        let queue = DispatchQueue.init(label: "aaa", qos: .default, attributes: .concurrent, autoreleaseFrequency: .workItem, target: nil)
        for i in 0 ..< 100{
                queue.async {
                    sleep(3)
                    print("\(i): even -- async -- \(Thread.current)")
                }
          }
image.png

这段代码运行时,先等待了3秒,然后飞快的输出了100次,可以看到开启了很多线程



          let queue = DispatchQueue.init(label: "aaa", qos: .default, attributes: .concurrent, autoreleaseFrequency: .workItem, target: nil)
            for i in 0 ..< 100{
                if i % 2 == 0{
                    queue.async {
                        print("\(i): even -- async -- \(Thread.current) -- \(Int(Date.init().timeIntervalSince1970))")
                    }
                }else{
                    queue.sync {
                        print("\(i): uneven -- sync \(Thread.current) -- \(Int(Date.init().timeIntervalSince1970))")
                    }
                }
            }
image.png

把上面的代码改造一下 ,单数同步,偶数异步,仍然是飞快的输出



          let queue = DispatchQueue.init(label: "aaa", qos: .default, attributes: .concurrent, autoreleaseFrequency: .workItem, target: nil)
            for i in 0 ..< 100{
                if i % 2 == 0{
                    queue.async {
                        print("\(i): even -- async -- \(Thread.current) -- \(Int(Date.init().timeIntervalSince1970))")
                    }
                }else{
                    queue.sync {
                        sleep(2)
                        print("\(i): uneven -- sync \(Thread.current) -- \(Int(Date.init().timeIntervalSince1970))")
                    }
                }
            }
image.png

再改造一下,给同步的任务添加一个耗时2秒,再运行;
现在发现两秒跳一次,说明异步在等待同步,为什么异步没有一瞬间执行完,让同步自己去慢慢跑呢:

并发队列和串行队列都是先进先出,只不过并发队列里的任务不用等待前面的任务执行完,但是要等前面的任务开始了,后面的才能开始;
同步的任务这里在主线程执行,主线程就一个,线程也只能做一件事,做完了才能做其他事,前面异步任务一瞬间完成,是因为开了很多个线程;
这个例子i=3的同步任务要等待i=1完成之后才能进行,而i=3不开始,456...也不能开始,所以后面的任务虽然是异步的,队列也是并发的,但是却被迫等待前面的任务完成.



//        DispatchQueue.global().async {
        DispatchQueue.init(label: "q", qos: .default, attributes: .concurrent, autoreleaseFrequency: .workItem, target: nil).async {
            let queue = DispatchQueue.init(label: "aaa", qos: .default, attributes: .concurrent, autoreleaseFrequency: .workItem, target: nil)
            for i in 0 ..< 100{
                if i % 2 == 0{
                    queue.async {
                        //                    sleep(3)
                        print("\(i): even -- async -- \(Thread.current) -- \(Int(Date.init().timeIntervalSince1970))")
                    }
                }else{
                    queue.sync {
                        sleep(2)
                        print("\(i): uneven -- sync \(Thread.current) -- \(Int(Date.init().timeIntervalSince1970))")
                    }
                }
            }
        }
//        }
image.png

这个例子在最外面套上DispatchQueue.global().async {} 或者再自定义一个异步也是一样的,主线程会变成另一个线程,但是仍然要等待.因为async {}里的线程已经确定了,而且就一个.

上一篇 下一篇

猜你喜欢

热点阅读