ios多線程的一些淺見

前言

在移動(dòng)端開發(fā)中不可避免的會(huì)接觸到多線程。從用戶使用體驗(yàn)角度來講,也不可避免的會(huì)接觸到多線程的操作。

多線程基礎(chǔ)

什么是線程

線程,也被稱為輕量級(jí)進(jìn)程,是程序執(zhí)行的最小單元。一個(gè)標(biāo)準(zhǔn)的線程由線程 ID、當(dāng)前指令指針、寄存器和堆棧組成。一個(gè)進(jìn)程由一到多個(gè)線程組成,線程之間又共享程序的內(nèi)存空間和一些進(jìn)程級(jí)資源。

線程調(diào)度優(yōu)先級(jí)

當(dāng)線程數(shù)小于處理器核心數(shù)量時(shí),是真正的并發(fā),當(dāng)大于的時(shí)候,線程的并發(fā)會(huì)受到一定阻礙。這可能也是為什么 Intel 即將要推出的 i7 - 9700k 是 8 核心 8 線程的原因,而不是 i7 - 8700k 那樣擁有超線程技術(shù)的 6 核心 12 線程的 CPU。

在單核處理多線程的情況下,并發(fā)操作是模擬出來的一種狀態(tài),操作系統(tǒng)會(huì)讓這些線程輪流執(zhí)行一段時(shí)間,時(shí)間短到足以看起來這些線程是在同步執(zhí)行的。這種行為稱為線程調(diào)度。在線程調(diào)度中是有優(yōu)先級(jí)調(diào)度的,高優(yōu)先級(jí)的先執(zhí)行,低優(yōu)先級(jí)的線程通常要等到系統(tǒng)已經(jīng)沒有高優(yōu)先級(jí)的可執(zhí)行線程存在時(shí)才會(huì)開始執(zhí)行,這也是為什么 GCD 會(huì)提供 Background、utility 等優(yōu)先級(jí)選項(xiàng)。

除了用戶手動(dòng)控制線程的優(yōu)先級(jí),操作系統(tǒng)還會(huì)自動(dòng)調(diào)整線程優(yōu)先級(jí)。頻繁進(jìn)入等待狀態(tài)的線程被稱為 IO 密集型線程,很少等待,處理耗時(shí)操作長(zhǎng)時(shí)間占用時(shí)間片的線程一般稱為 CPU 密集型線程,IO 密集型線程比 CPU 密集型線程在線程優(yōu)先級(jí)的調(diào)整中,更容易獲得優(yōu)先級(jí)的提升。

在線程調(diào)度中存在一種餓死現(xiàn)象。餓死現(xiàn)象是說,這個(gè)線程的優(yōu)先級(jí)較低,而在它之前又有一個(gè)耗時(shí)的線程執(zhí)行,導(dǎo)致它無法執(zhí)行,最后餓死。為了避免這種情況,調(diào)度系統(tǒng)通常會(huì)提升那些等待時(shí)間過長(zhǎng)線程的優(yōu)先級(jí),提升到足夠讓它執(zhí)行的程度。

線程安全

數(shù)據(jù)競(jìng)爭(zhēng)

舉個(gè)例子,線程 1 有一個(gè)變量 i,并且在做 i += 1 的操作,線程 2 同時(shí)對(duì)這個(gè)變量做 i -= 1 的操作,線程 1、2 是并發(fā)執(zhí)行的,這時(shí)就會(huì)發(fā)生競(jìng)爭(zhēng)關(guān)系。

同步和鎖

同步,指在一個(gè)線程操作一個(gè)數(shù)據(jù)未結(jié)束時(shí),其他線程不得對(duì)同一個(gè)數(shù)據(jù)進(jìn)行訪問。為了避免多個(gè)線程同事讀寫一個(gè)數(shù)據(jù)而產(chǎn)生不可預(yù)知的結(jié)果,我們要將各個(gè)線程對(duì)這個(gè)數(shù)據(jù)的訪問進(jìn)行同步。

