iOS多線程Swift GCD 一:Dispatch Queue

前言:

Dispatch(Grand Central Dispatch)(超級中二的命名
與pthread和Thread不同的是,GCD增加了兩個很重要的概念,任務(wù)和隊列,它是iOS多線程的核心框架.

  • 任務(wù)(Work Item, source等
    任務(wù)就是要執(zhí)行的代碼段,很容易理解,GCD提供了很多任務(wù)的形式,比如DispatchWorkItem,DispatchSource,DispatchGroup,或者是一個block中的代碼.

  • 隊列(queue
    隊列指的是任務(wù)的隊列,或者叫調(diào)度隊列(Dispatch queue),但這個是抽象的概念,并不是數(shù)據(jù)結(jié)構(gòu)上的隊列,不僅可以存放任務(wù),還可以指定任務(wù)如何執(zhí)行,隊列有兩種,串行隊列DISPATCH_QUEUE_SERIAL和并發(fā)隊列DISPATCH_QUEUE_CONCURRENT.
    隊列永遠(yuǎn)都是先進(jìn)先出,串行會等待前面的任務(wù)執(zhí)行完,再執(zhí)行下一個任務(wù),并發(fā)的主旨是讓任務(wù)一起執(zhí)行,具體能不能做到還有其他限制,但是并發(fā)仍然是先進(jìn)的先執(zhí)行,只不過不會等待前面的任務(wù)結(jié)束,并且無法得知某個任務(wù)什么時候開始,什么時候結(jié)束,這些都取決于GCD自身.

  • 同步和異步(sync和async
    只有串行隊列和并發(fā)隊列還不能決定任務(wù)如何執(zhí)行,還需要指定是否創(chuàng)建線程,也就是指定同步sync還是異步async.同步不會開啟新的線程,異步一定會開啟新線程.
    a.串行隊列指定任務(wù)需要一個個執(zhí)行,如果指定同步sync進(jìn)行這個隊列,則會在當(dāng)前的線程中執(zhí)行;
    b.如果串行隊列指定異步async進(jìn)行,由于線程是cpu最小調(diào)度單位,沒辦法在兩個任務(wù)之間調(diào)度,那GCD只好開啟一個新的線程來進(jìn)行這個隊列,之后就和a是一樣的邏輯了;
    c.并發(fā)隊列指定任務(wù)同時進(jìn)行,如果指定sync,那么就不會生效
    d.如果并發(fā)隊列指定異步進(jìn)行,那么GCD會根據(jù)任務(wù)數(shù)量創(chuàng)建一個或多個新的線程同時執(zhí)行,當(dāng)然線程的數(shù)量還會受到GCD底層實(shí)現(xiàn)的限制,一方面如果線程太多,來一個任務(wù)就開一個線程,還要隊列干嘛,另一方面太多線程占用更多資源,一般最多是64個.

不管是主線程還是其他線程,一般都不會直接操作,需要關(guān)注的是任務(wù)本身,而管理任務(wù)的則是隊列,同步還是異步是任務(wù)的執(zhí)行策略,而執(zhí)行策略決定會不會開啟線程.

  • 并發(fā)與并行:
    并行指任務(wù)同時執(zhí)行,是系統(tǒng)的行為,并發(fā)指的是代碼的設(shè)計,希望代碼的不同部分能夠同時執(zhí)行,是不是真的能"同時"執(zhí)行取決于系統(tǒng),多核可以真正的同時執(zhí)行,而CPU自己也可以通過快速切換上下文來"同時"執(zhí)行多個任務(wù);雖然能寫出并發(fā)的代碼,但是是不是真的并行還要看GCD自己;并行的前提是并發(fā),并發(fā)不能保證并行.
    線程是cpu調(diào)度的方式,cpu只能處理一個任務(wù),一個線程最多占用cpu幾毫秒的時間,之后cpu會調(diào)度到其他線程,如果任務(wù)沒執(zhí)行完,之后還會調(diào)度回來繼續(xù)執(zhí)行.

一:基本使用

本篇講的是swift的Dispatch庫,大多數(shù)東西和OC的Dispatch相差不大,但是也優(yōu)化了一些東西.

1.主線程(主隊列)
主線程是唯一能夠更新UI的線程,通常說的主線程其實(shí)指的是主隊列(DispatchQueue.main),它是一個串行隊列,里面的任務(wù)都會在主線程中串行執(zhí)行;
GCD使用線程池來管理線程, 除了主隊列只會在主線程進(jìn)行之外(一開始就創(chuàng)建好了),任何任務(wù)都不能確定在哪個線程上進(jìn)行;當(dāng)然可以在運(yùn)行的過程中查看當(dāng)前線程.
就像前面說的,同步和異步?jīng)Q定是否開啟線程,同步任務(wù)會在當(dāng)前線程等待別的任務(wù)完成,如果添加一個同步的任務(wù),并且指定在主隊列中,會造成主隊列的死鎖.

文檔中強(qiáng)調(diào)不能在主隊列中添加同步任務(wù)

但是這個現(xiàn)象不是特例,這個過程可以簡化成這樣:

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

為了和主線程區(qū)分開,這段代碼先創(chuàng)建了一個線程,假設(shè)叫T,里面的代碼不在主線程運(yùn)行;
然后創(chuàng)建了一個串行隊列queue,添加了一個同步的任務(wù)A,于是A會在線程T中執(zhí)行,在A中又添加了一個同步任務(wù)B到同一個隊列中,B會在A執(zhí)行完后再執(zhí)行,也就是走完c的位置,但是c的位置依賴于B的代碼走完,于是就堵在了這里;簡單來說就是A執(zhí)行了一半把B塞了進(jìn)去,他倆在同一個線程中誰都不能先完成.
再重新思考主隊列的死鎖,如果同步任務(wù)的代碼寫在viewDidLoad里,那么主隊列的任務(wù)要在viewDidLoad至少走完才會結(jié)束,添加在viewDidLoad中間的同步任務(wù)和viewDidLoad本身互相等待造成死鎖.



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

如果把任務(wù)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中執(zhí)行(兩個print輸出的結(jié)果一樣),會按照添加的順序執(zhí)行.



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

再或者把B異步添加到隊列queue,也能正常執(zhí)行,這時gcd會開啟新線程,兩個print結(jié)果不一樣.



        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換成并發(fā)隊列,同步執(zhí)行不會開啟線程,0,1,2都是同一個線程,但是不會死鎖,并發(fā)隊列可以看出多軌道,但是線程只能只能執(zhí)行一個任務(wù),如果只有一個線程,就只好一個個的執(zhí)行,但是不會造成第一個例子的那種阻塞.
總結(jié)一下就是,同步串行再加上任務(wù)重疊,就會造成死鎖



  • 前言說到串行隊列添加異步任務(wù)會開啟新的線程,但是主線程例外,主隊列的任務(wù)只會運(yùn)行在主線程.
      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不會,并且子線程的執(zhí)行速度還更快,因?yàn)閙ain.async里的任務(wù)要等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相繼執(zhí)行.



2.全局隊列
GCD隊列黔州,系統(tǒng)隊列編號有11個尿招,1為主隊列,2為管理隊列,3保留度苔;4-11為8個全局隊列伪货,有四種優(yōu)先級(quality-of-service) ,對應(yīng)文檔里從上到下是優(yōu)先級從高到低,不過還有一個default和一個暫不指定unspecified
這些隊列是系統(tǒng)提供的,初始化其實(shí)是獲取而非創(chuàng)建,而且可能會有系統(tǒng)的任務(wù)在里面,因此開發(fā)者的代碼不是其中唯一的任務(wù).

DispatchQueue.global(),可以獲取一個默認(rèn)的全局隊列.
DispatchQueue.global(qos:),獲取一個指定優(yōu)先級的全局隊列

      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)")
        }

