iOS之武功秘籍?: 多線程原理與GCD和NSOperation

iOS之武功秘籍 文章匯總

寫在前面

多線程在iOS中有著舉足輕重的地位甥角,那么本篇文章就來帶你全面走進她.....

本節(jié)可能用到的秘籍Demo

一、基本概念及原理

① 線程控妻、進程與隊列

①.1 線程的定義

  • 線程是進程的基本執(zhí)行單元迂烁,一個進程的所有任務(wù)都在線程中執(zhí)行
  • 進程想要執(zhí)行任務(wù)看尼,必須得有線程,進程至少要有一條線程
  • 程序啟動會默認開啟一條線程盟步,這條線程被成為主線程UI線程

①.2 進程的定義

  • 進程是指在系統(tǒng)中正在運行的一個應(yīng)用程序藏斩,如微信、支付寶app都是一個進程
  • 每個進程之間是獨立的却盘,每個進程均運行在其專用的且受保護的內(nèi)存空間內(nèi)
  • 通過“活動監(jiān)視器”可以查看mac系統(tǒng)中所開啟的線程

所以狰域,可以簡單的理解為:進程是線程的容器媳拴,而線程用來執(zhí)行任務(wù).在iOS中是單進程開發(fā),一個進程就是一個app兆览,進程之間是相互獨立的屈溉,如支付寶、微信抬探、qq等子巾,這些都是屬于不同的進程.

①.3 進程與線程的關(guān)系和區(qū)別

  • 地址空間:同一進程線程共享本進程的地址空間,而進程之間則是獨立的地址空間
  • 資源擁有:同一進程內(nèi)的線程共享本進程的資源如內(nèi)存、I/O、cpu等汁胆,但是進程之間的資源是獨立的
  • 兩個之間的關(guān)系就相當(dāng)于工廠與流水線的關(guān)系,工廠與工廠之間是相互獨立的仪搔,而工廠中的流水線是共享工廠的資源的,即進程相當(dāng)于一個工廠蜻牢,線程相當(dāng)于工廠中的一條流水線
  • 一個進程崩潰后僻造,在保護模式下不會對其他進程產(chǎn)生影響,但是一個線程崩潰整個進程都死掉孩饼,所以多進程要比多線程健壯
  • 進程切換時髓削,消耗的資源大、效率高.所以設(shè)計到頻繁的切換時镀娶,使用線程要好于進程.同樣如果要求同時進行并且又要共享某些變量的并發(fā)操作立膛,只能用線程而不能用進程
  • 執(zhí)行過程:每個獨立的進程有一個程序運行的入口、順序執(zhí)行序列和程序入口.但是線程不能獨立執(zhí)行梯码,必須依存在應(yīng)用程序中宝泵,由應(yīng)用程序提供多個線程執(zhí)行控制
  • 線程是處理器調(diào)度的基本單位,但進程不是
  • 線程沒有地址空間,線程包含在進程地址空間中

可能會覺得這些理論知識很抽象轩娶,百度出來一大堆但是都不好理解儿奶,看完下面的理解就全明白了

①.4 進程與線程的關(guān)系圖

可以把iOS系統(tǒng)想象成商場進程則是商場中的店鋪鳄抒,線程是店鋪雇傭的員工

  • 進程之間的相互獨立
    • 奶茶店看不到果汁店的賬目(訪問不了別的進程的內(nèi)存)
    • 果汁店用不了奶茶店的波霸(進程之間的資源是獨立的)
  • 進程至少要有一條線程
    • 店鋪至少要有一個員工(進程至少有一個線程)
    • 早上開店門的員工(相當(dāng)于主線程)
  • 進程/線程崩潰的情況
    • 奶茶店倒閉了并不會牽連果汁店倒閉(進程崩潰不會對其他進程產(chǎn)生影響)
    • 奶茶店的收銀員不干了會導(dǎo)致奶茶店無法正常運作(線程崩潰導(dǎo)致進程癱瘓)

移動開發(fā)不一定是單進程處理的闯捎,android就是多進程處理的;而iOS采用沙盒機制许溅,這也是蘋果運行能夠流暢安全的一個主要原因

①.5 線程和runloop的關(guān)系

  • runloop與線程是一一對應(yīng)的 —— 一個runloop對應(yīng)一個核心的線程瓤鼻,為什么說是核心的,是因為runloop是可以嵌套的贤重,但是核心的只能有一個茬祷,他們的關(guān)系保存在一個全局的字典里
  • runloop是來管理線程的 —— 當(dāng)線程的runloop被開啟后,線程會在執(zhí)行完任務(wù)后進入休眠狀態(tài)并蝗,有了任務(wù)就會被喚醒去執(zhí)行任務(wù)
  • runloop在第一次獲取時被創(chuàng)建祭犯,在線程結(jié)束時被銷毀
    • 對于主線程來說秸妥,runloop在程序一啟動就默認創(chuàng)建好了
    • 對于子線程來說,runloop是懶加載的 —— 只有當(dāng)我們使用的時候才會創(chuàng)建沃粗,所以在子線程用定時器要注意:確保子線程的runloop被創(chuàng)建粥惧,不然定時器不會回調(diào)

①.6 影響任務(wù)執(zhí)行速度的因素

以下因素都會對任務(wù)的執(zhí)行速度造成影響:

  • cpu的調(diào)度
  • 線程的執(zhí)行速率
  • 隊列情況
  • 任務(wù)執(zhí)行的復(fù)雜度
  • 任務(wù)的優(yōu)先級

② 多線程

②.1 多線程原理

  • 對于單核CPU,同一時間陪每,CPU只能處理一條線程影晓,即只有一條線程在工作(執(zhí)行)
  • iOS中的多線程同時執(zhí)行的本質(zhì)CPU在多個任務(wù)之間進行快速的切換镰吵,由于CPU調(diào)度線程的時間足夠快檩禾,就造成了多線程的“同時”執(zhí)行的效果.其中切換的時間間隔就是時間片

②.2 多線程意義

優(yōu)點

  • 能適當(dāng)提高程序的執(zhí)行效率
  • 能適當(dāng)提高資源的利用率(CPU、內(nèi)存)
  • 線程上的任務(wù)執(zhí)行完成后疤祭,線程會自動銷毀

缺點

  • 開啟線程需要占用一定的內(nèi)存空間(默認情況下盼产,每一個線程都占512KB,創(chuàng)建線程大約需要90毫秒的創(chuàng)建時間)
  • 如果開啟大量的線程勺馆,會占用大量的內(nèi)存空間戏售,降低程序的性能
  • 線程越多,CPU在調(diào)用線程上的開銷就越大
  • 程序設(shè)計更加復(fù)雜草穆,比如線程間的通信灌灾、多線程的數(shù)據(jù)共享

②.3 多線程生命周期

多線程的生命周期主要分為5部分:新建 - 就緒 - 運行 - 阻塞 - 死亡,如下圖所示

  • 新建:主要是實例化線程對象
  • 就緒:線程對象調(diào)用start方法悲柱,將線程對象加入可調(diào)度線程池锋喜,等待CPU的調(diào)用,即調(diào)用start方法豌鸡,并不會立即執(zhí)行嘿般,進入就緒狀態(tài),需要等待一段時間涯冠,經(jīng)CPU調(diào)度后才執(zhí)行炉奴,也就是從就緒狀態(tài)進入運行狀態(tài)
  • 運行CPU 負責(zé)調(diào)度可調(diào)度線程池中線程的執(zhí)行.在線程執(zhí)行完成之前,其狀態(tài)可能會在就緒和運行之間來回切換.就緒和運行之間的狀態(tài)變化由CPU負責(zé)蛇更,程序員不能干預(yù).
  • 阻塞:當(dāng)滿足某個預(yù)定條件時瞻赶,可以使用休眠或鎖,阻塞線程執(zhí)行.sleepForTimeInterval(休眠指定時長)派任,sleepUntilDate(休眠到指定日期)共耍,@synchronized(self):(互斥鎖)
  • 死亡:分為兩種情況:正常死亡,即線程執(zhí)行完畢. 非正常死亡吨瞎,即當(dāng)滿足某個條件后痹兜,在線程內(nèi)部(或者主線程中)終止執(zhí)行(調(diào)用exit方法等退出)