同步最常見的方法是使用鎖。每個(gè)線程在訪問數(shù)據(jù)之前會(huì)先獲取鎖,并在訪問之后釋放鎖。在鎖已經(jīng)被占用時(shí),試圖獲取鎖,線程會(huì)等待到鎖重新可用。

信號(hào)量(Semaphore)

在 iOS 中,信號(hào)量主要表現(xiàn)方式為 dispatch_semaphore_t,最終會(huì)調(diào)用 sem_wait 方法。
和 dispatch_semaphore 相關(guān)的函數(shù)有三個(gè),創(chuàng)建信號(hào),等待信號(hào),發(fā)送信號(hào)。
信號(hào)量是允許并發(fā)訪問的,可以由一個(gè)線程獲取,另一個(gè)線程釋放。

互斥量(Mutex)

互斥量?jī)H允許一個(gè)線程訪問。互斥量和信號(hào)量不同的是,互斥量要求哪個(gè)線程獲取了,哪個(gè)線程就要負(fù)責(zé)去釋放。
在 iOS 中,pthread_mutex 可以作為互斥鎖。pthread_mutex 不是使用忙等,會(huì)阻塞線程并進(jìn)行等待。它本身擁有設(shè)置協(xié)議的功能,通過設(shè)置協(xié)議來解決優(yōu)先級(jí)反轉(zhuǎn)的問題:

pthread_mutexattr_setprotocol(pthread_mutexattr_t *attr, int protocol)

NSLock 也是互斥鎖,只不過是用 OC 的方式暴露出來,內(nèi)部封裝了一個(gè) pthread_mutex。在 YYKit 源碼中,ibireme 大佬頻繁使用 pthread_mutex 而不是 NSLock,是應(yīng)為 NSLock 是 OC 類,在使用時(shí)會(huì)經(jīng)過消息轉(zhuǎn)發(fā),方法調(diào)用等操作,比 pthread 略慢。

let lock = NSLock()
lock.lock()
// Todo
lock.unlock()

@synchronized(Obj) 也是一種便捷的互斥鎖創(chuàng)建方式,同事它也是一個(gè)遞歸鎖。

讀寫鎖(Read-Write Lock)

讀寫鎖,在對(duì)文件進(jìn)行操作的時(shí)候,寫操作是排他的,一旦有多個(gè)線程對(duì)同一個(gè)文件進(jìn)行寫操作,后果不可估量,但讀是可以的,多個(gè)線程讀取時(shí)沒有問題的。

  1. 當(dāng)讀寫鎖被一個(gè)線程以讀模式占用的時(shí)候,寫操作的其他線程會(huì)被阻塞,讀操作的其他線程還可以繼續(xù)進(jìn)行
  2. 當(dāng)讀寫鎖被一個(gè)線程以寫模式占用的時(shí)候,寫操作的其他線程會(huì)被阻塞,讀操作的其他線程也被阻塞

在 iOS 中,讀寫鎖主要變現(xiàn)為 pthread_rwlock_t

條件變量(Condition Variable)

條件變量,作用類似于一個(gè)柵欄。

  1. 線程可以等待條件變量,一個(gè)條件變量可以被多個(gè)線程等待。
  2. 線程可以喚醒條件變量,此時(shí)所有等待此變量的線程都會(huì)被喚醒。

使用條件變量,可以讓許多線程一起等待某個(gè)事件的發(fā)生,當(dāng)事件發(fā)生時(shí),所有線程可以恢復(fù)執(zhí)行。

在 iOS 中,NSCondition 表現(xiàn)為條件變量。

介紹條件變量的文章非常多,但大多都對(duì)一個(gè)一個(gè)基本問題避而不談:“為什么要用條件變量?它僅僅是控制了線程的執(zhí)行順序,用信號(hào)量或者互斥鎖能不能模擬出類似效果?”