不同優(yōu)先級的全局隊列,cpu調(diào)度的優(yōu)先度不同,即便1寫在后面,也是先執(zhí)行


image.png


3.自定義隊列
事實(shí)上,自定義的隊列最終都會指向系統(tǒng)創(chuàng)建的隊列,雖然是init,但其實(shí)是獲取已經(jīng)存在的或者系統(tǒng)在需要的時候創(chuàng)建的隊列,之所以這么做,是為了方便管理任務(wù),或者說給系統(tǒng)隊列取個別名.

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

自定義一個串行隊列

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

自定義一個并發(fā)隊列

  • label是取個名字
  • qos參數(shù)和獲取全局隊列是一樣的,
  • attributes有兩個值, concurrent(創(chuàng)建一個并發(fā)隊列), initiallyInactive(需要手動調(diào)用activate觸發(fā)),如果傳nil,則返回串行隊列,并且可以傳數(shù)組[concurrent,initiallyInactive]
  • autoreleaseFrequency是任務(wù)相關(guān)的自動釋放,有三個值,inherit跟隨后面的target隊列,后面再說; workItem按照任務(wù)的周期,取決于任務(wù)本身; never不主動釋放,需要手動管理
  • target是獲取已存在的隊列,這個目標(biāo)隊列決定了最終返回的隊列的屬性.