簡要說明,就是處于運行中的線程擁有一段可以執(zhí)行的時間(稱為時間片

  • 如果時間片用盡颤诀,線程就會進入就緒狀態(tài)隊列
  • 如果時間片沒有用盡字旭,且需要開始等待某事件对湃,就會進入阻塞狀態(tài)隊列
  • 等待事件發(fā)生后,線程又會重新進入就緒狀態(tài)隊列
  • 每當(dāng)一個線程離開運行遗淳,即執(zhí)行完畢或者強制退出后拍柒,會重新從就緒狀態(tài)隊列選擇一個線程繼續(xù)執(zhí)行

線程的exitcancel說明

  • exit:一旦強行終止線程,后續(xù)的所有代碼都不會執(zhí)行
  • cancel:取消當(dāng)前線程屈暗,但是不能取消正在執(zhí)行的線程

那么是不是線程的優(yōu)先級越高拆讯,意味著任務(wù)的執(zhí)行越快?
并不是养叛,線程執(zhí)行的快慢种呐,除了要看優(yōu)先級,還需要查看資源的大小(即任務(wù)的復(fù)雜度)弃甥、以及 CPU 調(diào)度情況.在NSThread中爽室,線程優(yōu)先級threadPriority已經(jīng)被服務(wù)質(zhì)量qualityOfService取代,以下是相關(guān)的枚舉值

②.4 線程池的原理

  • 【第一步】判斷核心線程池是否都正在執(zhí)行任務(wù)

    • 返回NO淆攻,創(chuàng)建新的工作線程去執(zhí)行
    • 返回YES阔墩,進入【第二步】
  • 【第二步】判斷線程池工作隊列是否已經(jīng)飽滿

    • 返回NO,將任務(wù)存儲到工作隊列瓶珊,等待CPU調(diào)度
    • 返回YES啸箫,進入【第三步】
  • 【第三步】判斷線程池中的線程是否都處于執(zhí)行狀態(tài)

    • 返回NO,安排可調(diào)度線程池中空閑的線程去執(zhí)行任務(wù)
    • 返回YES伞芹,進入【第四步】
  • 【第四步】交給飽和策略去執(zhí)行忘苛,主要有以下四種(在iOS中并沒有找到以下4種策略)

    • AbortPolicy:直接拋出RejectedExecutionExeception異常來阻止系統(tǒng)正常運行
    • CallerRunsPolicy:將任務(wù)回退到調(diào)用者
    • DisOldestPolicy:丟掉等待最久的任務(wù)
    • DisCardPolicy:直接丟棄任務(wù)

②.5 iOS中多線程的實現(xiàn)方案

iOS中的多線程實現(xiàn)方式,主要有四種:pthread丑瞧、NSThread柑土、GCD、NSOperation绊汹,匯總?cè)鐖D所示

下面是以上四種方案的簡單示例

C和OC的橋接
其中涉及C與OC的橋接稽屏,有以下幾點說明

  • __bridge只做類型轉(zhuǎn)換,但是不修改對象(內(nèi)存)管理權(quán)
  • __bridge_retained(也可以使用CFBridgingRetain)將Objective-C的對象轉(zhuǎn)換為 Core Foundation的對象西乖,同時將對象(內(nèi)存)的管理權(quán)交給我們狐榔,后續(xù)需要使用 CFRelease或者相關(guān)方法來釋放對象
  • __bridge_transfer(也可以使用CFBridgingRelease)將Core Foundation的對象轉(zhuǎn)換為Objective-C的對象,同時將對象(內(nèi)存)的管理權(quán)交給ARC

②.6 線程安全問題

當(dāng)多個線程同時訪問一塊資源時获雕,容易引發(fā)數(shù)據(jù)錯亂和數(shù)據(jù)安全問題薄腻,有以下兩種解決方案

  • 互斥鎖(即同步鎖):@synchronized
  • 自旋鎖
②.6.1 互斥鎖 vs 自旋鎖

互斥鎖

  • 保證鎖內(nèi)的代碼,同一時間届案,只有一條線程能夠執(zhí)行!
  • 互斥鎖的鎖定范圍庵楷,應(yīng)該盡量小,鎖定范圍越大,效率越差!
  • 加了互斥鎖的代碼尽纽,當(dāng)新線程訪問時咐蚯,如果發(fā)現(xiàn)其他線程正在執(zhí)行鎖定的代碼,新線程就會進入休眠
  • 能夠加鎖的任意 NSObject 對象
  • 注意:鎖對象一定要保證所有的線程都能夠訪問
  • 如果代碼中只有一個地方需要加鎖弄贿,大多都使用 self春锋,這樣可以避免單獨再創(chuàng)建一個鎖對象

自旋鎖

  • 自旋鎖與互斥鎖類似,但它不是通過休眠使線程阻塞差凹,而是在獲取鎖之前一直處于忙等(即原地打轉(zhuǎn)期奔,稱為自旋)阻塞狀態(tài)
  • 使用場景:鎖持有的時間短,且線程不希望在重新調(diào)度上花太多成本時危尿,就需要使用自旋鎖呐萌,屬性修飾符atomic,本身就有一把自旋鎖
  • 加入了自旋鎖脚线,當(dāng)新線程訪問代碼時搁胆,如果發(fā)現(xiàn)有其他線程正在鎖定代碼弥搞,新線程會用死循環(huán)的方法邮绿,一直等待鎖定的代碼執(zhí)行完成,即不停的嘗試執(zhí)行代碼攀例,比較消耗性能

考考你: 自旋鎖vs互斥鎖的區(qū)別?

  • 相同點:在同一時間船逮,保證了只有一條線程執(zhí)行任務(wù),即保證了相應(yīng)同步的功能

  • 不同點:

    • 互斥鎖:發(fā)現(xiàn)其他線程執(zhí)行粤铭,當(dāng)前線程 休眠(即就緒狀態(tài))挖胃,進入等待執(zhí)行,即掛起.一直等其他線程打開之后梆惯,然后喚醒執(zhí)行
    • 自旋鎖:發(fā)現(xiàn)其他線程執(zhí)行酱鸭,當(dāng)前線程 忙等(即一直訪問),處于忙等狀態(tài)垛吗,耗費的性能比較高
  • 使用場景:根據(jù)任務(wù)復(fù)雜度區(qū)分凹髓,使用不同的鎖

    • 當(dāng)前的任務(wù)狀態(tài)比較短小精悍時,用自旋鎖
    • 反之的怯屉,用互斥鎖
②.6.2 atomic與nonatomic 的區(qū)別

atomicnonatomic主要用于屬性的修飾蔚舀,以下是相關(guān)的一些說明

  • nonatomic 非原子屬性
  • atomic 原子屬性(線程安全),針對多線程設(shè)計的锨络,默認值
    • 保證同一時間只有一個線程能夠?qū)懭?但是同一個時間多個線程都可以取值)
    • atomic 本身就有一把鎖(自旋鎖)
    • 單寫多讀:單個線程寫入赌躺,多個線程可以讀取
  • atomic:線程安全,需要消耗大量的資源
  • nonatomic:非線程安全羡儿,適合內(nèi)存小的移動設(shè)備

iOS 開發(fā)的建議

  • 所有屬性都聲明為 nonatomic
  • 盡量避免多線程搶奪同一塊資源 盡量將加鎖礼患、資源搶奪的業(yè)務(wù)邏輯交給服務(wù)器端處理,減小移動客戶端的壓力

②.7 線程間通訊

Threading Programming Guide文檔中,提及缅叠,線程間的通訊有以下幾種方式
[圖片上傳失敗...(image-a8fb76-1614613062037)]

  • 直接消息傳遞: 通過performSelector的一系列方法咏瑟,可以實現(xiàn)由某一線程指定在另外的線程上執(zhí)行任務(wù).因為任務(wù)的執(zhí)行上下文是目標(biāo)線程,這種方式發(fā)送的消息將會自動的被序列化
  • 全局變量痪署、共享內(nèi)存塊和對象: 在兩個線程之間傳遞信息的另一種簡單方法是使用全局變量码泞,共享對象或共享內(nèi)存塊.盡管共享變量既快速又簡單,但是它們比直接消息傳遞更脆弱.必須使用鎖或其他同步機制仔細保護共享變量狼犯,以確保代碼的正確性. 否則可能會導(dǎo)致競爭狀況余寥,數(shù)據(jù)損壞或崩潰。
  • 條件執(zhí)行: 條件是一種同步工具悯森,可用于控制線程何時執(zhí)行代碼的特定部分.您可以將條件視為關(guān)守欣喧,讓線程僅在滿足指定條件時運行.
  • Runloop sources: 一個自定義的 Runloop source 配置可以讓一個線程上收到特定的應(yīng)用程序消息.由于 Runloop source 是事件驅(qū)動的谬运,因此在無事可做時,線程會自動進入睡眠狀態(tài),從而提高了線程的效率
  • Ports and sockets:基于端口的通信是在兩個線程之間進行通信的一種更為復(fù)雜的方法遥赚,但它也是一種非常可靠的技術(shù).更重要的是狂秦,端口和套接字可用于與外部實體(例如其他進程和服務(wù))進行通信.為了提高效率关带,使用 Runloop source 來實現(xiàn)端口,因此當(dāng)端口上沒有數(shù)據(jù)等待時褥傍,線程將進入睡眠狀態(tài).需要注意的是儡嘶,端口通訊需要將端口加入到主線程的Runloop中,否則不會走到端口回調(diào)方法
  • 消息隊列: 傳統(tǒng)的多處理服務(wù)定義了先進先出(FIFO)隊列抽象恍风,用于管理傳入和傳出數(shù)據(jù).盡管消息隊列既簡單又方便蹦狂,但是它們不如其他一些通信技術(shù)高效
  • Cocoa 分布式對象: 分布式對象是一種 Cocoa 技術(shù),可提供基于端口的通信的高級實.盡管可以將這種技術(shù)用于線程間通信朋贬,但是強烈建議不要這樣做凯楔,因為它會產(chǎn)生大量開銷.分布式對象更適合與其他進程進行通信,盡管在這些進程之間進行事務(wù)的開銷也很高.

②.8 GCD和NSOperation的比較

  • GCDNSOperation的關(guān)系如下:

    • GCD是面向底層的C語言的API
    • NSOperation是用GCD封裝構(gòu)建的锦募,是GCD的高級抽象
  • GCD和NSOperation的對比如下:

    • GCD執(zhí)行效率更高摆屯,而且由于隊列中執(zhí)行的是由block構(gòu)成的任務(wù),這是一個輕量級的數(shù)據(jù)結(jié)構(gòu) —— 寫起來更加方便
    • GCD只支持FIFO的隊列御滩,而NSOpration可以設(shè)置最大并發(fā)數(shù)鸥拧、設(shè)置優(yōu)先級、添加依賴關(guān)系等調(diào)整執(zhí)行順序
    • NSOpration甚至可以跨隊列設(shè)置依賴關(guān)系削解,但是GCD只能通過設(shè)置串行隊列富弦,或者在隊列內(nèi)添加barrier任務(wù)才能控制執(zhí)行順序,較為復(fù)雜
    • NSOperation支持KVO(面向?qū)ο螅┛梢詸z測operation是否正在執(zhí)行氛驮、是否結(jié)束腕柜、是否取消