網(wǎng)上的相關(guān)資料比較少,我簡(jiǎn)單說一下個(gè)人看法。信號(hào)量可以一定程度上替代 condition,但是互斥鎖不行。在以上給出的生產(chǎn)者-消費(fèi)者模式的代碼中, pthread_cond_wait 方法的本質(zhì)是鎖的轉(zhuǎn)移,消費(fèi)者放棄鎖,然后生產(chǎn)者獲得鎖,同理,pthread_cond_signal 則是一個(gè)鎖從生產(chǎn)者到消費(fèi)者轉(zhuǎn)移的過程。

參考鏈接:bestswifter iOS鎖的博文

自旋鎖(Spin lock)

關(guān)于自旋鎖,可以查閱 ibirme 大佬的《不再安全的 OSSpinLock》

Thread

創(chuàng)建

Thread 創(chuàng)建有三種方式:

//第一種,手動(dòng)調(diào)用 start
// convenience init(target: Any, selector: Selector, object argument: Any?)
let thread = Thread(target: self, selector: #selector(thread1Action(_:)), object: "Thread1")
thread.name = "Background 1"
thread.start()

// 第二種,類方法
// class func detachNewThreadSelector(_ selector: Selector, toTarget target: Any, with argument: Any?)
Thread.detachNewThreadSelector(#selector(thread2Action(_:)), toTarget: self, with: "Thread2")

// 第三種 performSelector
performSelector(inBackground: #selector(thread3Action(_:)), with: "Thread3")

線程安全

在 OC 中可以添加 @synchronized() 方法方便的給線程加鎖,但是 Swift 中,這個(gè)方法已經(jīng)不存在。@synchronized 實(shí)際上在底層是調(diào)用了 objc_sync_enterobjc_sync_exit 方法以及一些異常處理。所以忽略異常問題可以簡(jiǎn)單實(shí)現(xiàn)一個(gè) synchronized 方法:

func synchronized(_ lock: AnyObject, closure:() -> ()) {
 objc_sync_enter(lock)
 closure()
 objc_sync_exit(lock)
}

經(jīng)典的售票系統(tǒng)簡(jiǎn)單模擬:

@IBAction func saleTicket(_ sender: Any) {
    
    firstTicketWindow = Thread(target: self, selector: #selector(saleTicketAction), object: "Ticket Window 1")
    firstTicketWindow.name = "Ticket Window 1"
    
    secondTicketWindow = Thread(target: self, selector: #selector(saleTicketAction), object: "Ticket Window 2")
    secondTicketWindow.name = "Ticket Window 2"
    
    thirdTicketWindow = Thread(target: self, selector: #selector(saleTicketAction), object: "Ticket Window 3")
    thirdTicketWindow.name = "Ticket Window 3"
    
    firstTicketWindow.start()
    secondTicketWindow.start()
    thirdTicketWindow.start()
}

@objc func saleTicketAction() {
    
    while ticketCount > 0 {
        synchronized(self) {
            Thread.sleep(forTimeInterval: 0.1)
            if ticketCount > 0 {
                ticketCount -= 1
                print("\(Thread.current.name!) sold 1 ticket, \(self.ticketCount) remains.")
            } else {
                print("Tickets have been sold out.")
            }
        }
    }
}

線程間通信

在主線程上顯示余票:

if ticketCount > 0 {
    ticketCount -= 1
    print("\(Thread.current.name!) sold 1 ticket, \(self.ticketCount) remains.")
                    
    // 主線程顯示余票
    self.performSelector(onMainThread: #selector(showTicketNum), with: nil, waitUntilDone: true)
}

@objc func showTicketNum() {
    remainingLabel.text = "Ticket remains: \(ticketCount)"
}

Operation

Operation 是 Apple 對(duì)于 GCD 的封裝,但是并不局限于 GCD 的先進(jìn)先出隊(duì)列。API 更加面向?qū)ο蠡僮髌饋硎址奖恪?/p>

Operation 和 OperationQueue

Operation 相當(dāng)于 GCD 的任務(wù), OperationQueue 相當(dāng)于 GCD 的隊(duì)列。
使用 Operation 實(shí)現(xiàn)多線程的具體步驟:

  • 將需要執(zhí)行的操作封裝到 Operation 對(duì)象中
  • 將 Operation 添加到 OperationQueue

創(chuàng)建

一般情況下有三種使用方法:

  • NSInvocaionOperation

NSInvocation 在 Swift 中已被廢除,因?yàn)樗皇穷愋桶踩?ARC 安全的。

下面是 OC 實(shí)現(xiàn):

- (void)testNSInvocationOperation {
    NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationOperation) object:nil];
    [invocationOperation start];
}

- (void)invocationOperation {
    NSLog(@"NSInvocationOperation: %@", [NSThread currentThread]);
}
  • BlockOperation
let operation = BlockOperation {
    print("An block operation without being added in a queue, the thread is: \(Thread.current)")
}
operation.start()

Block Operation 添加執(zhí)行閉包:

let operation = BlockOperation {
    print("Create a block operation in \(Thread.current).")
}
operation.addExecutionBlock {
    print("The block operation has add an execution block in \(Thread.current).")
}
operation.addExecutionBlock {
    print("The block operation has add an execution block in \(Thread.current).")
}
operation.start()
  • Operation 子類

Operation 子類需要?jiǎng)?chuàng)建一個(gè)繼承于 Operation 的類,需要重寫 main() 方法:

class CustomOperation: Operation {
    override func main() {
        
        // Things to do
        for _ in 0 ..< 2 {
            print("Cunstom operation in thread: \(Thread.current)")
        }
    }
}

使用:

let operation = CustomOperation()
operation.start()

OperationQueue

  • OperationQueue 直接創(chuàng)建為子線程:let queue = OperationQueue()
  • OperationQueue 獲取主線程方法:OperationQueue.main

將 Operation 添加到 Queue 中 會(huì)自動(dòng)異步執(zhí)行 Operation 中封裝的操作,不需要再調(diào)用 Operation 的 start() 方法。

使用 addOperation(_:) 方法把 Operation 添加到隊(duì)列

let queue = OperationQueue()

let operation1 = BlockOperation {
 print("Operation 1 has beed added in a queue, in \(Thread.current).")
}

let operation2 = BlockOperation {
 print("Operation 2 has beed added in a queue, in \(Thread.current).")
}

// Operation1 和 Operation2 執(zhí)行順序是不固定的
queue.addOperation(operation1)
queue.addOperation(operation2)

使用 addOperation {} 方法添加 Operation

let queue = OperationQueue()
queue.addOperation {
    for _ in 0 ..< 2 {
        print("A queue add operation with block in \(Thread.current).")
    }
}

OperationQueue 線程間通信

下面以一個(gè)偽下載圖片的代碼來模擬 Operation 線程間通信:

let downloadQueue = OperationQueue()

indicator.startAnimating()

downloadQueue.addOperation {
    
    Thread.sleep(forTimeInterval: 1)
    
    let imageURLString = "https://clutchpoints.com/wp-content/uploads/2018/09/lebron-james.png"
    let imageURL = URL(string: imageURLString)
    let data = try? Data(contentsOf: imageURL!)
    
    guard let theData = data else {
        // 如果沒有圖片數(shù)據(jù),回到主線程停止 indicator
        OperationQueue.main.addOperation {
            self.indicator.stopAnimating()
        }
        print("Download failed.")
        return
    }
    let image = UIImage(data: theData)
    
    // 下載完圖片回到主線程更新 UI  
    OperationQueue.main.addOperation {
        if let image = image {
            self.imageView.image = image
            self.hideButton.isHidden = false
            self.imageView.isHidden = false
            self.indicator.stopAnimating()
        }
    }
}

控制 OperationQueue 最大并發(fā)數(shù)

可以通過 maxConcurrentOperationCount 來控制并發(fā)數(shù)。

let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
queue.addOperation {
 print("First operation - max concurrent number in \(Thread.current).")
}
queue.addOperation {
 print("Second operation - max concurrent number in \(Thread.current).")
}
queue.addOperation {
 print("Third operation - max concurrent number in \(Thread.current).")
}
queue.addOperation {
 print("Fourth operation - max concurrent number in \(Thread.current).")
}

依賴和完成監(jiān)聽

你可以通過 Operation 的 addDependency(_ op: Operation) 方法來添加操作間的依賴關(guān)系:
例如 operation2.addDependency(operation1) 就是說 Operation1 執(zhí)行完畢后 Operation2 才會(huì)執(zhí)行。

你也可以通過 completionBlock 屬性來監(jiān)聽某個(gè)操作已經(jīng)完成。

et queue = OperationQueue()

var flag = false
let operation1 = BlockOperation {
    // 模擬一個(gè)操作是否成功
    flag = true
    print("Operation 1 in \(Thread.current).")
    Thread.sleep(forTimeInterval: 2)
}

// 監(jiān)聽 Operation 1 是否完成
operation1.completionBlock = {
    print("Operation 1 is completed.")
}

let operation2 = BlockOperation {
    if flag {
        print("Operation 2 in \(Thread.current).")
    } else {
        print("Something went wrong.")
    }
}

operation2.addDependency(operation1)

// 過兩秒之后控制臺(tái)才會(huì)打印 Operation1 完成和 Operation2 的執(zhí)行信息
queue.addOperation(operation1)
queue.addOperation(operation2)

取消 Operation

可以通過 Operation 的 cancel() 方法 或 Queue 的 cancelAllOperations() 來取消 Operation。

但,值得注意的是,cancel() 方法,它做的唯一做的就是將 Operation 的 isCancelled 屬性從 false 改為 true。由于它并不會(huì)真正去深入代碼將具體執(zhí)行的工作暫停,所以我們必須利用 isCancelled 屬性的變化來暫停 main() 方法中的工作。

let queue = OperationQueue()
queue.addOperation {
    for i in 0 ... 100000000 {
        print("i: \(i) in \(Thread.current)")
    }
}
queue.cancelAllOperations()
queue.addOperation {
    print("Second operation in \(Thread.current)")
}

let operation = CustomOperation()
// 將 isCancelled 屬性更改為 true
operation.cancel()

// 控制臺(tái)只會(huì)輸出第二個(gè) Operation 的執(zhí)行信息。

GCD

GCD(Grand Central Dispatch) 是 Apple 推薦的方式,它將線程管理推給了系統(tǒng),用的是名為 dispatch queue 的隊(duì)列。開發(fā)者只要定義每個(gè)線程需要執(zhí)行的工作即可。所有的工作都是先進(jìn)先出,每一個(gè) block 運(yùn)轉(zhuǎn)速度極快(納秒級(jí)別)。使用場(chǎng)景主要是為了追求高效處理大量并發(fā)數(shù)據(jù),如圖片異步加載、網(wǎng)絡(luò)請(qǐng)求等。

Dispatch 在 Swift 3 中的改變

任務(wù)和隊(duì)列

  • Async:異步任務(wù)
  • Sync:同步任務(wù)

DispatchQueue 是一個(gè)類似線程的概念,這里稱作對(duì)列隊(duì)列是一個(gè)FIFO數(shù)據(jù)結(jié)構(gòu),意味著先提交到隊(duì)列的任務(wù)會(huì)先開始執(zhí)行)。DispatchQueue 背后是一個(gè)由系統(tǒng)管理的線程池。