4.任務(wù)運(yùn)行在哪個線程
前言提到了隊列是開發(fā)者關(guān)注的重點(diǎn),隊列的屬性是串行或者并發(fā);
同步還是異步是任務(wù)的執(zhí)行策略,這其中還好包含優(yōu)先級等屬性,在下一篇會說到;

        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

這個例子創(chuàng)建一個串行隊列,循環(huán)100次,偶數(shù)時向隊列添加異步任務(wù),奇數(shù)添加同步任務(wù),看一下控制臺輸出
可以看到一定是按照順序輸出的,串行隊列規(guī)定后添加的任務(wù)要在前面的任務(wù)完成后才開始,同步和異步只決定是否開啟新的線程,并且可以看到同步的任務(wù)當(dāng)前線程執(zhí)行(這里是主線程),異步的在其他線程,具體是哪一個線程,由GCD決定,每一次的運(yùn)行不一定會相同.



 override func viewDidLoad() {
        super.viewDidLoad()

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

image.png

所謂當(dāng)前的線程,主要看任務(wù)如何被添加到隊列,任務(wù)(代碼)一定是寫在函數(shù)里的,至少要在函數(shù)里被調(diào)用,例如上面這段代碼,直接寫viewDidLoad(),viewDidLoad運(yùn)行在主線程,如果這時添加一個同步任務(wù),不管是添加到那個隊列里去(主隊列自然不行),代碼運(yùn)行到這,所在的線程就是主線程;



         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

同樣的,上面是一個異步的代碼塊,在其中添加了一個同步任務(wù),因此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

這段代碼運(yùn)行時,先等待了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

把上面的代碼改造一下 ,單數(shù)同步,偶數(shù)異步,仍然是飛快的輸出



          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

再改造一下,給同步的任務(wù)添加一個耗時2秒,再運(yùn)行;
現(xiàn)在發(fā)現(xiàn)兩秒跳一次,說明異步在等待同步,為什么異步?jīng)]有一瞬間執(zhí)行完,讓同步自己去慢慢跑呢:

并發(fā)隊列和串行隊列都是先進(jìn)先出,只不過并發(fā)隊列里的任務(wù)不用等待前面的任務(wù)執(zhí)行完,但是要等前面的任務(wù)開始了,后面的才能開始;
同步的任務(wù)這里在主線程執(zhí)行,主線程就一個,線程也只能做一件事,做完了才能做其他事,前面異步任務(wù)一瞬間完成,是因?yàn)殚_了很多個線程;
這個例子i=3的同步任務(wù)要等待i=1完成之后才能進(jìn)行,而i=3不開始,456...也不能開始,所以后面的任務(wù)雖然是異步的,隊列也是并發(fā)的,但是卻被迫等待前面的任務(wù)完成.



//        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 {} 或者再自定義一個異步也是一樣的,主線程會變成另一個線程,但是仍然要等待.因?yàn)閍sync {}里的線程已經(jīng)確定了,而且就一個.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末巍虫,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子惧互,更是在濱河造成了極大的恐慌,老刑警劉巖喇伯,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件喊儡,死亡現(xiàn)場離奇詭異,居然都是意外死亡稻据,警方通過查閱死者的電腦和手機(jī)管宵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來攀甚,“玉大人箩朴,你說我怎么就攤上這事∏锒龋” “怎么了炸庞?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長荚斯。 經(jīng)常有香客問我埠居,道長,這世上最難降的妖魔是什么事期? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任滥壕,我火速辦了婚禮,結(jié)果婚禮上兽泣,老公的妹妹穿的比我還像新娘绎橘。我一直安慰自己,他們只是感情好唠倦,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布称鳞。 她就那樣靜靜地躺著,像睡著了一般稠鼻。 火紅的嫁衣襯著肌膚如雪冈止。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天候齿,我揣著相機(jī)與錄音熙暴,去河邊找鬼。 笑死慌盯,一個胖子當(dāng)著我的面吹牛周霉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播润匙,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼诗眨,長吁一口氣:“原來是場噩夢啊……” “哼唉匾!你這毒婦竟也來了孕讳?” 一聲冷哼從身側(cè)響起匠楚,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎厂财,沒想到半個月后芋簿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡璃饱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年与斤,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片荚恶。...
    茶點(diǎn)故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡撩穿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出谒撼,到底是詐尸還是另有隱情食寡,我是刑警寧澤,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布廓潜,位于F島的核電站抵皱,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏辩蛋。R本人自食惡果不足惜呻畸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望悼院。 院中可真熱鬧伤为,春花似錦、人聲如沸据途。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽昨凡。三九已至爽醋,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間便脊,已是汗流浹背蚂四。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留哪痰,地道東北人遂赠。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像晌杰,于是被迫代替她去往敵國和親跷睦。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評論 2 345