二、NSthread

NSthread是蘋果官方提供面向?qū)ο蟮木€程操作技術(shù),是對thread的上層封裝盏缤,比較偏向于底層.簡單方便砰蠢,可以直接操作線程對象,使用頻率較少.

① 創(chuàng)建線程

線程的創(chuàng)建方式主要有以下三種方式

  • 通過init初始化方式創(chuàng)建
  • 通過detachNewThreadSelector構(gòu)造器方式創(chuàng)建
  • 通過performSelector...方法創(chuàng)建唉铜,主要是用于獲取主線程台舱,以及后臺線程

② 屬性

③ 類方法

常用的類方法有以下幾個

  • currentThread:獲取當(dāng)前線程
  • sleep...:阻塞線程
  • exit:退出線程
  • mainThread:獲取主線程

三、GCD

① GCD簡介

什么是GCD?
GCD全稱是Grand Central Dispatch潭流,它是純 C 語言竞惋,并且提供了非常多強大的函數(shù)

GCD的優(yōu)勢:

  • GCD 是蘋果公司為多核的并行運算提出的解決方案
  • GCD自動利用更多的CPU內(nèi)核(比如雙核、四核)
  • GCD自動管理線程的生命周期(創(chuàng)建線程灰嫉、調(diào)度任務(wù)拆宛、銷毀線程)
  • 程序員只需要告訴 GCD 想要執(zhí)行什么任務(wù),不需要編寫任何線程管理代碼

用一句話總結(jié)GCD就是:將任務(wù)添加到隊列讼撒,并且指定執(zhí)行任務(wù)的函數(shù)

② GCD核心

在日常開發(fā)中浑厚,GCD一般寫成下面這種形式

將上述代碼拆分,方便我們來理解GCD的核心,主要是由 任務(wù) + 隊列 + 函數(shù) 構(gòu)成

  • 使用dispatch_block_t創(chuàng)建任務(wù)
  • 使用dispatch_queue_t創(chuàng)建隊列
  • 將任務(wù)添加到隊列根盒,并指定執(zhí)行任務(wù)的函數(shù)dispatch_async

注意
這里的任務(wù)是指執(zhí)行操作的意思钳幅,在使用dispatch_block_t創(chuàng)建任務(wù)時,主要有以下兩點說明

  • 任務(wù)使用block封裝
  • 任務(wù)的block沒有參數(shù)也沒有返回值

③ 函數(shù)與隊列

③.1 函數(shù)

GCD中執(zhí)行任務(wù)的方式有兩種郑象,同步執(zhí)行異步執(zhí)行贡这,分別對應(yīng)同步函數(shù)dispatch_sync異步函數(shù)dispatch_async,兩者對比如下

  • 同步執(zhí)行茬末,對應(yīng)同步函數(shù)dispatch_sync
    • 必須等待當(dāng)前語句執(zhí)行完畢厂榛,才會執(zhí)行下一條語句
    • 不會開啟線程,即不具備開啟新線程的能力
    • 在當(dāng)前線程中執(zhí)行block任務(wù)
  • 異步執(zhí)行丽惭,對應(yīng)異步函數(shù)dispatch_async
    • 不用等待當(dāng)前語句執(zhí)行完畢击奶,就可以執(zhí)行下一條語句
    • 會開啟線程執(zhí)行block任務(wù),即具備開啟新線程的能力(但并不一定開啟新線程责掏,這個與任務(wù)所指定的隊列類型有關(guān))
    • 異步是多線程的代名詞

綜上所述柜砾,兩種執(zhí)行方式的主要區(qū)別有兩點:

  • 是否等待隊列的任務(wù)執(zhí)行完畢
  • 是否具備開啟新線程的能力

③.2 隊列

多線程中所說的隊列Dispatch Queue)是指執(zhí)行任務(wù)的等待隊列,即用來存放任務(wù)的隊列.隊列是一種特殊的線性表换衬,遵循先進先出(FIFO)原則痰驱,即新任務(wù)總是被插入到隊尾,而任務(wù)的讀取從隊首開始讀取.每讀取一個任務(wù)瞳浦,則動隊列中釋放一個任務(wù)担映,如下圖所示

③.2.1 串行隊列 和 并發(fā)隊列

GCD中,隊列主要分為串行隊列(Serial Dispatch Queue)并發(fā)隊列(Concurrent Dispatch Queue)兩種叫潦,如下圖所示

  • 串行隊列:每次只有一個任務(wù)被執(zhí)行蝇完,等待上一個任務(wù)執(zhí)行完畢再執(zhí)行下一個,即只開啟一個線程(通俗理解:同一時刻只調(diào)度一個任務(wù)執(zhí)行)
    • 使用dispatch_queue_create("xxx", DISPATCH_QUEUE_SERIAL);創(chuàng)建串行隊列
    • 其中的DISPATCH_QUEUE_SERIAL也可以使用NULL表示,這兩種均表示 默認的串行隊列
  • 并發(fā)隊列:一次可以并發(fā)執(zhí)行多個任務(wù)短蜕,即開啟多個線程氢架,并同時執(zhí)行任務(wù)(通俗理解:同一時刻可以調(diào)度多個任務(wù)執(zhí)行)
    • 使用dispatch_queue_create("xxx", DISPATCH_QUEUE_CONCURRENT);創(chuàng)建并發(fā)隊列
    • 注意:并發(fā)隊列的并發(fā)功能只有在異步函數(shù)下才有效
③.2.2 主隊列 和 全局并發(fā)隊列

GCD中,針對上述兩種隊列朋魔,分別提供了主隊列(Main Dispatch Queue)全局并發(fā)隊列(Global Dispatch Queue)

  • 主隊列Main Dispatch Queue):GCD中提供的特殊的串行隊列
    • 專門用來在主線程上調(diào)度任務(wù)的串行隊列岖研,依賴于主線程主Runloop警检,在main函數(shù)調(diào)用之前自動創(chuàng)建
    • 不會開啟線程
    • 如果當(dāng)前主線程正在有任務(wù)執(zhí)行缎玫,那么無論主隊列中當(dāng)前被添加了什么任務(wù),都不會被調(diào)度
    • 使用dispatch_get_main_queue()獲得主隊列
    • 通常在返回主線程更新UI時使用
  • 全局并發(fā)隊列Global Dispatch Queue):GCD提供的默認的并發(fā)隊列
  • 為了方便程序員的使用解滓,蘋果提供了全局隊列
  • 在使用多線程開發(fā)時赃磨,如果對隊列沒有特殊需求,在執(zhí)行異步任務(wù)時洼裤,可以直接使用全局隊列
  • 使用dispatch_get_global_queue獲取全局并發(fā)隊列邻辉,最簡單的是dispatch_get_global_queue(0, 0)
    • 第一個參數(shù)表示隊列優(yōu)先級,默認優(yōu)先級為DISPATCH_QUEUE_PRIORITY_DEFAULT=0腮鞍,在ios9之后值骇,已經(jīng)被服務(wù)質(zhì)量(quality-of-service)取代
    • 第二個參數(shù)使用0
③.2.3 全局并發(fā)隊列 + 主隊列 配合使用

在日常開發(fā)中,全局隊列+并發(fā)并列一般是這樣配合使用的

③.3 函數(shù)與隊列的不同組合

主隊列和全局隊列單獨考慮移国,組合結(jié)果以總結(jié)表格為準(zhǔn)

③.3.1 串行隊列 + 同步函數(shù)

任務(wù)一個接一個的在當(dāng)前線程執(zhí)行吱瘩,不會開辟新線程

③.3.2 串行隊列 + 異步函數(shù)

任務(wù)一個接一個的執(zhí)行,會開辟新線程

③.3.3 并發(fā)隊列 + 同步函數(shù)

任務(wù)一個接一個的執(zhí)行迹缀,不開辟線程
③.3.4 并發(fā)隊列 + 異步函數(shù)

任務(wù)亂序執(zhí)行使碾,會開辟新線程

③.3.5 主隊列 + 同步函數(shù)

任務(wù)相互等待造成死鎖

造成死鎖的原因分析如下:

  • 主隊列有兩個任務(wù)祝懂,順序為:CJNSLog任務(wù) - 同步block
  • 執(zhí)行CJNSLog任務(wù)后票摇,執(zhí)行同步Block,會將任務(wù)1(即i=1時)加入到主隊列砚蓬,主隊列順序為:CJNSLog任務(wù) - 同步block - 任務(wù)1
  • 任務(wù)1的執(zhí)行需要等待同步block執(zhí)行完畢才會執(zhí)行矢门,而同步block的執(zhí)行需要等待任務(wù)1執(zhí)行完畢,所以就造成了任務(wù)互相等待的情況灰蛙,即造成死鎖崩潰

死鎖現(xiàn)象

  • 主線程因為你同步函數(shù)的原因等著先執(zhí)行任務(wù)
  • 主隊列等著主線程的任務(wù)執(zhí)行完畢再執(zhí)行自己的任務(wù)
  • 主隊列和主線程相互等待會造成死鎖