DispatchQueue 又分為串行隊(duì)列和并發(fā)隊(duì)列。

串行隊(duì)列使用同步操作容易造成死鎖,例如主線程進(jìn)行同步操作 DispatchQueue.main.sync {}

創(chuàng)建隊(duì)列

創(chuàng)建串行隊(duì)列

如果不設(shè)置 DispatchQueue 的 Attributes,那么默認(rèn)就會(huì)創(chuàng)建串行隊(duì)列。

  • 串行隊(duì)列的同步操作:
let queue = DispatchQueue(label: "com.demo.Serial1")
// 串行隊(duì)列做同步操作, 容易造成死鎖, 不建議這樣使用
queue.sync {
    print("Sync operation in a serial queue.")
}
  • 串行隊(duì)列的異步操作:
let queue = DispatchQueue(label: "com.demo.Serial2")
// 串行隊(duì)列做異步操作是順序執(zhí)行
queue.async {
    for i in 0 ..< 2 {
        print("First i: \(i)")
    }
}
queue.async {
    for i in 0 ..< 2 {
        print("Second i: \(i)")
    }
}

創(chuàng)建并發(fā)隊(duì)列

  • 并發(fā)隊(duì)列同步操作是順序執(zhí)行
let label = "com.demo.Concurrent1"
let qos = DispatchQoS.default
let attributes = DispatchQueue.Attributes.concurrent
let autoreleaseFrequency = DispatchQueue.AutoreleaseFrequency.never
let queue = DispatchQueue(label: label, qos: qos, attributes: attributes, autoreleaseFrequency: autoreleaseFrequency, target: nil)

