iOS多线程Swift GCD 一:Dispatch Queue

前言:

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

  • 任务(Work Item, source等
    任务就是要执行的代码段,很容易理解,GCD提供了很多任务的形式,比如DispatchWorkItem,DispatchSource,DispatchGroup,或者是一个block中的代码.

  • 队列(queue
    队列指的是任务的队列,或者叫调度队列(Dispatch queue),但这个是抽象的概念,并不是数据结构上的队列,不仅可以存放任务,还可以指定任务如何执行,队列有两种,串行队列DISPATCH_QUEUE_SERIAL和并发队列DISPATCH_QUEUE_CONCURRENT.
    队列永远都是先进先出,串行会等待前面的任务执行完,再执行下一个任务,并发的主旨是让任务一起执行,具体能不能做到还有其他限制,但是并发仍然是先进的先执行,只不过不会等待前面的任务结束,并且无法得知某个任务什么时候开始,什么时候结束,这些都取决于GCD自身.

  • 同步和异步(sync和async
    只有串行队列和并发队列还不能决定任务如何执行,还需要指定是否创建线程,也就是指定同步sync还是异步async.同步不会开启新的线程,异步一定会开启新线程.
    a.串行队列指定任务需要一个个执行,如果指定同步sync进行这个队列,则会在当前的线程中执行;
    b.如果串行队列指定异步async进行,由于线程是cpu最小调度单位,没办法在两个任务之间调度,那GCD只好开启一个新的线程来进行这个队列,之后就和a是一样的逻辑了;
    c.并发队列指定任务同时进行,如果指定sync,那么就不会生效
    d.如果并发队列指定异步进行,那么GCD会根据任务数量创建一个或多个新的线程同时执行,当然线程的数量还会受到GCD底层实现的限制,一方面如果线程太多,来一个任务就开一个线程,还要队列干嘛,另一方面太多线程占用更多资源,一般最多是64个.

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

  • 并发与并行:
    并行指任务同时执行,是系统的行为,并发指的是代码的设计,希望代码的不同部分能够同时执行,是不是真的能"同时"执行取决于系统,多核可以真正的同时执行,而CPU自己也可以通过快速切换上下文来"同时"执行多个任务;虽然能写出并发的代码,但是是不是真的并行还要看GCD自己;并行的前提是并发,并发不能保证并行.
    线程是cpu调度的方式,cpu只能处理一个任务,一个线程最多占用cpu几毫秒的时间,之后cpu会调度到其他线程,如果任务没执行完,之后还会调度回来继续执行.

一:基本使用

本篇讲的是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)

自定义一个并发队列

  • label是取个名字
  • qos参数和获取全局队列是一样的,
  • attributes有两个值, concurrent(创建一个并发队列), initiallyInactive(需要手动调用activate触发),如果传nil,则返回串行队列,并且可以传数组[concurrent,initiallyInactive]
  • autoreleaseFrequency是任务相关的自动释放,有三个值,inherit跟随后面的target队列,后面再说; workItem按照任务的周期,取决于任务本身; never不主动释放,需要手动管理
  • target是获取已存在的队列,这个目标队列决定了最终返回的队列的属性.

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 {}里的线程已经确定了,而且就一个.

你可能感兴趣的:(iOS多线程Swift GCD 一:Dispatch Queue)