③.3.6 主隊列 + 異步函數(shù)

任務(wù)一個接一個的執(zhí)行祟剔,不開辟線程

③.3.7 全局并發(fā)隊列 + 同步函數(shù)

任務(wù)一個接一個的執(zhí)行,不開辟新線程

③.3.8 全局并發(fā)隊列 + 異步函數(shù)

任務(wù)亂序執(zhí)行摩梧,會開辟新線程

③.3.9 總結(jié)
函數(shù)與隊列 串行隊列 并發(fā)隊列 主隊列 全局并發(fā)隊列
同步函數(shù) 順序執(zhí)行,不開辟線程 順序執(zhí)行,不開辟線程 死鎖 順序執(zhí)行,不開辟線程
異步函數(shù) 順序執(zhí)行,開辟線程 亂序執(zhí)行,開辟線程 順序執(zhí)行,不開辟線程 亂序執(zhí)行,開辟線程

④ dispatch_after

⑤ dispatch_once

⑥ dispatch_apply

⑦ dispatch_group_t

dispatch_group_t:調(diào)度組將任務(wù)分組執(zhí)行物延,能監(jiān)聽任務(wù)組完成,并設(shè)置等待時間
應(yīng)用場景:多個接口請求之后刷新頁面
有以下兩種使用方式

⑦.1 使用dispatch_group_async + dispatch_group_notify

dispatch_group_notifydispatch_group_async執(zhí)行結(jié)束之后會受收到通知

⑦.2 使用dispatch_group_enter + dispatch_group_leave + dispatch_group_notify

dispatch_group_enterdispatch_group_leave成對出現(xiàn)障本,使進出組的邏輯更加清晰

調(diào)度組要注意搭配使用教届,必須先進組再出組响鹃,缺一不可

⑦.3 在⑦.2 的基礎(chǔ)上使用 dispatch_group_wait

⑧ dispatch_barrier_sync & dispatch_barrier_async

柵欄函數(shù),主要有兩種使用場景:串行隊列案训、并發(fā)隊列.
應(yīng)用場景:同步鎖

等柵欄前追加到隊列中的任務(wù)執(zhí)行完畢后买置,再將柵欄后的任務(wù)追加到隊列中.
簡而言之,就是先執(zhí)行柵欄前任務(wù)强霎,再執(zhí)行柵欄任務(wù)忿项,最后執(zhí)行柵欄后任務(wù).

⑧.1 串行隊列使用柵欄函數(shù)

不使用柵欄函數(shù)


使用柵欄函數(shù)


柵欄函數(shù)的作用是將隊列中的任務(wù)進行分組,所以我們只要關(guān)注任務(wù)1城舞、任務(wù)2

結(jié)論:由于串行隊列異步執(zhí)行任務(wù)是一個接一個執(zhí)行完畢的轩触,所以使用柵欄函數(shù)沒意義

⑧.2 并發(fā)隊列使用柵欄函數(shù)

不使用柵欄函數(shù)


使用柵欄函數(shù)


結(jié)論:由于并發(fā)隊列異步執(zhí)行任務(wù)是亂序執(zhí)行完畢的,所以使用柵欄函數(shù)可以很好的控制隊列內(nèi)任務(wù)執(zhí)行的順序

⑧.3 dispatch_barrier_sync/dispatch_barrier_async區(qū)別
  • dispatch_barrier_async:前面的任務(wù)執(zhí)行完畢才會來到這里
  • dispatch_barrier_sync:作用相同家夺,但是這個會堵塞線程脱柱,影響后面的任務(wù)執(zhí)行

將案例二中的dispatch_barrier_async改成dispatch_barrier_sync

結(jié)論:dispatch_barrier_async可以控制隊列中任務(wù)的執(zhí)行順序,而dispatch_barrier_sync不僅阻塞了隊列的執(zhí)行拉馋,也阻塞了線程的執(zhí)行(盡量少用)

⑧.4 柵欄函數(shù)注意點
  • 1.盡量使用自定義的并發(fā)隊列
    • 使用全局隊列起不到柵欄函數(shù)的作用
    • 使用全局隊列時由于對全局隊列造成堵塞榨为,可能致使系統(tǒng)其他調(diào)用全局隊列的地方也堵塞從而導(dǎo)致崩潰(并不是只有你在使用這個隊列)
  • 2.柵欄函數(shù)只能控制同一并發(fā)隊列:打個比方,平時在使用AFNetworking做網(wǎng)絡(luò)請求時為什么不能用柵欄函數(shù)起到同步鎖堵塞的效果煌茴,因為AFNetworking內(nèi)部有自己的隊列

⑨ dispatch_semaphore_t

信號量主要用作同步鎖随闺,用于控制GCD最大并發(fā)數(shù)

  • dispatch_semaphore_create():創(chuàng)建信號量
  • dispatch_semaphore_wait():等待信號量,信號量減1.當(dāng)信號量< 0時會阻塞當(dāng)前線程蔓腐,根據(jù)傳入的等待時間決定接下來的操作——如果永久等待將等到信號(signal)才執(zhí)行下去
  • dispatch_semaphore_signal():釋放信號量矩乐,信號量加1.當(dāng)信號量>= 0 會執(zhí)行wait之后的代碼.

下面這段代碼要求使用信號量來按序輸出(當(dāng)然柵欄函數(shù)可以滿足要求)


利用信號量的API來進行代碼改寫

如果當(dāng)創(chuàng)建信號量時傳入值為1又會怎么樣呢?

  • i=0時有可能先打印回论,也可能會先發(fā)出wait信號量-1散罕,但是wait之后信號量為0不會阻塞線程,所以進入i=1
  • i=1時有可能先打印透葛,也可能會先發(fā)出wait信號量-1笨使,但是wait之后信號量為-1阻塞線程,等待signal再執(zhí)行下去

結(jié)論:

  • 創(chuàng)建信號量時傳入值為1時僚害,可以通過兩次才堵塞
  • 傳入值為2時,可以通過三次才堵塞

⑩ dispatch_source

dispatch_source_t主要用于計時操作繁调,其原因是因為它創(chuàng)建的timer不依賴于RunLoop萨蚕,且計時精準(zhǔn)度比NSTimer

⑩.1 定義及使用

dispatch_source是一種基本的數(shù)據(jù)類型,可以用來監(jiān)聽一些底層的系統(tǒng)事件

  • Timer Dispatch Source:定時器事件源蹄胰,用來生成周期性的通知或回調(diào)
  • Signal Dispatch Source:監(jiān)聽信號事件源岳遥,當(dāng)有UNIX信號發(fā)生時會通知
  • Descriptor Dispatch Source:監(jiān)聽文件或socket事件源,當(dāng)文件或socket數(shù)據(jù)發(fā)生變化時會通知
  • Process Dispatch Source:監(jiān)聽進程事件源裕寨,與進程相關(guān)的事件通知
  • Mach port Dispatch Source:監(jiān)聽Mach端口事件源
  • Custom Dispatch Source:監(jiān)聽自定義事件源

主要使用的API:

  • dispatch_source_create: 創(chuàng)建事件源
  • dispatch_source_set_event_handler: 設(shè)置數(shù)據(jù)源回調(diào)
  • dispatch_source_merge_data: 設(shè)置事件源數(shù)據(jù)
  • dispatch_source_get_data: 獲取事件源數(shù)據(jù)
  • dispatch_resume: 繼續(xù)
  • dispatch_suspend: 掛起
  • dispatch_cancle: 取消
⑩.2 自定義定時器

在iOS開發(fā)中一般使用NSTimer來處理定時邏輯浩蓉,但NSTimer是依賴Runloop的派继,而Runloop可以運行在不同的模式下.如果NSTimer添加在一種模式下,當(dāng)Runloop運行在其他模式下的時候捻艳,定時器就掛機了驾窟;又如果Runloop在阻塞狀態(tài),NSTimer觸發(fā)時間就會推遲到下一個Runloop周.认轨。因此NSTimer在計時上會有誤差绅络,并不是特別精確,而GCD定時器不依賴Runloop嘁字,計時精度要高很多

使用dispatch_source自定義定時器注意點:

  • GCDTimer需要強持有恩急,否則出了作用域立即釋放,也就沒有了事件回調(diào)
  • GCDTimer默認是掛起狀態(tài)纪蜒,需要手動激活
  • GCDTimer沒有repeat衷恭,需要封裝來增加標(biāo)志位控制
  • GCDTimer如果存在循環(huán)引用,使用weak+strong或者提前調(diào)用dispatch_source_cancel取消timer
  • dispatch_resumedispatch_suspend調(diào)用次數(shù)需要平衡
  • source掛起狀態(tài)下纯续,如果直接設(shè)置source = nil或者重新創(chuàng)建source都會造成crash.正確的方式是在激活狀態(tài)下調(diào)用dispatch_source_cancel(source)釋放當(dāng)前的source

四匾荆、NSOperation

NSOperation是個抽象類,依賴于子類NSInvocationOperation杆烁、NSBlockOperation去實現(xiàn)

下面是開發(fā)者文檔上對NSOperation的一段描述

① NSInvocationOperation

  • 基本使用


  • 直接處理事務(wù)牙丽,不添加隱性隊列


  • 接下來就會引申出下面一段錯誤使用代碼


上述代碼之所以會崩潰厚掷,是因為線程生命周期:

  • queue addOperation:op已經(jīng)將處理事務(wù)的操作任務(wù)加入到隊列中续誉,并讓線程運行
  • op start將已經(jīng)運行的線程再次運行會造成線程混亂