// 并發(fā)隊(duì)列同步操作是順序執(zhí)行
queue.sync {
    for i in 0 ..< 2 {
        print("First sync i: \(i)")
    }
}
queue.sync {
    for i in 0 ..< 2 {
        print("Second sync i: \(i)")
    }
}
  • 并發(fā)隊(duì)列異步操作執(zhí)行順序不定
let label = "com.demo.Concurrent2"
let attributes = DispatchQueue.Attributes.concurrent
let queue = DispatchQueue(label: label, attributes: attributes)
        
// 并發(fā)隊(duì)列做異步操作執(zhí)行順序不固定
queue.async {
    for i in 0 ..< 2 {
        print("First async i: \(i)")
    }
}
queue.async {
    for i in 0 ..< 2 {
        print("Second async i: \(i)")
    }
}

創(chuàng)建主隊(duì)列和全局隊(duì)列

let mainQueue = DispatchQueue.main
let globalQueue = DispatchQueue.global()
let globalQueueWithQos = DispatchQueue.global(qos: .userInitiated)

QoS

QoS 全稱 Quality of Service,在 Swift 中是一個(gè)結(jié)構(gòu)體,用來指定隊(duì)列或任務(wù)的優(yōu)先級(jí)。

全局隊(duì)列肯定是并發(fā)隊(duì)列。如果不指定優(yōu)先級(jí),就是默認(rèn)(default)優(yōu)先級(jí)。另外還有 background,utility,user-Initiated,unspecified,user-Interactive。下面按照優(yōu)先級(jí)順序從低到高來排列:

  • Background:用來處理特別耗時(shí)的后臺(tái)操作,例如同步、備份數(shù)據(jù)。
  • Utility:用來處理需要一點(diǎn)時(shí)間而又不需要立刻返回結(jié)果的操作。特別適用于異步操作,例如下載、導(dǎo)入數(shù)據(jù)。
  • Default:默認(rèn)優(yōu)先級(jí)。一般來說開發(fā)者應(yīng)該指定優(yōu)先級(jí)。屬于特殊情況。
  • User-Initiated:用來處理用戶觸發(fā)的、需要立刻返回結(jié)果的操作。比如打開用戶點(diǎn)擊的文件。
  • User-Interactive:用來處理用戶交互的操作。一般用于主線程,如果不及時(shí)響應(yīng)就可能阻塞主線程的操作。
  • Unspecified:未確定優(yōu)先級(jí),由系統(tǒng)根據(jù)不同環(huán)境推斷。比如使用過時(shí)的 API 不支持優(yōu)先級(jí),此時(shí)就可以設(shè)定為未確定優(yōu)先級(jí)。屬于特殊情況。

After 延遲

Swift 寫法如下:

override func viewDidLoad() {
    super.viewDidLoad()
    
    print("View did load.")
    let dispatchTime = DispatchTime.now() + 0.5
    DispatchQueue.main.asyncAfter(deadline: dispatchTime) {
        print("After 0.5 seconds.")
    }
}

線程間通信

模擬下載單張圖片并在 imageView 上展示:

使用 DispatchQueue.global().async {}DispatchQueue.main.async {}

@IBAction func downloadImage(_ sender: Any) {
    indicator1.startAnimating()
    // let queue = DispatchQueue.global(qos: .default)
    DispatchQueue.global().async {
        sleep(1)
        let imageURL = URL(string: self.imageURLString1)
        let data = try? Data(contentsOf: imageURL!)
        
        guard let theData = data else {
            OperationQueue.main.addOperation {
                self.indicator1.stopAnimating()
            }
            print("Download failed.")
            return
        }
        let image = UIImage(data: theData)
        
        DispatchQueue.main.async {
            if let image = image {
                self.imageView1.image = image
                self.hideButton.isHidden = false
                self.imageView1.isHidden = false
                self.indicator1.stopAnimating()
            }
        }
    }
}

DispatchGroup

組操作,用來管理一組任務(wù)的執(zhí)行,然后監(jiān)聽任務(wù)都完成的事件。比如,多個(gè)網(wǎng)絡(luò)請(qǐng)求同時(shí)發(fā)出去,等網(wǎng)絡(luò)請(qǐng)求都完成后 reload UI。