② NSBlockOperation

NSInvocationOperationNSBlockOperation兩者的區(qū)別在于:

  • 前者類似target形式
  • 后者類似block形式——函數(shù)式編程,業(yè)務(wù)邏輯代碼可讀性更高

NSOperationQueue是異步執(zhí)行的锐墙,所以任務(wù)一析校、任務(wù)二的完成順序不確定

通過addExecutionBlock這個方法可以讓NSBlockOperation實現(xiàn)多線程

③ 自定義繼承自NSOperation的子類构罗,通過實現(xiàn)內(nèi)部相應(yīng)的方法來封裝任務(wù)

④ NSOperationQueue

NSOperationQueue有兩種隊列:主隊列、其他隊列.其他隊列包含了 串行和并發(fā).

  • 主隊列:主隊列上的任務(wù)是在主線程執(zhí)行的
  • 其他隊列(非主隊列):加入到非主隊列中的任務(wù)默認就是并發(fā)智玻,開啟多線程
    例如我們在 ② NSBlockOperation 中說的那樣.

⑤ 執(zhí)行順序

下列代碼可以證明操作與隊列的執(zhí)行效果是異步并發(fā)

⑥ 設(shè)置優(yōu)先級

NSOperation設(shè)置優(yōu)先級只會讓CPU有更高的幾率調(diào)用遂唧,不是說設(shè)置高就一定全部先完成

  • 不使用sleep——高優(yōu)先級的任務(wù)一先于低優(yōu)先級的任務(wù)二

  • 使用sleep進行延時——高優(yōu)先級的任務(wù)一慢于低優(yōu)先級的任務(wù)二

⑦ 設(shè)置并發(fā)數(shù)

  • GCD中只能使用信號量來設(shè)置并發(fā)數(shù)
  • NSOperation輕易就能設(shè)置并發(fā)數(shù)
    • 通過設(shè)置maxConcurrentOperationCount來控制單次出隊列去執(zhí)行的任務(wù)數(shù)

⑧ 添加依賴

NSOperation中添加依賴能很好的控制任務(wù)執(zhí)行的先后順序

⑨ 線程間通訊

  • GCD中使用異步進行網(wǎng)絡(luò)請求,然后回到主線程刷新UI
  • NSOperation中也有類似在線程間通訊的操作

⑩ 任務(wù)的掛起吊奢、繼續(xù)盖彭、取消

但是在使用中經(jīng)常會遇到一些匪夷所思的問題——明明已經(jīng)掛起了任務(wù),可還是繼續(xù)執(zhí)行了幾個任務(wù)才停止執(zhí)行

這幅圖是并發(fā)量為2的情況:

  • 掛起前:任務(wù)3页滚、任務(wù)4等待被調(diào)度
  • 掛起瞬間:任務(wù)3召边、任務(wù)4已經(jīng)被調(diào)度出隊列,準(zhǔn)備執(zhí)行裹驰,此時它們是無法掛起的
  • 掛起后:任務(wù)3隧熙、任務(wù)4被線程執(zhí)行,而原來的隊列被掛起不能被調(diào)度

五幻林、GCD底層分析

由于源碼的篇幅較大贞盯、邏輯分支音念、宏定義較多,使得源碼變得晦澀難懂躏敢,讓開發(fā)者們望而卻步.但如果帶著疑問闷愤、有目的性的去看源碼,就能減少難度父丰,忽略無關(guān)的代碼.首先提出我們要分析的幾個問題:

  • 隊列創(chuàng)建
  • 異步函數(shù)
  • 同步函數(shù)
  • 單例的原理
  • 柵欄函數(shù)的原理
  • 信號量的原理
  • 調(diào)度組的原理

① 源碼的出處

分析源碼首先得獲取到GCD源碼肝谭,之前已經(jīng)分析過objcmalloc蛾扇、dyld源碼攘烛,那么GCD內(nèi)容是在哪份源碼中呢?

這里分享一個小技巧镀首,由于已知要研究GCD坟漱,所以有以下幾種選擇源碼的方法

  • Baidu/Google
  • 下符號斷點dispatch_queue_createdispatch_async,打開匯編調(diào)式Debug->Debug Workflow->Always show Disassembly

這樣子就找到了我們需要的libdispatch-1271.40.12源碼

② 隊列創(chuàng)建

通過前面的學(xué)習(xí)我們知道隊列的創(chuàng)建是通過GCD中的dispatch_queue_create方法創(chuàng)建的,因此可以在源碼中搜索dispatch_queue_create.
假如我們就直接搜索dispatch_queue_create的話,會出現(xiàn)眾多的情況(66 results in 18 files),這時候就考驗一個開發(fā)者閱讀源碼的經(jīng)驗了