步驟:

  1. 創(chuàng)建一個(gè) DispatchGroup
  2. 在并發(fā)隊(duì)列中進(jìn)行異步組操作
  3. 通過 group.notify {} 來組合那些單個(gè)的組操作

模擬多圖下載操作:

@IBAction func downloadImagesInGroup(_ sender: Any) {
    
    indicator1.startAnimating()
    indicator2.startAnimating()
    
    let group = DispatchGroup()
    
    let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first
    var fileURL1 = URL(fileURLWithPath: documentsPath!)
    fileURL1 = fileURL1.appendingPathComponent("LBJ1")
    fileURL1 = fileURL1.appendingPathExtension("png")
    
    var fileURL2 = URL(fileURLWithPath: documentsPath!)
    fileURL2 = fileURL2.appendingPathComponent("LBJ2")
    fileURL2 = fileURL2.appendingPathExtension("jpg")
    
    // 下載圖片1
    group.enter()
    DispatchQueue.global().async {
        
        print("Begin to download image1.")
        
        let imageURL = URL(string: self.imageURLString1)
        let data = try? Data(contentsOf: imageURL!)
        
        guard let theData = data else {
            DispatchQueue.main.async {
                self.indicator1.stopAnimating()
            }
            print("Image 1 download failed.")
            return
        }
        
        try! theData.write(to: fileURL1, options: .atomic)
        
        print("Image1 downloaded.")
        sleep(1)
        group.leave()
    }
    
    // 下載圖片2
    group.enter()
    DispatchQueue.global().async {
        
        print("Begin to download image2.")
        
        let imageURL = URL(string: self.imageURLString2)
        let data = try? Data(contentsOf: imageURL!)
        
        guard let theData = data else {
            DispatchQueue.main.async {
                self.indicator2.stopAnimating()
            }
            print("Image 2 Download failed.")
            return
        }
        
        try! theData.write(to: fileURL2, options: .atomic)
        
        sleep(1)
        print("Image2 downloaded.")
        group.leave()
    }
    
    // 在主線程展示
    group.notify(queue: .main) {
        
        let imageData1 = try? Data(contentsOf: fileURL1)
        let imageData2 = try? Data(contentsOf: fileURL2)
        
        guard let theData1 = imageData1 else {
            return
        }
        guard let theData2 = imageData2 else {
            return
        }
        
        let image1 = UIImage(data: theData1)
        let image2 = UIImage(data: theData2)
        
        self.imageView1.image = image1
        self.imageView2.image = image2
        self.imageView1.isHidden = false
        self.imageView2.isHidden = false
        self.indicator1.stopAnimating()
        self.indicator2.stopAnimating()
        self.hideButton.isHidden = false
    }
}

DispatchBarrier

柵欄函數(shù),函數(shù)之前的任務(wù)提交完了才會(huì)執(zhí)行后續(xù)的任務(wù):

let label = "com.demo.Concurrent3"
let queue = DispatchQueue(label: label, attributes: .concurrent)

queue.async {
    for i in 0 ..< 2 {
        print("First i: \(i)")
    }
}
queue.async {
    for i in 0 ..< 2 {
        print("Second i: \(i)")
    }
}

queue.async(flags: .barrier) {
    print("This is a barrier.")
}

queue.async {
    for i in 0 ..< 2 {
        print("Third i: \(i)")
    }
}
queue.async {
    for i in 0 ..< 2 {
        print("Fourth i: \(i)")
    }
}

控制臺(tái)輸出:

由此可見,只有當(dāng) First 和 Second 執(zhí)行完畢才會(huì)執(zhí)行 Third 和 Fourth,并且 First 和 Second 執(zhí)行順序是不確定的,Third 和 Fourth 也是如此。

Semaphore

信號(hào)量,是鎖機(jī)制。

DispatchSemaphore 是傳統(tǒng)計(jì)數(shù)信號(hào)量的封裝,用來控制資源被多任務(wù)訪問的情況。

舉個(gè)例子,一共有兩個(gè)停車位,現(xiàn)在 A、B、C 都需要停車,A 和 B 先挺的情況下,C 過來了,這時(shí) C 就要等待 A 或 B 其中有一個(gè)出來,才會(huì)繼續(xù)停進(jìn)去。

注意:在串行隊(duì)列上使用信號(hào)量要注意死鎖的問題。

模擬停車操作:

let semaphore = DispatchSemaphore(value: 2)

// semaphore 在串行隊(duì)列需要注意死鎖問題
let queue = DispatchQueue(label: "com.demo.Concurrent4", qos: .default, attributes: .concurrent)

queue.async {
    semaphore.wait()
    print("First car in.")
    sleep(2)
    print("First car out.")
    semaphore.signal()
}

queue.async {
    semaphore.wait()
    print("Second car in.")
    sleep(3)
    print("Second car out.")
    semaphore.signal()
}

queue.async {
    semaphore.wait()
    print("Third car in.")
    sleep(4)
    print("Third car out.")
    semaphore.signal()
}

控制臺(tái)輸出:

由此可見,第一輛車出來了,第三輛車才能進(jìn)去。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,055評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,346評(píng)論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,889評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評(píng)論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,118評(píng)論 0 286
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,637評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,558評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,739評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評(píng)論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,980評(píng)論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評(píng)論 1 280
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,347評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,702評(píng)論 2 370

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

  • iOS多線程實(shí)踐中,常用的就是子線程執(zhí)行耗時(shí)操作,然后回到主線程刷新UI。在iOS中每個(gè)進(jìn)程啟動(dòng)后都會(huì)建立一個(gè)主線...
    jackyshan閱讀 1,463評(píng)論 2 12
  • iOS多線程編程 基本知識(shí) 1. 進(jìn)程(process) 進(jìn)程是指在系統(tǒng)中正在運(yùn)行的一個(gè)應(yīng)用程序,就是一段程序的執(zhí)...
    陵無山閱讀 6,092評(píng)論 1 14
  • 一.概述 1.基本概念 同步與異步的概念 同步 必須等待當(dāng)前語句執(zhí)行完畢,才可以執(zhí)行下一個(gè)語句。 異步 不用等待當(dāng)...
    Jt_Self閱讀 478評(píng)論 0 1
  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時(shí)...
    歐辰_OSR閱讀 29,463評(píng)論 8 265
  • 1、阿里巴巴 馬云有一次在美國(guó)一家餐廳吃飯時(shí),他突發(fā)奇想,找來了餐廳服務(wù)員,問他是否知道阿里巴巴這個(gè)名字。服務(wù)員回...
    GaryHost閱讀 259評(píng)論 0 0