在此,我們就要改一改搜索條件了:

  • 由于創(chuàng)建隊列代碼為dispatch_queue_create("XXX", NULL)更哄,所以搜索dispatch_queue_create( —— 將篩選結(jié)果降至(21 results in 6 files)
  • 由于第一個參數(shù)為字符串芋齿,在c語言中用const修飾,所以搜索dispatch_queue_create(const —— 將篩選結(jié)果降至(2 results in 2 files)

②.1 dispatch_queue_create

常規(guī)中間層封裝 —— 便于代碼迭代不改變上層使用


有時候也需要注意下源碼中函數(shù)中的傳參:

  • 此時label是上層的逆序全程域名成翩,主要用在崩潰調(diào)試
  • attrNULL/DISPATCH_QUEUE_SERIAL觅捆、DISPATCH_QUEUE_CONCURRENT,用于區(qū)分隊列是異步還是同步的

#define DISPATCH_QUEUE_SERIAL NULL 串行隊列的宏定義其實是個NULL

②.2 _dispatch_lane_create_with_target

  • 1.通過_dispatch_queue_attr_to_info方法傳入dqa(即隊列類型麻敌,串行栅炒、并發(fā)等)創(chuàng)建dispatch_queue_attr_info_t類型的對象dqai,用于存儲隊列的相關(guān)屬性信息

    • dispatch_queue_attr_info_tisa一樣术羔,是個位域結(jié)構(gòu),用于存儲隊列的相關(guān)屬性信息
  • 2.設(shè)置隊列相關(guān)聯(lián)的屬性赢赊,例如服務(wù)質(zhì)量qos等
  • 3.通過DISPATCH_VTABLE拼接隊列名稱,即vtable级历,其中DISPATCH_VTABLE是宏定義释移,如下所示,所以隊列的類型是通過OS_dispatch_+隊列類型queue_concurrent拼接而成的
    • 串行隊列類型:OS_dispatch_queue_serial寥殖,驗證如下
* 并發(fā)隊列類型:`OS_dispatch_queue_concurrent`玩讳,驗證如下![](https://upload-images.jianshu.io/upload_images/2340353-ac444e89cd7608be.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
  • 4.通過alloc+init初始化隊列,即dq扛禽,其中在_dispatch_queue_init傳參中根據(jù)dqai.dqai_concurrent的布爾值锋边,就能判斷隊列是串行還是并發(fā),而 vtable表示隊列的類型编曼,說明隊列也是對象
    • 進入_dispatch_object_alloc -> _os_object_alloc_realized方法中設(shè)置了isa的指向,從這里可以驗證隊列也是對象的說法
* 進入`_dispatch_queue_init`方法,隊列類型是`dispatch_queue_t`,并設(shè)置隊列的相關(guān)屬性![](https://upload-images.jianshu.io/upload_images/2340353-bc9fbe87c7aea2e9.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
  • 5.通過_dispatch_trace_queue_create對創(chuàng)建的隊列進行處理剩辟,其中_dispatch_trace_queue_create_dispatch_introspection_queue_create封裝的宏定義掐场,最后會返回處理過的_dq
* 進入`_dispatch_introspection_queue_create_hook -> dispatch_introspection_queue_get_info -> _dispatch_introspection_lane_get_info`中可以看出往扔,與我們自定義的類還是有所區(qū)別的,`創(chuàng)建隊列`在底層的實現(xiàn)是`通過模板創(chuàng)建`的![](https://upload-images.jianshu.io/upload_images/2340353-8558202b2f11d069.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

②.3 總結(jié)

  • 隊列創(chuàng)建方法dispatch_queue_create中的參數(shù)二(即隊列類型)熊户,決定了下層中 max & 1(用于區(qū)分是 串行 還是 并發(fā))萍膛,其中1表示串行
  • queue 也是一個對象,也需要底層通過alloc + init 創(chuàng)建嚷堡,并且在alloc中也有一個class蝗罗,這個class是通過宏定義拼接而成,并且同時會指定isa的指向
  • 創(chuàng)建隊列在底層的處理是通過模板創(chuàng)建的蝌戒,其類型是dispatch_introspection_queue_s結(jié)構(gòu)體

dispatch_queue_create底層分析流程如下圖所示

③ 異步函數(shù)

③.1 dispatch_async

主要分析兩個函數(shù)

  • _dispatch_continuation_init:任務(wù)包裝函數(shù)
  • _dispatch_continuation_async:并發(fā)處理函數(shù)

③.2 _dispatch_continuation_init 任務(wù)包裝器

主要是包裝任務(wù)串塑,并設(shè)置線程的回程函數(shù),相當(dāng)于初始化

主要有以下幾步

  • 通過_dispatch_Block_copy拷貝任務(wù)
  • 通過_dispatch_Block_invoke封裝任務(wù)北苟,其中_dispatch_Block_invoke是個宏定義桩匪,根據(jù)以上分析得知是異步回調(diào)
  • 如果是同步的,則回調(diào)函數(shù)賦值為_dispatch_call_block_and_release
  • 通過_dispatch_continuation_init_f方法將回調(diào)函數(shù)賦值友鼻,即f就是func傻昙,將其保存在屬性中

③.3 _dispatch_continuation_async 并發(fā)處理

這個函數(shù)中,主要是執(zhí)行block回調(diào)

  • 其中的關(guān)鍵代碼是dx_push(dqu._dq, dc, qos)彩扔,dx_push是宏定義妆档,如下所示
  • 而其中的dq_push需要根據(jù)隊列的類型,執(zhí)行不同的函數(shù)

在此我們通過符號斷點調(diào)試執(zhí)行函數(shù)

  • 運行demo虫碉,通過符號斷點贾惦,來判斷執(zhí)行的是哪個函數(shù),由于是并發(fā)隊列蔗衡,通過增加_dispatch_lane_concurrent_push符號斷點纤虽,看看是否會走到這里
  • 運行發(fā)現(xiàn),走的確實是_dispatch_lane_concurrent_push
  • 進入_dispatch_lane_concurrent_push源碼绞惦,發(fā)現(xiàn)有兩步逼纸,繼續(xù)通過符號斷點_dispatch_continuation_redirect_push_dispatch_lane_push調(diào)試,發(fā)現(xiàn)走的是_dispatch_continuation_redirect_push
  • 進入_dispatch_continuation_redirect_push源碼济蝉,發(fā)現(xiàn)又走到了dx_push杰刽,即遞歸了,綜合前面隊列創(chuàng)建時可知王滤,隊列也是一個對象贺嫂,有父類、根類雁乡,所以會遞歸執(zhí)行到根類的方法
  • 接下來第喳,通過根類的_dispatch_root_queue_push符號斷點,來驗證猜想是否正確踱稍,從運行結(jié)果看出曲饱,完全是正確的
  • 進入_dispatch_root_queue_push -> _dispatch_root_queue_push_inline ->_dispatch_root_queue_poke -> _dispatch_root_queue_poke_slow源碼悠抹,經(jīng)過符號斷點驗證,確實是走的這里扩淀,查看該方法的源碼實現(xiàn)楔敌,主要有兩步操作
    • 通過_dispatch_root_queues_init方法注冊回調(diào)
    • 通過do-while循環(huán)創(chuàng)建線程,使用pthread_create方法

③.4 _dispatch_root_queues_init

  • 進入_dispatch_root_queues_init源碼實現(xiàn)驻谆,發(fā)現(xiàn)是一個dispatch_once_f單例(請查看后續(xù)單例的底層分析們卵凑,這里不作說明),其中傳入的func_dispatch_root_queues_init_once
  • 進入_dispatch_root_queues_init_once的源碼胜臊,其內(nèi)部不同事務(wù)的調(diào)用句柄都是_dispatch_worker_thread2

block回調(diào)執(zhí)行的調(diào)用路徑為:_dispatch_root_queues_init_once ->_dispatch_worker_thread2 -> _dispatch_root_queue_drain -> _dispatch_root_queue_drain -> _dispatch_continuation_pop_inline -> _dispatch_continuation_invoke_inline -> _dispatch_client_callout -> dispatch_call_block_and_release

這個路徑可以通過斷點勺卢,bt打印堆棧信息得出

在這里需要說明一點的是,單例block回調(diào)和異步函數(shù)block回調(diào)是不同的

  • 單例中区端,block回調(diào)中的func_dispatch_Block_invoke(block)
  • 而異步函數(shù)中值漫,block回調(diào)中的funcdispatch_call_block_and_release

④ 總結(jié)

綜上所述,異步函數(shù)的底層分析如下

  • 準(zhǔn)備工作: 首先织盼,將異步任務(wù)拷貝并封裝杨何,并設(shè)置回調(diào)函數(shù)func
  • block回調(diào):底層通過dx_push遞歸,會重定向到根隊列沥邻,然后通過pthread_creat創(chuàng)建線程危虱,最后通過dx_invoke執(zhí)行block回調(diào)(注意dx_pushdx_invoke 是成對的)
    異步函數(shù)的底層分析流程如圖所示

④ 同步函數(shù)

④.1 dispatch_sync

其底層的實現(xiàn)是通過柵欄函數(shù)實現(xiàn)的(柵欄函數(shù)的底層分析見后文)

④.2 _dispatch_sync_f

④.3 _dispatch_sync_f_inline

查看_dispatch_sync_f_inline源碼,其中width = 1表示是串行隊列唐全,其中有兩個重點:

  • 柵欄:_dispatch_barrier_sync_f(可以通過后文的柵欄函數(shù)底層分析解釋)埃跷,可以得出同步函數(shù)的底層實現(xiàn)其實是同步柵欄函數(shù)
  • 死鎖:_dispatch_sync_f_slow,如果存在相互等待的情況邮利,就會造成死鎖

④.4 _dispatch_sync_f_slow 死鎖

進入_dispatch_sync_f_slow弥雹,當(dāng)前的主隊列掛起、阻塞

  • 往一個隊列中加入任務(wù)延届,會push加入主隊列剪勿,進入_dispatch_trace_item_push
  • 進入__DISPATCH_WAIT_FOR_QUEUE__,判斷dq是否為正在等待的隊列方庭,然后給出一個狀態(tài)state厕吉,然后將dq的狀態(tài)和當(dāng)前任務(wù)依賴的隊列進行匹配
  • 進入_dq_state_drain_locked_by -> _dispatch_lock_is_locked_by源碼

如果當(dāng)前等待的和正在執(zhí)行的是同一個隊列,即判斷線程ID是否相等械念,如果相等头朱,則會造成死鎖

同步函數(shù) + 并發(fā)隊列 順序執(zhí)行的原因
_dispatch_sync_invoke_and_complete -> _dispatch_sync_function_invoke_inline源碼中,主要有三個步驟:

  • 將任務(wù)壓入隊列:_dispatch_thread_frame_push
  • 執(zhí)行任務(wù)的block回調(diào): _dispatch_client_callout
  • 將任務(wù)出隊:_dispatch_thread_frame_pop

從實現(xiàn)中可以看出龄减,是先將任務(wù)push隊列中项钮,然后執(zhí)行block回調(diào),在將任務(wù)pop,所以任務(wù)是順序執(zhí)行的

④.5 總結(jié)

同步函數(shù)的底層實現(xiàn)如下:

  • 同步函數(shù)的底層實現(xiàn)實際是同步柵欄函數(shù)
  • 同步函數(shù)中如果當(dāng)前正在執(zhí)行的隊列和等待的是同一個隊列寄纵,形成相互等待的局面鳖敷,則會造成死鎖

綜上所述脖苏,同步函數(shù)的底層實現(xiàn)流程如圖所示

⑤ dispatch_once 單例

在日常開發(fā)中程拭,我們一般使用GCDdispatch_once來創(chuàng)建單例,如下所示

首先對于單例棍潘,我們需要了解兩點

  • 執(zhí)行一次的原因: 單例的流程只執(zhí)行一次恃鞋,底層是如何控制的,即為什么只能執(zhí)行一次亦歉?
  • block調(diào)用時機: 單例的block是在什么時候進行調(diào)用的恤浪?

下面帶著以上兩點疑問,我們來針對單例的底層進行分析

⑤.1 dispatch_once

進入dispatch_once源碼實現(xiàn),底層是通過dispatch_once_f實現(xiàn)的

  • 參數(shù)1:onceToken肴楷,它是一個靜態(tài)變量水由,由于不同位置定義的靜態(tài)變量是不同的,所以靜態(tài)變量具有唯一性
  • 參數(shù)2:block回調(diào)

⑤.2 dispatch_once_f

進入dispatch_once_f源碼赛蔫,其中的val是外界傳入的onceToken靜態(tài)變量砂客,而func_dispatch_Block_invoke(block),其中單例的底層主要分為以下幾步

  • val呵恢,也就是靜態(tài)變量轉(zhuǎn)換為dispatch_once_gate_t類型的變量l
  • 通過os_atomic_load獲取此時的任務(wù)的標(biāo)識符v
    • 如果v等于DLOCK_ONCE_DONE鞠值,表示任務(wù)已經(jīng)執(zhí)行過了,直接return
    • 如果 任務(wù)執(zhí)行后渗钉,加鎖失敗了彤恶,則走到_dispatch_once_mark_done_if_quiesced函數(shù),再次進行存儲鳄橘,將標(biāo)識符置為DLOCK_ONCE_DONE
    • 反之声离,則通過_dispatch_once_gate_tryenter嘗試進入任務(wù),即解鎖瘫怜,然后執(zhí)行_dispatch_once_callout執(zhí)行block回調(diào)
  • 如果此時有任務(wù)正在執(zhí)行术徊,再次進來一個任務(wù)2,則通過_dispatch_once_wait函數(shù)讓任務(wù)2進入無限次等待

⑤.3 _dispatch_once_gate_tryenter 解鎖

查看其源碼宝磨,主要是通過底層os_atomic_cmpxchg方法進行對比弧关,如果比較沒有問題,則進行加鎖唤锉,即任務(wù)的標(biāo)識符置為DLOCK_ONCE_UNLOCKED

⑤.4 _dispatch_once_callout 回調(diào)

進入_dispatch_once_callout源碼世囊,主要就兩步

  • _dispatch_client_calloutblock回調(diào)執(zhí)行
  • _dispatch_once_gate_broadcast:進行廣播
  • 進入_dispatch_client_callout源碼,主要就是執(zhí)行block回調(diào)窿祥,其中的f等于_dispatch_Block_invoke(block)株憾,即異步回調(diào)
  • 進入 _dispatch_once_gate_broadcast -> _dispatch_once_mark_done源碼,主要就是給dgo->dgo_once一個值,然后將任務(wù)的標(biāo)識符為DLOCK_ONCE_DONE嗤瞎,即解鎖

⑤.5 總結(jié)

針對單例的底層實現(xiàn)墙歪,主要說明如下:

  • 單例只執(zhí)行一次的原理GCD單例中,有兩個重要參數(shù)贝奇,onceTokenblock虹菲,其中onceToken是靜態(tài)變量,具有唯一性掉瞳,在底層被封裝成了dispatch_once_gate_t類型的變量l毕源,l主要是用來獲取底層原子封裝性的關(guān)聯(lián),即變量v陕习,通過v來查詢?nèi)蝿?wù)的狀態(tài)霎褐,如果此時v等于DLOCK_ONCE_DONE,說明任務(wù)已經(jīng)處理過一次了该镣,直接return

  • block調(diào)用時機:如果此時任務(wù)沒有執(zhí)行過冻璃,則會在底層通過C++函數(shù)的比較,將任務(wù)進行加鎖损合,即任務(wù)狀態(tài)置為DLOCK_ONCE_UNLOCK省艳,目的是為了保證當(dāng)前任務(wù)執(zhí)行的唯一性申屹,防止在其他地方有多次定義.加鎖之后進行block回調(diào)函數(shù)的執(zhí)行植阴,執(zhí)行完成后磅废,將當(dāng)前任務(wù)解鎖减宣,將當(dāng)前的任務(wù)狀態(tài)置為DLOCK_ONCE_DONE矫渔,在下次進來時雹有,就不會在執(zhí)行剥汤,會直接返回

  • 多線程影響:如果在當(dāng)前任務(wù)執(zhí)行期間免姿,有其他任務(wù)進來擦耀,會進入無限次等待棉圈,原因是當(dāng)前任務(wù)已經(jīng)獲取了鎖,進行了加鎖眷蜓,其他任務(wù)是無法獲取鎖的

單例的底層流程分析如下如所示


⑥ 柵欄函數(shù)

GCD中常用的柵欄函數(shù)分瘾,主要有兩種

  • 同步柵欄函數(shù)dispatch_barrier_sync(在主線程中執(zhí)行):前面的任務(wù)執(zhí)行完畢才會來到這里,但是同步柵欄函數(shù)會堵塞線程吁系,影響后面的任務(wù)執(zhí)行
  • 異步柵欄函數(shù)dispatch_barrier_async:前面的任務(wù)執(zhí)行完畢才會來到這里

柵欄函數(shù)最直接的作用就是 控制任務(wù)執(zhí)行順序德召,使同步執(zhí)行

柵欄函數(shù)需要注意以下幾點

  • 柵欄函數(shù)只能控制同一并發(fā)隊列
  • 同步柵欄添加進入隊列的時候,當(dāng)前線程會被鎖死汽纤,直到同步柵欄之前的任務(wù)和同步柵欄任務(wù)本身執(zhí)行完畢時上岗,當(dāng)前線程才會打開然后繼續(xù)執(zhí)行下一句代碼
  • 在使用柵欄函數(shù)時,使用自定義隊列才有意義
    • 如果柵欄函數(shù)中使用全局隊列運行會崩潰蕴坪,原因是系統(tǒng)也在用全局并發(fā)隊列肴掷,使用柵欄同時會攔截系統(tǒng)的敬锐,所以會崩潰
    • 如果將自定義并發(fā)隊列改為串行隊列,即serial 呆瞻,串行隊列本身就是有序同步 此時加?xùn)艡谔ǘ幔瑫速M性能

⑥.1 異步柵欄函數(shù)

進入dispatch_barrier_async源碼實現(xiàn),其底層的實現(xiàn)與dispatch_async類似痴脾,這里就不再做分析了颤介,有興趣的可以自行探索下

⑥.2 同步柵欄函數(shù)

⑥.2.1 dispatch_barrier_sync

進入dispatch_barrier_sync源碼,實現(xiàn)如下

⑥.2.2 _dispatch_barrier_sync_f_inline

進入_dispatch_barrier_sync_f -> _dispatch_barrier_sync_f_inline源碼

主要有分為以下幾部分

  • 通過_dispatch_tid_self獲取線程ID
  • 通過_dispatch_queue_try_acquire_barrier_sync判斷線程狀態(tài)
* 進入`_dispatch_queue_try_acquire_barrier_sync_and_suspend`明郭,在這里進行釋放![](https://upload-images.jianshu.io/upload_images/2340353-4ccaf0f24ed3b69a.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
  • 通過_dispatch_sync_recurse遞歸查找柵欄函數(shù)的target
  • 通過_dispatch_introspection_sync_begin對向前信息進行處理
  • 通過_dispatch_lane_barrier_sync_invoke_and_complete執(zhí)行block并釋放

⑦ 信號量

信號量的作用一般是用來使任務(wù)同步執(zhí)行买窟,類似于互斥鎖,用戶可以根據(jù)需要控制GCD最大并發(fā)數(shù).前面我們已經(jīng)說了怎么使用了

下面我們來分析其底層原理

⑦.1 dispatch_semaphore_create 創(chuàng)建

該函數(shù)的底層實現(xiàn)如下薯定,主要是初始化信號量,并設(shè)置GCD的最大并發(fā)數(shù)瞳购,其最大并發(fā)數(shù)必須大于0

⑦.2 dispatch_semaphore_wait 加鎖

該函數(shù)的源碼實現(xiàn)如下话侄,其主要作用是對信號量dsema通過os_atomic_dec2o進行了--操作,其內(nèi)部是執(zhí)行的C++atomic_fetch_sub_explicit方法

  • 如果value >= 0学赛,表示操作無效年堆,即執(zhí)行成功
  • 如果value = LONG_MIN,系統(tǒng)會拋出一個crash
  • 如果value < 0盏浇,則進入長等待

其中os_atomic_dec2o的宏定義轉(zhuǎn)換如下

os_atomic_inc2o(p, f, m) 
os_atomic_sub2o(p, f, 1, m)
_os_atomic_c11_op((p), (v), m, sub, -)
_os_atomic_c11_op((p), (v), m, add, +)
({ _os_atomic_basetypeof(p) _v = (v), _r = \
        atomic_fetch_##o##_explicit(_os_atomic_c11_atomic(p), _v, \
        memory_order_##m); (__typeof__(_r))(_r op _v); })

將具體的值代入為

os_atomic_dec2o(dsema, dsema_value, acquire);
os_atomic_sub2o(dsema, dsema_value, 1, m)
os_atomic_sub(dsema->dsema_value, 1, m)
_os_atomic_c11_op(dsema->dsema_value, 1, m, sub, -)
_r = atomic_fetch_sub_explicit(dsema->dsema_value, 1),
等價于 dsema->dsema_value - 1

進入_dispatch_semaphore_wait_slow的源碼實現(xiàn)变丧,當(dāng)value < 0時,根據(jù)等待事件timeout做出不同操作

⑦.3 dispatch_semaphore_signal 解鎖

該函數(shù)的源碼實現(xiàn)如下绢掰,其核心也是通過os_atomic_inc2o函數(shù)對value進行了++操作痒蓬,os_atomic_inc2o內(nèi)部是通過C++atomic_fetch_add_explicit

  • 如果value > 0,表示操作無效滴劲,即執(zhí)行成功
  • 如果value < 0攻晒,則進入長等待

其中os_atomic_dec2o的宏定義轉(zhuǎn)換如下

os_atomic_inc2o(p, f, m) 
os_atomic_add2o(p, f, 1, m)
os_atomic_add(&(p)->f, (v), m)
_os_atomic_c11_op((p), (v), m, add, +)
({ _os_atomic_basetypeof(p) _v = (v), _r = \
        atomic_fetch_##o##_explicit(_os_atomic_c11_atomic(p), _v, \
        memory_order_##m); (__typeof__(_r))(_r op _v); })

將具體的值代入為

os_atomic_inc2o(dsema, dsema_value, release);
os_atomic_add2o(dsema, dsema_value, 1, m) 
os_atomic_add(&(dsema)->dsema_value, (1), m)
_os_atomic_c11_op((dsema->dsema_value), (1), m, add, +)
_r = atomic_fetch_add_explicit(dsema->dsema_value, 1),
等價于 dsema->dsema_value + 1

⑦.4 總結(jié)

  • dispatch_semaphore_create 主要就是初始化限號量
  • dispatch_semaphore_wait是對信號量的value進行--,即加鎖操作
  • dispatch_semaphore_signal 是對信號量的value進行++班挖,即解鎖操作

綜上所述鲁捏,信號量相關(guān)函數(shù)的底層操作如圖所示

⑧ 調(diào)度組的原理

調(diào)度組的最直接作用是控制任務(wù)執(zhí)行順序,常見方式如下

⑧.1 dispatch_group_create

  • 進入dispatch_group_create源碼
    主要是創(chuàng)建group萧芙,并設(shè)置屬性给梅,此時的groupvalue為0
  • 進入_dispatch_group_create_with_count源碼,其中是對group對象屬性賦值双揪,并返回group對象动羽,其中的n等于0

⑧.2 dispatch_group_enter 進組

進入dispatch_group_enter源碼,通過os_atomic_sub_orig2odg->dg.bits--操作盟榴,對數(shù)值進行處理

⑧.3 dispatch_group_leave 出組

進入dispatch_group_leave源碼,可知

  • -1 到 0曹质,即++操作
  • 根據(jù)狀態(tài),do-while循環(huán),喚醒執(zhí)行block任務(wù)
  • 如果0 + 1 = 1羽德,enter-leave不平衡几莽,即leave多次調(diào)用,會crash
  • 進入_dispatch_group_wake源碼宅静,do-while 循環(huán)進行異步命中章蚣,調(diào)用_dispatch_continuation_async執(zhí)行
  • 進入_dispatch_continuation_async源碼

這步與異步函數(shù)的block回調(diào)執(zhí)行是一致的,這里不再作說明

⑧.4 dispatch_group_notify 通知

進入dispatch_group_notify源碼姨夹,如果old_state等于0纤垂,就可以進行釋放了

除了leave可以通過_dispatch_group_wake喚醒,其中dispatch_group_notify也是可以喚醒的

  • 其中os_mpsc_push_update_tail是宏定義磷账,用于獲取dg的狀態(tài)碼

⑧.5 dispatch_group_async

進入dispatch_group_async 源碼峭沦,主要是包裝任務(wù)異步處理任務(wù)

  • 進入_dispatch_continuation_group_async源碼,主要是封裝了dispatch_group_enter進組操作
  • 進入_dispatch_continuation_async源碼逃糟,執(zhí)行常規(guī)的異步函數(shù)底層操作.既然有了enter吼鱼,肯定有leave,我們猜測block執(zhí)行之后隱性的執(zhí)行l(wèi)eave绰咽,通過斷點調(diào)試菇肃,打印堆棧信息

  • 搜索_dispatch_client_callout的調(diào)用,在_dispatch_continuation_with_group_invoke

所以取募,完美的印證dispatch_group_async底層封裝的是enter-leave

⑧.6 總結(jié)

  • enter-leave只要成對就可以琐谤,不管遠近
  • dispatch_group_enter在底層是通過C++函數(shù),對groupvalue進行--操作(即0 -> -1
  • dispatch_group_leave在底層是通過C++函數(shù)玩敏,對groupvalue進行++操作(即-1 -> 0
  • dispatch_group_notify在底層主要是判斷groupstate是否等于0斗忌,當(dāng)等于0時,就通知
  • block任務(wù)的喚醒聊品,可以通過dispatch_group_leave飞蹂,也可以通過dispatch_group_notify
  • dispatch_group_async 等同于enter - leave,其底層的實現(xiàn)就是enter-leave

綜上所述翻屈,調(diào)度組的底層分析流程如下圖所示

六陈哑、相關(guān)試題解析

① 異步函數(shù)+并行隊列

下面代碼的輸出順序是什么?


異步函數(shù)并不會阻塞主隊列伸眶,會開辟新線程執(zhí)行異步任務(wù)

分析思路如下圖所示惊窖,紅線表示任務(wù)的執(zhí)行順序
  • 主線程的任務(wù)隊列為:任務(wù)1、異步block1厘贼、任務(wù)5界酒,其中異步block1會比較耗費性能,任務(wù)1任務(wù)5的任務(wù)復(fù)雜度是相同的嘴秸,所以任務(wù)1和任務(wù)5優(yōu)先于異步block1執(zhí)行
  • 異步block1中毁欣,任務(wù)隊列為:任務(wù)2庇谆、異步block2、任務(wù)4凭疮,其中block2相對比較耗費性能饭耳,任務(wù)2任務(wù)4是復(fù)雜度一樣,所以任務(wù)2和任務(wù)4優(yōu)先于block2執(zhí)行
  • 最后執(zhí)行block2中的任務(wù)3
  • 在極端情況下执解,可能出現(xiàn) 任務(wù)2先于任務(wù)1任務(wù)5執(zhí)行寞肖,原因是出現(xiàn)了當(dāng)前主線程卡頓或者 延遲的情況

擴展一
并行隊列 改成 串行隊列,對結(jié)果沒有任何影響衰腌,順序仍然是1 5 2 4 3

擴展二
在任務(wù)5之前新蟆,休眠2s,即sleep(2)右蕊,執(zhí)行的順序為:1 2 4 3 5,原因是因為I/O的打印琼稻,相比于休眠2s,復(fù)雜度更簡單尤泽,所以異步block1 會先于任務(wù)5執(zhí)行.當(dāng)然如果主隊列堵塞欣簇,會出現(xiàn)其他的執(zhí)行順序

② 異步函數(shù)嵌套同步函數(shù) + 并發(fā)隊列

下面代碼的輸出順序是什么?


分析如下:

  • 任務(wù)1任務(wù)5的分析同前面一致坯约,執(zhí)行順序為 任務(wù)1 任務(wù)5 異步block
  • 異步block中,首先執(zhí)行任務(wù)2莫鸭,然后走到同步block闹丐,由于同步函數(shù)會阻塞主線程,所以任務(wù)4需要等待任務(wù)3執(zhí)行完成后被因,才能執(zhí)行卿拴,所以異步block中的執(zhí)行順序是:任務(wù)2 任務(wù)3 任務(wù)4

③ 異步函數(shù)嵌套同步函數(shù) + 串行隊列(即同步隊列)

下面代碼的執(zhí)行順序是什么?會出現(xiàn)什么情況梨与?為什么堕花?


分析如下圖所示,紅色表示任務(wù)執(zhí)行順序粥鞋,黑色虛線表示等待
  • 首先執(zhí)行任務(wù)1缘挽,接下來是異步block,并不會阻塞主線程呻粹,相比任務(wù)5而言壕曼,復(fù)雜度更高,所以優(yōu)先執(zhí)行任務(wù)5等浊,在執(zhí)行異步block
  • 異步block中腮郊,先執(zhí)行任務(wù)2,接下來是同步block筹燕,同步函數(shù)會阻塞線程轧飞,所以執(zhí)行任務(wù)4需要等待任務(wù)3執(zhí)行完成衅鹿,而任務(wù)3的執(zhí)行,需要等待異步block執(zhí)行完成过咬,相當(dāng)于任務(wù)3等待任務(wù)4完成
  • 所以就造成了任務(wù)4等待任務(wù)3大渤,任務(wù)3等待任務(wù)4,即互相等待的局面援奢,就會造成死鎖兼犯,這里有個重點是關(guān)鍵的堆棧 slow

擴展一
去掉任務(wù)4,執(zhí)行順序是什么集漾?
還是會死鎖切黔,因為任務(wù)3等待的是異步block執(zhí)行完畢,而異步block等待任務(wù)3.

④ 異步函數(shù) + 同步函數(shù) + 并發(fā)隊列

下面代碼的執(zhí)行順序是什么具篇?(答案是 AC)
A: 1230789
B: 1237890
C: 3120798
D: 2137890


分析

  • 任務(wù)1任務(wù)2由于是異步函數(shù)+并發(fā)隊列纬霞,會開啟線程,所以沒有固定順序
  • 任務(wù)7驱显、任務(wù)8诗芜、任務(wù)9同理,會開啟線程埃疫,所以沒有固定順序
  • 任務(wù)3同步函數(shù)+并發(fā)隊列伏恐,同步函數(shù)會阻塞主線程,但是也只會阻塞0栓霜,所以翠桦,可以確定的是 0一定在3之后在789之前

以下是不同的執(zhí)行順序的打印


⑤ 下面代碼中胳蛮,隊列的類型有幾種销凑?

隊列總共有兩種: 并發(fā)隊列串行隊列

  • 串行隊列:serialmainQueue
  • 并發(fā)隊列:conque仅炊、globalQueue

寫在后面

和諧學(xué)習(xí),不急不躁.我還是我,顏色不一樣的煙火.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末斗幼,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子抚垄,更是在濱河造成了極大的恐慌蜕窿,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件督勺,死亡現(xiàn)場離奇詭異渠羞,居然都是意外死亡,警方通過查閱死者的電腦和手機智哀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門次询,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人瓷叫,你說我怎么就攤上這事屯吊∷脱玻” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵盒卸,是天一觀的道長骗爆。 經(jīng)常有香客問我,道長蔽介,這世上最難降的妖魔是什么摘投? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮虹蓄,結(jié)果婚禮上犀呼,老公的妹妹穿的比我還像新娘。我一直安慰自己薇组,他們只是感情好外臂,可當(dāng)我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著律胀,像睡著了一般宋光。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上炭菌,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天罪佳,我揣著相機與錄音,去河邊找鬼黑低。 笑死菇民,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的投储。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼阔馋,長吁一口氣:“原來是場噩夢啊……” “哼玛荞!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起呕寝,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤勋眯,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后下梢,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體客蹋,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年孽江,在試婚紗的時候發(fā)現(xiàn)自己被綠了讶坯。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡岗屏,死狀恐怖辆琅,靈堂內(nèi)的尸體忽然破棺而出漱办,到底是詐尸還是另有隱情,我是刑警寧澤婉烟,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布娩井,位于F島的核電站,受9級特大地震影響似袁,放射性物質(zhì)發(fā)生泄漏洞辣。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一昙衅、第九天 我趴在偏房一處隱蔽的房頂上張望扬霜。 院中可真熱鬧,春花似錦绒尊、人聲如沸畜挥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蟹但。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間估灿,已是汗流浹背友存。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留客叉,地道東北人。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓话告,卻偏偏與公主長得像兼搏,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子沙郭,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容