การทำงานพร้อมกันใน iOS เป็นหัวข้อใหญ่ ดังนั้นในบทความนี้ ฉันต้องการขยายในหัวข้อย่อยเกี่ยวกับคิวและเฟรมเวิร์ก Grand Central Dispatch (GCD)
โดยเฉพาะอย่างยิ่ง ฉันต้องการสำรวจความแตกต่างระหว่างคิวแบบอนุกรมและแบบพร้อมกัน ตลอดจนความแตกต่างระหว่างการดำเนินการแบบซิงโครนัสและแบบอะซิงโครนัส
หากคุณไม่เคยใช้ GCD มาก่อน บทความนี้เป็นจุดเริ่มต้นที่ดี หากคุณมีประสบการณ์กับ GCD แล้ว แต่ยังสงสัยเกี่ยวกับหัวข้อที่กล่าวถึงข้างต้น ฉันคิดว่าคุณยังคงพบว่ามีประโยชน์ และฉันหวังว่าคุณจะเลือกสิ่งใหม่หนึ่งหรือสองอย่างระหว่างทาง
ฉันสร้างแอพคู่หู SwiftUI เพื่อสาธิตแนวคิดในบทความนี้ด้วยสายตา แอพนี้ยังมีแบบทดสอบสั้นๆ สนุกๆ ที่ฉันแนะนำให้คุณลองอ่านก่อนและหลังอ่านบทความนี้ ดาวน์โหลดซอร์สโค้ดที่นี่ หรือรับเบต้าสาธารณะที่นี่
ฉันจะเริ่มต้นด้วยการแนะนำ GCD ตามด้วยคำอธิบายโดยละเอียดเกี่ยวกับการซิงค์ อะซิงโครนัส อนุกรมและพร้อมกัน หลังจากนั้น ฉันจะอธิบายข้อผิดพลาดบางประการเมื่อทำงานกับการทำงานพร้อมกัน สุดท้ายนี้ ผมจะลงท้ายด้วยบทสรุปและคำแนะนำทั่วไปบางส่วน
แนะนำตัว
เริ่มต้นด้วยการแนะนำสั้น ๆ เกี่ยวกับ GCD และคิวการส่ง ข้ามไปที่ Sync vs Async . ได้ตามสบาย ส่วนถ้าคุณคุ้นเคยกับหัวข้อนี้แล้ว
Concurrency และ Grand Central Dispatch
การทำงานพร้อมกันช่วยให้คุณใช้ประโยชน์จากข้อเท็จจริงที่ว่าอุปกรณ์ของคุณมีคอร์ CPU หลายคอร์ หากต้องการใช้คอร์เหล่านี้ คุณจะต้องใช้หลายเธรด อย่างไรก็ตาม เธรดเป็นเครื่องมือระดับต่ำ และการจัดการเธรดด้วยตนเองอย่างมีประสิทธิภาพนั้นยากมาก
Grand Central Dispatch สร้างขึ้นโดย Apple เมื่อกว่า 10 ปีที่แล้วเพื่อเป็นนามธรรมเพื่อช่วยให้นักพัฒนาเขียนโค้ดแบบมัลติเธรดโดยไม่ต้องสร้างและจัดการเธรดด้วยตนเอง
ด้วย GCD Apple ใช้แนวทางการออกแบบแบบอะซิงโครนัส กับปัญหา แทนที่จะสร้างเธรดโดยตรง คุณใช้ GCD เพื่อกำหนดเวลางาน และระบบจะทำงานเหล่านี้ให้คุณโดยใช้ทรัพยากรให้เกิดประโยชน์สูงสุด GCD จะจัดการสร้างเธรดที่จำเป็น และจะกำหนดเวลางานของคุณบนเธรดเหล่านั้น โดยเปลี่ยนภาระในการจัดการเธรดจากผู้พัฒนาไปยังระบบ
ข้อได้เปรียบที่สำคัญของ GCD คือคุณไม่ต้องกังวลกับทรัพยากรฮาร์ดแวร์ขณะเขียนโค้ดพร้อมกัน GCD จัดการเธรดพูลให้คุณ และจะปรับขนาดจาก Apple Watch แบบคอร์เดียวไปจนถึง MacBook Pro แบบมัลติคอร์
ส่งคิว
เหล่านี้เป็นส่วนประกอบหลักของ GCD ที่ให้คุณดำเนินการบล็อกของโค้ดโดยอำเภอใจโดยใช้ชุดของพารามิเตอร์ที่คุณกำหนด งานในคิวการจัดส่งจะเริ่มต้นแบบเข้าก่อนออกก่อน (FIFO) เสมอ สังเกตว่าฉันพูดว่า เริ่มต้น เนื่องจากเวลาที่งานของคุณเสร็จสิ้นขึ้นอยู่กับปัจจัยหลายประการ และไม่รับประกันว่าจะเป็นแบบ FIFO (เพิ่มเติมในภายหลัง)
โดยทั่วไปแล้ว คุณมีคิวอยู่สามประเภท:
- คิวการจัดส่งหลัก (แบบอนุกรม กำหนดไว้ล่วงหน้า)
- คิวส่วนกลาง (กำหนดไว้ล่วงหน้าพร้อมกัน)
- คิวส่วนตัว (อาจเป็นแบบอนุกรมหรือพร้อมกัน คุณสร้างขึ้นเองได้)
ทุกแอพมาพร้อม Main Queue ซึ่งเป็น serial คิวที่รันงานบนเธรดหลัก คิวนี้มีหน้าที่ในการวาด UI ของแอปพลิเคชันของคุณและตอบสนองต่อการโต้ตอบของผู้ใช้ (แตะ เลื่อน เลื่อน ฯลฯ) หากคุณบล็อกคิวนี้นานเกินไป แอป iOS ของคุณจะหยุดทำงาน และแอป macOS ของคุณจะแสดงชายหาดที่น่าอับอาย ลูก/ล้อหมุน
เมื่อทำงานที่ใช้เวลานาน (การโทรผ่านเครือข่าย งานที่ต้องใช้การประมวลผลสูง ฯลฯ) เราจะหลีกเลี่ยงการหยุด UI โดยดำเนินการงานนี้ในคิวพื้นหลัง จากนั้นเราอัปเดต UI ด้วยผลลัพธ์ในคิวหลัก:
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
DispatchQueue.main.async { // UI work
self.label.text = String(data: data, encoding: .utf8)
}
}
} ตามหลักการทั่วไป งาน UI ทั้งหมดจะต้องดำเนินการบนคิวหลัก คุณสามารถเปิดตัวเลือกตัวตรวจสอบเธรดหลักใน Xcode เพื่อรับคำเตือนเมื่อใดก็ตามที่ UI ทำงานบนเธรดพื้นหลัง
นอกจากคิวหลักแล้ว ทุกแอปยังมาพร้อมกับคิวพร้อมกันที่กำหนดไว้ล่วงหน้าหลายรายการซึ่งมีระดับคุณภาพการบริการที่แตกต่างกัน (แนวคิดเชิงนามธรรมของลำดับความสำคัญใน GCD)
ตัวอย่างเช่น นี่คือรหัสสำหรับส่งงานแบบอะซิงโครนัสไปยัง การโต้ตอบของผู้ใช้ (ลำดับความสำคัญสูงสุด) คิว QoS:
DispatchQueue.global(qos: .userInteractive).async {
print("We're on a global concurrent queue!")
} หรือคุณสามารถเรียก ลำดับความสำคัญเริ่มต้น คิวโกลบอลโดยไม่ระบุ QoS แบบนี้:
DispatchQueue.global().async {
print("Generic global queue")
} นอกจากนี้ คุณสามารถสร้างคิวส่วนตัวของคุณเองโดยใช้ไวยากรณ์ต่อไปนี้:
let serial = DispatchQueue(label: "com.besher.serial-queue")
serial.async {
print("Private serial queue")
} เมื่อสร้างคิวส่วนตัว การใช้ป้ายกำกับที่สื่อความหมาย (เช่น สัญกรณ์ DNS แบบย้อนกลับ) จะช่วยได้มาก เนื่องจากจะช่วยคุณในขณะแก้ไขจุดบกพร่องในเนวิเกเตอร์, lldb และเครื่องมือของ Xcode:
โดยค่าเริ่มต้น คิวส่วนตัวจะเป็น ซีเรียล (ฉันจะอธิบายความหมายในไม่ช้านี้ สัญญา!) หากคุณต้องการสร้าง พร้อมกัน ส่วนตัว ในคิว คุณสามารถทำได้โดยใช้ แอตทริบิวต์ พารามิเตอร์:
let concurrent = DispatchQueue(label: "com.besher.serial-queue", attributes: .concurrent)
concurrent.sync {
print("Private concurrent queue")
} มีพารามิเตอร์ QoS เสริมเช่นกัน คิวส่วนตัวที่คุณสร้างในท้ายที่สุดจะลงจอดในคิวพร้อมกันทั่วโลกตามพารามิเตอร์ที่กำหนด
งานมีอะไรบ้าง
ฉันพูดถึงการส่งงานไปยังคิว งานสามารถอ้างถึงกลุ่มของรหัสใด ๆ ที่คุณส่งไปยังคิวโดยใช้ sync หรือ async ฟังก์ชั่น. สามารถส่งในรูปแบบของการปิดโดยไม่ระบุชื่อ:
DispatchQueue.global().async {
print("Anonymous closure")
} หรือภายในรายการงานจัดส่งที่ดำเนินการในภายหลัง:
let item = DispatchWorkItem(qos: .utility) {
print("Work item to be executed later")
} ไม่ว่าคุณจะส่งแบบซิงโครนัสหรืออะซิงโครนัส และไม่ว่าคุณจะเลือกคิวแบบอนุกรมหรือแบบพร้อมกัน โค้ดทั้งหมดภายในงานเดียวจะดำเนินการทีละบรรทัด การทำงานพร้อมกันเกี่ยวข้องเฉพาะเมื่อประเมิน หลายรายการ งาน
ตัวอย่างเช่น หากคุณมี 3 ลูปภายใน เดียวกัน งาน ลูปเหล่านี้จะ เสมอ ดำเนินการตามลำดับ:
DispatchQueue.global().async {
for i in 0..<10 {
print(i)
}
for _ in 0..<10 {
print("?")
}
for _ in 0..<10 {
print("?")
}
} รหัสนี้พิมพ์ตัวเลขสิบหลักจาก 0 ถึง 9 เสมอ ตามด้วยวงกลมสีน้ำเงิน 10 วง ตามด้วยหัวใจที่แตกสลาย 10 ดวง ไม่ว่าคุณจะส่งการปิดนั้นด้วยวิธีใด
งานแต่ละงานสามารถมีระดับ QoS ของตัวเองได้เช่นกัน (โดยค่าเริ่มต้น งานจะใช้ลำดับความสำคัญของคิว) ความแตกต่างระหว่าง QoS ของคิวและ QoS ของงานนำไปสู่พฤติกรรมที่น่าสนใจที่เราจะพูดถึงในส่วนการผกผันลำดับความสำคัญ
ถึงตอนนี้ คุณอาจสงสัยว่า ซีเรียล . อะไร และ พร้อมกัน ทั้งหมดเกี่ยวกับ คุณอาจสงสัยเกี่ยวกับความแตกต่างระหว่าง sync และ async เมื่อส่งงานของคุณ นี่นำเราไปสู่จุดสำคัญของบทความนี้ มาดำดิ่งกัน!
ซิงค์กับ Async
เมื่อคุณส่งงานไปยังคิว คุณสามารถเลือกทำแบบซิงโครนัสหรือไม่พร้อมกันได้โดยใช้ sync และ async ฟังก์ชั่นการจัดส่ง การซิงค์และอะซิงโครนัสมีผลกับแหล่งที่มาเป็นหลัก ของงานที่ส่ง นั่นคือคิวที่ส่ง จาก .
เมื่อรหัสของคุณถึง sync คำสั่งจะบล็อกคิวปัจจุบันจนกว่างานนั้นจะเสร็จสิ้น เมื่องานคืน/เสร็จสิ้น การควบคุมจะถูกส่งกลับไปยังผู้เรียก และรหัสที่ตามหลัง sync งานจะดำเนินต่อไป
คิดถึง sync มีความหมายเหมือนกันกับ 'การปิดกั้น'
async ในทางกลับกันคำสั่งจะดำเนินการแบบอะซิงโครนัสสำหรับคิวปัจจุบันและส่งคืนการควบคุมกลับไปยังผู้โทรทันทีโดยไม่ต้องรอเนื้อหาของ async ปิดเพื่อดำเนินการ ไม่มีการรับประกันว่ารหัสภายในการปิดแบบ async จะทำงานเมื่อใด
คิวปัจจุบัน?
อาจไม่แน่ชัดว่าต้นทางหรือปัจจุบัน คิวเป็นเพราะมันไม่ได้กำหนดไว้อย่างชัดเจนในโค้ดเสมอไป
ตัวอย่างเช่น หากคุณเรียก sync . ของคุณ คำสั่งภายใน viewDidLoad คิวปัจจุบันของคุณจะเป็นคิวการจัดส่งหลัก หากคุณเรียกใช้ฟังก์ชันเดียวกันภายในตัวจัดการการเสร็จสิ้น URLSession คิวปัจจุบันของคุณจะเป็นคิวเบื้องหลัง
กลับไปที่ sync vs async มาดูตัวอย่างกัน:
DispatchQueue.global().sync {
print("Inside")
}
print("Outside")
// Console output:
// Inside
// Outside โค้ดด้านบนจะบล็อกคิวปัจจุบัน เข้าสู่การปิดและรันโค้ดบนคิวส่วนกลางโดยพิมพ์ "Inside" ก่อนพิมพ์ "Outside" รับประกันคำสั่งซื้อนี้
มาดูกันว่าจะเกิดอะไรขึ้นถ้าเราลอง async แทน:
DispatchQueue.global().async {
print("Inside")
}
print("Outside")
// Potential console output (based on QoS):
// Outside
// Inside ตอนนี้รหัสของเราส่งการปิดไปยังคิวส่วนกลาง จากนั้นดำเนินการเรียกใช้บรรทัดถัดไปทันที มันจะ น่าจะ พิมพ์ "Outside" ก่อน "Inside" แต่ไม่รับประกันการสั่งซื้อนี้ ขึ้นอยู่กับ QoS ของคิวต้นทางและปลายทาง ตลอดจนปัจจัยอื่นๆ ที่ระบบควบคุม
เธรดเป็นรายละเอียดการใช้งานใน GCD — เราไม่สามารถควบคุมเธรดเหล่านั้นได้โดยตรงและจัดการได้โดยใช้การแยกคิวเท่านั้น อย่างไรก็ตาม ฉันคิดว่าอาจมีประโยชน์ที่จะ "แอบดูพฤติกรรมของเธรด" เพื่อทำความเข้าใจความท้าทายที่เราอาจพบกับ GCD
ตัวอย่างเช่น เมื่อคุณส่งงานโดยใช้ sync , GCD เพิ่มประสิทธิภาพการทำงานโดยดำเนินการงานนั้นบนเธรดปัจจุบัน (ผู้โทร)
อย่างไรก็ตาม มีข้อยกเว้นประการหนึ่ง ซึ่งก็คือเมื่อคุณส่งงานการซิงค์ไปยังคิวหลัก— การทำเช่นนั้นจะเป็นการรันงานบนเธรดหลักเสมอ ไม่ใช่ผู้โทร พฤติกรรมนี้อาจมีการแตกสาขาที่เราจะสำรวจในส่วนการผกผันลำดับความสำคัญ

อันไหนน่าใช้
เมื่อส่งงานไปยังคิว Apple แนะนำให้ใช้การดำเนินการแบบอะซิงโครนัสแทนการดำเนินการแบบซิงโครนัส อย่างไรก็ตาม มีบางสถานการณ์ที่ sync อาจเป็นทางเลือกที่ดีกว่า เช่น เมื่อต้องรับมือกับสภาพการแข่งขัน หรือเมื่อทำงานเล็กๆ น้อยๆ ฉันจะอธิบายสถานการณ์เหล่านี้ในไม่ช้า
ผลที่ตามมาใหญ่อย่างหนึ่งของการทำงานแบบอะซิงโครนัสภายในฟังก์ชันคือ ฟังก์ชันไม่สามารถคืนค่าได้โดยตรงอีกต่อไป (หากขึ้นอยู่กับงานแบบอะซิงโครนัสที่กำลังดำเนินการอยู่) ต้องใช้พารามิเตอร์ตัวจัดการการปิด/เสร็จสิ้นเพื่อแสดงผลแทน
เพื่อแสดงแนวคิดนี้ ลองใช้ฟังก์ชันเล็กๆ ที่ยอมรับข้อมูลภาพ ทำการคำนวณราคาแพงเพื่อประมวลผลภาพ แล้วส่งคืนผลลัพธ์:
func processImage(data: Data) -> UIImage? {
guard let image = UIImage(data: data) else { return nil }
// calling an expensive function
let processedImage = upscaleAndFilter(image: image)
return processedImage
}
ในตัวอย่างนี้ ฟังก์ชัน upscaleAndFilter(image:) อาจใช้เวลาหลายวินาที เราจึงต้องการถ่ายลงในคิวแยกต่างหากเพื่อหลีกเลี่ยงการหยุด UI มาสร้างคิวเฉพาะสำหรับการประมวลผลภาพ แล้วส่งฟังก์ชันราคาแพงแบบอะซิงโครนัส:
let imageProcessingQueue = DispatchQueue(label: "com.besher.image-processing")
func processImageAsync(data: Data) -> UIImage? {
guard let image = UIImage(data: data) else { return nil }
imageProcessingQueue.async {
let processedImage = upscaleAndFilter(image: image)
return processedImage
}
}
รหัสนี้มีปัญหาสองประการ ขั้นแรก คำสั่ง return อยู่ภายในการปิดแบบ async ดังนั้นจึงไม่คืนค่าเป็น processImageAsync(data:) อีกต่อไป ฟังก์ชั่นและปัจจุบันไม่มีจุดประสงค์
แต่ปัญหาที่ใหญ่กว่าคือ processImageAsync(data:) . ของเรา ฟังก์ชั่นจะไม่คืนค่าใด ๆ อีกต่อไป เนื่องจากฟังก์ชั่นถึงจุดสิ้นสุดของร่างกายก่อนที่จะเข้าสู่ async ปิด
เพื่อแก้ไขข้อผิดพลาดนี้ เราจะปรับฟังก์ชันเพื่อไม่ให้ส่งกลับค่าโดยตรงอีกต่อไป แต่จะมีพารามิเตอร์ตัวจัดการความสมบูรณ์ใหม่ที่เราสามารถเรียกได้เมื่อฟังก์ชันอะซิงโครนัสของเราทำงานเสร็จแล้ว:
let imageProcessingQueue = DispatchQueue(label: "com.besher.image-processing")
func processImageAsync(data: Data, completion: @escaping (UIImage?) -> Void) {
guard let image = UIImage(data: data) else {
completion(nil)
return
}
imageProcessingQueue.async {
let processedImage = self.upscaleAndFilter(image: image)
completion(processedImage)
}
} ดังที่เห็นได้ชัดในตัวอย่างนี้ การเปลี่ยนแปลงเพื่อทำให้ฟังก์ชันแบบอะซิงโครนัสได้แพร่กระจายไปยังผู้เรียก ซึ่งขณะนี้ต้องผ่านการปิดและจัดการผลลัพธ์แบบอะซิงโครนัสเช่นกัน การแนะนำงานแบบอะซิงโครนัสอาจทำให้คุณปรับเปลี่ยนห่วงโซ่ของฟังก์ชันต่างๆ ได้
การทำงานพร้อมกันและการดำเนินการแบบอะซิงโครนัสจะเพิ่มความซับซ้อนให้กับโครงการของคุณในขณะที่เราเพิ่งสังเกต ทางอ้อมนี้ยังทำให้การดีบักทำได้ยากขึ้น นั่นเป็นเหตุผลว่าทำไมการคิดเกี่ยวกับการทำงานพร้อมกันตั้งแต่เนิ่นๆ จึงคุ้มค่าจริงๆ — ไม่ใช่สิ่งที่คุณต้องการนำไปใช้เมื่อสิ้นสุดรอบการออกแบบ
ในทางตรงกันข้าม การดำเนินการแบบซิงโครนัสไม่ได้เพิ่มความซับซ้อน แต่ช่วยให้คุณสามารถใช้คำสั่ง return ต่อไปได้เหมือนที่เคยทำมา ฟังก์ชันที่มี sync งานจะไม่ส่งคืนจนกว่ารหัสภายในงานนั้นจะเสร็จสมบูรณ์ ดังนั้นจึงไม่ต้องการตัวจัดการความสมบูรณ์
หากคุณกำลังส่งงานเล็กๆ (เช่น อัปเดตค่า) ให้พิจารณาดำเนินการพร้อมกัน ไม่เพียงแต่ช่วยให้คุณรักษาโค้ดของคุณให้เรียบง่ายเท่านั้น แต่ยังทำงานได้ดีขึ้นอีกด้วย — เชื่อกันว่า Async มีค่าใช้จ่ายที่มากกว่าประโยชน์ของการทำงานแบบอะซิงโครนัสสำหรับงานเล็กๆ ที่ใช้เวลาน้อยกว่า 1 มิลลิวินาทีในการดำเนินการให้เสร็จ
อย่างไรก็ตาม หากคุณกำลังส่งงานขนาดใหญ่ เช่น การประมวลผลภาพที่เราดำเนินการข้างต้น ให้พิจารณาทำแบบอะซิงโครนัสเพื่อหลีกเลี่ยงการบล็อกผู้โทรนานเกินไป
กำลังจัดส่งในคิวเดียวกัน
แม้ว่าการส่งงานแบบอะซิงโครนัสจากคิวไปยังตัวเองจะปลอดภัย (เช่น คุณสามารถใช้ .asyncAfter ในคิวปัจจุบัน) คุณไม่สามารถส่งงาน พร้อมกัน จากคิวเข้าคิวเดียวกัน การทำเช่นนั้นจะส่งผลให้แอปหยุดชะงักทันที!
ปัญหานี้อาจเกิดขึ้นได้เมื่อทำการเรียกแบบซิงโครนัสที่นำกลับไปยังคิวเดิม นั่นคือคุณ sync งานไปยังคิวอื่น และเมื่องานเสร็จสิ้น งานจะซิงค์ผลลัพธ์กลับเข้าไปในคิวเดิม นำไปสู่การหยุดชะงัก ใช้ async เพื่อหลีกเลี่ยงปัญหาดังกล่าว
การบล็อกคิวหลัก
ส่งงานพร้อมกัน จาก คิวหลักจะบล็อกคิวนั้น ซึ่งจะทำให้ UI ค้าง จนกว่างานจะเสร็จสิ้น ดังนั้นจึงเป็นการดีกว่าที่จะหลีกเลี่ยงการส่งงานพร้อมกันจากคิวหลัก เว้นแต่คุณจะทำงานที่เบามาก

ซีเรียลเทียบกับพร้อมกัน
ซีเรียล และ พร้อมกัน ส่งผลต่อปลายทาง — คิวที่งานของคุณถูกส่งไปรัน ซึ่งตรงกันข้ามกับ ซิงค์ และ async ซึ่งส่งผลต่อ แหล่งที่มา .
คิวอนุกรมจะไม่ทำงานบนเธรดมากกว่าหนึ่งในแต่ละครั้ง ไม่ว่าคุณจะส่งงานในคิวนั้นกี่งาน ดังนั้นงานจึงรับประกันว่าไม่เพียงแค่เริ่มต้นเท่านั้น แต่ยังยุติการทำงานแบบเข้าก่อน-ออกก่อนด้วย
นอกจากนี้ เมื่อคุณบล็อกคิวซีเรียล (โดยใช้ sync การโทร สัญญาณ หรือเครื่องมืออื่นๆ) การทำงานทั้งหมดในคิวนั้นจะหยุดจนกว่าการบล็อกจะสิ้นสุดลง

คิวพร้อมกันสามารถวางไข่ได้หลายเธรด และระบบจะตัดสินว่าจะสร้างเธรดจำนวนเท่าใด งาน เริ่มต้น ในลำดับ FIFO แต่คิวไม่รอให้งานเสร็จก่อนเริ่มงานถัดไป ดังนั้นงานในคิวพร้อมกันสามารถเสร็จสิ้นในลำดับใดก็ได้
เมื่อคุณดำเนินการคำสั่งบล็อกในคิวที่เกิดขึ้นพร้อมกัน คำสั่งจะไม่บล็อกเธรดอื่นๆ ในคิวนี้ นอกจากนี้ เมื่อคิวพร้อมกันถูกบล็อก จะเสี่ยงต่อการ เธรดระเบิด . ฉันจะอธิบายรายละเอียดเพิ่มเติมในภายหลัง

คิวหลักในแอปของคุณเป็นแบบอนุกรม คิวที่กำหนดไว้ล่วงหน้าทั่วโลกทั้งหมดเกิดขึ้นพร้อมกัน คิวการจัดส่งส่วนตัวที่คุณสร้างจะเป็นแบบอนุกรมโดยค่าเริ่มต้น แต่สามารถตั้งค่าให้ทำงานพร้อมกันได้โดยใช้แอตทริบิวต์ทางเลือกตามที่กล่าวไว้ก่อนหน้านี้
สิ่งสำคัญที่ควรทราบคือแนวคิดของ ซีเรียล เทียบกับ พร้อมกัน เกี่ยวข้องเฉพาะเมื่อกล่าวถึงคิวเฉพาะ คิวทั้งหมดสัมพันธ์กัน ซึ่งกันและกัน .
นั่นคือ หากคุณส่งงานแบบอะซิงโครนัสจากคิวหลักไปยัง ซีเรียลส่วนตัว คิวงานนั้นจะแล้วเสร็จ พร้อมกัน ในส่วนของคิวหลัก และหากคุณสร้างคิวอนุกรมที่แตกต่างกันสองรายการ แล้วทำการบล็อกกับคิวใดรายการหนึ่ง คิวอื่นจะไม่ได้รับผลกระทบ
เพื่อแสดงให้เห็นถึงความพร้อมกันของหลายคิวอนุกรม มาดูตัวอย่างนี้:
let serial1 = DispatchQueue(label: "com.besher.serial1")
let serial2 = DispatchQueue(label: "com.besher.serial2")
serial1.async {
for _ in 0..<5 { print("?") }
}
serial2.async {
for _ in 0..<5 { print("?") }
}
คิวทั้งสองนี้เป็นแบบอนุกรม แต่ผลลัพธ์จะสับสนเพราะทำงานพร้อมกันโดยสัมพันธ์กัน ข้อเท็จจริงที่ว่ามันเป็นแบบอนุกรม (หรือพร้อมกัน) ก็ไม่มีผลกับผลลัพธ์นี้ ระดับ QoS ของพวกเขากำหนดว่าใครโดยทั่วไป เสร็จสิ้นก่อน (ไม่รับประกันคำสั่งซื้อ)
หากเราต้องการให้แน่ใจว่าการวนรอบแรกเสร็จสิ้นก่อนเริ่มการวนรอบที่สอง เราสามารถส่งงานแรกพร้อมกันจากผู้โทรได้:
let serial1 = DispatchQueue(label: "com.besher.serial1")
let serial2 = DispatchQueue(label: "com.besher.serial2")
serial1.sync { // <---- we changed this to 'sync'
for _ in 0..<5 { print("?") }
}
// we don't get here until first loop terminates
serial2.async {
for _ in 0..<5 { print("?") }
}
สิ่งนี้ไม่จำเป็นเสมอไป เพราะตอนนี้เรากำลังบล็อกผู้โทรในขณะที่วงแรกกำลังทำงานอยู่
เพื่อหลีกเลี่ยงการบล็อกผู้โทร เราสามารถส่งงานทั้งสองแบบอะซิงโครนัส แต่ส่งไปยัง เดียวกัน คิวอนุกรม:
let serial = DispatchQueue(label: "com.besher.serial")
serial.async {
for _ in 0..<5 { print("?") }
}
serial.async {
for _ in 0..<5 { print("?") }
}
ตอนนี้งานของเราดำเนินการไปพร้อมกับ ผู้โทร , ในขณะที่ยังรักษาคำสั่งของพวกเขาไว้เหมือนเดิม.
โปรดทราบว่าหากเราสร้างคิวเดี่ยวพร้อมกันผ่านพารามิเตอร์ทางเลือก เราจะกลับไปที่ผลลัพธ์ที่สับสนตามที่คาดไว้:
let concurrent = DispatchQueue(label: "com.besher.concurrent", attributes: .concurrent)
concurrent.async {
for _ in 0..<5 { print("?") }
}
concurrent.async {
for _ in 0..<5 { print("?") }
}
บางครั้งคุณอาจสับสนระหว่างการดำเนินการแบบซิงโครนัสกับการดำเนินการแบบอนุกรม (อย่างน้อยฉันก็ทำ) แต่สิ่งเหล่านี้แตกต่างกันมาก ตัวอย่างเช่น ลองเปลี่ยนการส่งครั้งแรกในบรรทัดที่ 3 จากตัวอย่างก่อนหน้าของเราเป็น sync โทร:
let concurrent = DispatchQueue(label: "com.besher.concurrent", attributes: .concurrent)
concurrent.sync {
for _ in 0..<5 { print("?") }
}
concurrent.async {
for _ in 0..<5 { print("?") }
}
ทันใดนั้นผลลัพธ์ของเราก็กลับมาอยู่ในลำดับที่สมบูรณ์แบบ แต่นี่เป็นคิวที่เกิดขึ้นพร้อมกัน แล้วมันจะเกิดขึ้นได้อย่างไร? sync . หรือเปล่า คำสั่งใด ๆ เปลี่ยนเป็นคิวอนุกรม?
คำตอบคือ ไม่!
นี้ค่อนข้างลับๆล่อๆ สิ่งที่เกิดขึ้นคือเราไปไม่ถึง async เรียกจนกว่างานแรกจะเสร็จสิ้น คิวยังคงเกิดขึ้นพร้อมกันอย่างมาก แต่อยู่ในส่วนการซูมเข้าของโค้ดนี้ ดูเหมือนว่ามันเป็นซีเรียล เนื่องจากเรากำลังบล็อกผู้โทรและไม่ดำเนินการงานต่อไป จนกว่างานแรกจะเสร็จสิ้น
หากคิวอื่นที่อื่นในแอปของคุณพยายามส่งงานไปยังคิวเดียวกันนี้ในขณะที่ยังดำเนินการ sync คำสั่งที่ทำงาน จะ วิ่งไปพร้อมกับอะไรก็ตามที่เราวิ่งมาที่นี่ เพราะมันยังเป็นคิวพร้อมกัน
อันไหนน่าใช้
คิวอนุกรมใช้ประโยชน์จากการเพิ่มประสิทธิภาพ CPU และการแคช และช่วยลดการสลับบริบท
Apple แนะนำให้เริ่มต้นด้วยคิวซีเรียลหนึ่งคิวต่อระบบย่อยในแอปของคุณ — ตัวอย่างเช่น คิวสำหรับเครือข่าย คิวสำหรับการบีบอัดไฟล์ ฯลฯ หากจำเป็น คุณสามารถขยายลำดับชั้นของคิวต่อระบบย่อยได้ในภายหลังโดยใช้เมธอด setTarget หรือเป้าหมายเสริม พารามิเตอร์เมื่อสร้างคิว
หากคุณประสบปัญหาคอขวดด้านประสิทธิภาพ ให้วัดประสิทธิภาพของแอปแล้วดูว่าคิวพร้อมกันช่วยได้หรือไม่ หากคุณไม่เห็นประโยชน์ที่วัดผลได้ ควรใช้คิวซีเรียลดีกว่า
หลุมพราง
การผกผันลำดับความสำคัญและคุณภาพของบริการ
การผกผันของลำดับความสำคัญคือเมื่องานที่มีลำดับความสำคัญสูงถูกป้องกันไม่ให้ทำงานโดยงานที่มีลำดับความสำคัญต่ำกว่า โดยจะสลับลำดับความสำคัญที่สัมพันธ์กันอย่างมีประสิทธิภาพ
สถานการณ์นี้มักเกิดขึ้นเมื่อคิว QoS สูงแบ่งปันทรัพยากรที่มีคิว QoS ต่ำ และคิว QoS ต่ำได้รับการล็อกบนทรัพยากรนั้น
แต่ฉันต้องการครอบคลุมสถานการณ์ที่แตกต่างกันที่เกี่ยวข้องกับการสนทนาของเรามากขึ้น—— เมื่อคุณส่งงานไปยังคิวซีเรียล QoS ที่ต่ำ จากนั้นส่งงาน QoS ที่สูงไปยังคิวเดียวกันนั้น สถานการณ์นี้ยังส่งผลให้เกิดการผกผันของลำดับความสำคัญ เนื่องจากงาน QoS ระดับสูงต้องรอให้งาน QoS ที่ต่ำกว่าจึงจะเสร็จ
GCD แก้ไขการผกผันของลำดับความสำคัญโดยเพิ่ม QoS ของคิวชั่วคราวที่มีงานที่มีลำดับความสำคัญต่ำซึ่งอยู่ "ข้างหน้า" หรือปิดกั้นงานที่มีลำดับความสำคัญสูงของคุณ
เหมือนมีรถติดอยู่ข้างหน้า ของ รถพยาบาล. ทันใดนั้นพวกเขาก็ได้รับอนุญาตให้ข้ามไฟแดงเพียงเพื่อให้รถพยาบาลสามารถเคลื่อนที่ได้ (ในความเป็นจริงรถเคลื่อนไปด้านข้าง แต่ลองนึกภาพถนนแคบ ๆ (ต่อเนื่อง) หรืออะไรก็ตาม :-P)
เพื่อแสดงปัญหาการผกผัน ให้เริ่มด้วยรหัสนี้:
enum Color: String {
case blue = "?"
case white = "⚪️"
}
func output(color: Color, times: Int) {
for _ in 1...times {
print(color.rawValue)
}
}
let starterQueue = DispatchQueue(label: "com.besher.starter", qos: .userInteractive)
let utilityQueue = DispatchQueue(label: "com.besher.utility", qos: .utility)
let backgroundQueue = DispatchQueue(label: "com.besher.background", qos: .background)
let count = 10
starterQueue.async {
backgroundQueue.async {
output(color: .white, times: count)
}
backgroundQueue.async {
output(color: .white, times: count)
}
utilityQueue.async {
output(color: .blue, times: count)
}
utilityQueue.async {
output(color: .blue, times: count)
}
// next statement goes here
} เราสร้างคิวเริ่มต้น (ที่เราส่งงาน จาก ) รวมถึงสองคิวที่มี QoS ต่างกัน จากนั้นเราส่งงานไปยังแต่ละคิวทั้งสองนี้ โดยแต่ละงานจะพิมพ์วงกลมที่มีสีเฉพาะจำนวนเท่ากัน (ยูทิลิตี้ คิว เป็นสีน้ำเงิน พื้นหลัง เป็นสีขาว)
เนื่องจากงานเหล่านี้ถูกส่งแบบอะซิงโครนัส ทุกครั้งที่คุณเรียกใช้แอป คุณจะเห็นผลลัพธ์ที่แตกต่างกันเล็กน้อย อย่างไรก็ตาม ตามที่คุณคาดไว้ คิวที่มี QoS ต่ำกว่า (พื้นหลัง) มักจะสิ้นสุดอยู่เสมอ อันที่จริง วงกลม 10-15 อันสุดท้ายมักจะเป็นสีขาวทั้งหมด

แต่คอยดูว่าจะเกิดอะไรขึ้นเมื่อเราส่งซิงค์ งานไปยังคิวพื้นหลังหลังจากคำสั่ง async ล่าสุด คุณไม่จำเป็นต้องพิมพ์อะไรใน sync คำสั่ง แค่เพิ่มบรรทัดนี้ก็พอ:
// add this after the last async statement,
// still inside starterQueue.async
backgroundQueue.sync {} 
ผลลัพธ์ในคอนโซลพลิกคว่ำ! ตอนนี้ คิวที่มีลำดับความสำคัญสูงกว่า (ยูทิลิตี้) จะอยู่ท้ายสุดเสมอ และ 10–15 วงสุดท้ายจะเป็น สีน้ำเงิน
เพื่อให้เข้าใจถึงสาเหตุที่เกิดขึ้น เราต้องทบทวนข้อเท็จจริงที่ว่าการทำงานแบบซิงโครนัสถูกดำเนินการบนเธรดผู้โทร (เว้นแต่คุณจะส่งไปยังคิวหลัก)
ในตัวอย่างข้างต้น ผู้โทร (starterQueue) มี QoS สูงสุด (userInteractive) ดังนั้น sync ที่ดูเหมือนไม่มีพิษมีภัย งานไม่เพียงแต่บล็อกคิวเริ่มต้นเท่านั้น แต่ยังทำงานบนเธรด QoS ระดับสูงของสตาร์ทเตอร์ด้วย งานจึงทำงานด้วย QoS สูง แต่มีงานอื่นอีกสองงานที่อยู่ข้างหน้าในคิวพื้นหลังเดียวกันที่มี พื้นหลัง QoS ตรวจพบการผกผันของลำดับความสำคัญ!
ตามที่คาดไว้ GCD แก้ไขการผกผันนี้โดยเพิ่ม QoS ของคิวทั้งหมดเพื่อให้ตรงกับงาน QoS ที่สูงชั่วคราว ดังนั้น งานทั้งหมดในคิวเบื้องหลังจึงทำงานที่ การโต้ตอบของผู้ใช้ QoS ซึ่งสูงกว่า ยูทิลิตี้ QoS และนั่นเป็นเหตุผลที่ว่าทำไมงานยูทิลิตี้จึงเสร็จช้า!
หมายเหตุด้านข้าง:หากคุณลบคิวเริ่มต้นออกจากตัวอย่างนั้นและส่งจากคิวหลักแทน คุณจะได้ผลลัพธ์ที่คล้ายคลึงกัน เนื่องจากคิวหลักยังมี การโต้ตอบของผู้ใช้ QoS.
เพื่อหลีกเลี่ยงการผกผันของลำดับความสำคัญในตัวอย่างนี้ เราต้องหลีกเลี่ยงการบล็อกคิวเริ่มต้นด้วย sync คำแถลง. ใช้ async จะแก้ปัญหานั้นได้
แม้ว่าจะไม่เหมาะเสมอไป แต่คุณสามารถลดการผกผันของลำดับความสำคัญได้โดยการยึดติดกับ QoS เริ่มต้นเมื่อสร้างคิวส่วนตัวหรือส่งไปยังคิวพร้อมกันทั่วโลก
ด้ายระเบิด
เมื่อคุณใช้คิวพร้อมกัน คุณจะเสี่ยงต่อการระเบิดของเธรดหากคุณไม่ระวัง กรณีนี้อาจเกิดขึ้นได้เมื่อคุณพยายามส่งงานไปยังคิวพร้อมกันที่ถูกบล็อกอยู่ในปัจจุบัน (เช่น ด้วยสัญญาณ การซิงค์ หรือวิธีอื่นๆ) งานของคุณ จะ ทำงาน แต่ระบบมักจะจบลงด้วยการปั่นเธรดใหม่เพื่อรองรับงานใหม่เหล่านี้ และเธรดก็ไม่ถูก
นี่อาจเป็นสาเหตุที่ Apple แนะนำให้เริ่มต้นด้วยคิวซีเรียลต่อระบบย่อยในแอปของคุณ เนื่องจากแต่ละคิวอนุกรมสามารถใช้เธรดเดียวเท่านั้น โปรดจำไว้ว่าคิวอนุกรมสัมพันธ์กัน ถึงอื่นๆ คิว ดังนั้นคุณจึงยังคงได้รับประโยชน์ด้านประสิทธิภาพเมื่อคุณโอนงานของคุณไปที่คิว แม้ว่าจะไม่ได้เกิดขึ้นพร้อมกัน
สภาพการแข่งขัน
Swift Arrays, Dictionaries, Structs และประเภทค่าอื่นๆ จะไม่ปลอดภัยสำหรับเธรดตามค่าเริ่มต้น ตัวอย่างเช่น เมื่อคุณมีหลายเธรดที่พยายามเข้าถึงและแก้ไข อาร์เรย์เดียวกัน คุณจะเริ่มประสบปัญหา
มีวิธีแก้ปัญหาที่แตกต่างกันสำหรับปัญหาผู้อ่าน-ผู้เขียน เช่น การใช้การล็อกหรือสัญญาณ แต่วิธีแก้ปัญหาที่เกี่ยวข้องที่ฉันต้องการจะพูดถึงในที่นี้คือการใช้คิวการแยก
สมมติว่าเรามีอาร์เรย์ของจำนวนเต็ม และเราต้องการส่งงานแบบอะซิงโครนัสที่อ้างอิงถึงอาร์เรย์นี้ ตราบใดที่งานของเรา อ่าน อาร์เรย์และไม่ปรับเปลี่ยนเราปลอดภัย แต่ทันทีที่เราพยายามแก้ไขอาร์เรย์ในงานอะซิงโครนัสของเรา เราจะแนะนำความไม่เสถียรในแอปของเรา
เป็นปัญหาที่ยุ่งยากเพราะแอปของคุณสามารถทำงานได้ 10 ครั้งโดยไม่มีปัญหา จากนั้นจึงหยุดทำงานเป็นครั้งที่ 11 เครื่องมือหนึ่งที่มีประโยชน์มากสำหรับสถานการณ์นี้คือ Thread Sanitizer ใน Xcode การเปิดใช้งานตัวเลือกนี้จะช่วยให้คุณระบุสภาพการแข่งขันที่อาจเกิดขึ้นในแอปของคุณ

เพื่อแสดงปัญหา ลองมาดูตัวอย่างนี้ (ที่เป็นที่ยอมรับ)
class ViewController: UIViewController {
let concurrent = DispatchQueue(label: "com.besher.concurrent", attributes: .concurrent)
var array = [1,2,3,4,5]
override func viewDidLoad() {
for _ in 0...1 {
race()
}
}
func race() {
concurrent.async {
for i in self.array { // read access
print(i)
}
}
concurrent.async {
for i in 0..<10 {
self.array.append(i) // write access
}
}
}
}
หนึ่งใน async งานกำลังแก้ไขอาร์เรย์โดยการต่อท้ายค่า หากคุณลองใช้สิ่งนี้บนเครื่องจำลอง คุณอาจไม่เกิดความผิดพลาด แต่รันให้เพียงพอ (หรือเพิ่มความถี่ลูปในบรรทัดที่ 7) และในที่สุดคุณจะพัง หากคุณเปิดใช้งานโปรแกรมฆ่าเชื้อเธรด คุณจะได้รับคำเตือนทุกครั้งที่เรียกใช้แอป
เพื่อจัดการกับสภาพการแข่งขันนี้ เราจะเพิ่มคิวการแยกที่ใช้แฟล็กบาเรีย การตั้งค่าสถานะนี้ช่วยให้งานที่ค้างอยู่ในคิวสามารถเสร็จสิ้นได้ แต่จะบล็อกงานอื่นๆ ไม่ให้ดำเนินการจนกว่างานกั้นจะเสร็จสิ้น
ลองนึกถึงสิ่งกีดขวางเช่นภารโรงทำความสะอาดห้องน้ำสาธารณะ (ทรัพยากรที่ใช้ร่วมกัน) มีแผงลอยหลายแห่ง (พร้อมกัน) ในห้องน้ำที่ผู้คนสามารถใช้ได้
เมื่อมาถึง ภารโรงจะวางป้ายทำความสะอาด (สิ่งกีดขวาง) ที่กั้นไม่ให้ผู้มาใหม่เข้ามาจนกว่าการทำความสะอาดจะเสร็จสิ้น แต่ภารโรงจะไม่เริ่มทำความสะอาดจนกว่าคนในนั้นทั้งหมดจะเสร็จธุระ เมื่อทุกคนออกไป ภารโรงก็ดำเนินการทำความสะอาดห้องน้ำสาธารณะแบบแยกส่วน
เมื่อเสร็จแล้ว ภารโรงจะเอาป้าย (สิ่งกีดขวาง) เพื่อให้คนที่ต่อคิวอยู่ข้างนอกเข้าไปได้ในที่สุด
นี่คือสิ่งที่ดูเหมือนในโค้ด:
class ViewController: UIViewController {
let concurrent = DispatchQueue(label: "com.besher.concurrent", attributes: .concurrent)
let isolation = DispatchQueue(label: "com.besher.isolation", attributes: .concurrent)
private var _array = [1,2,3,4,5]
var threadSafeArray: [Int] {
get {
return isolation.sync {
_array
}
}
set {
isolation.async(flags: .barrier) {
self._array = newValue
}
}
}
override func viewDidLoad() {
for _ in 0...15 {
race()
}
}
func race() {
concurrent.async {
for i in self.threadSafeArray {
print(i)
}
}
concurrent.async {
for i in 0..<10 {
self.threadSafeArray.append(i)
}
}
}
} เราได้เพิ่มการแยกคิวใหม่และจำกัดการเข้าถึงอาร์เรย์ส่วนตัวโดยใช้ตัวรับและตัวตั้งค่าที่จะวางอุปสรรคเมื่อแก้ไขอาร์เรย์
ผู้รับต้องเป็น sync เพื่อส่งกลับค่าโดยตรง ตัวตั้งค่าสามารถเป็น async เนื่องจากเราไม่จำเป็นต้องบล็อกผู้โทรในขณะที่กำลังเขียน
เราสามารถใช้คิวอนุกรมโดยไม่มีสิ่งกีดขวางเพื่อแก้ไขสภาวะการแย่งชิง แต่จากนั้นเราจะสูญเสียข้อได้เปรียบของการมีการเข้าถึงการอ่านอาร์เรย์พร้อมกัน บางทีนั่นอาจสมเหตุสมผลในกรณีของคุณ คุณต้องตัดสินใจ
บทสรุป
ขอบคุณมากสำหรับการอ่านมาจนถึงตอนนี้! ฉันหวังว่าคุณจะได้เรียนรู้สิ่งใหม่จากบทความนี้ ฉันจะให้ข้อมูลสรุปและคำแนะนำทั่วไปแก่คุณ:
สรุป
- คิวเสมอ เริ่มต้น งานของพวกเขาในลำดับ FIFO
- คิวมักจะสัมพันธ์กับ อื่นๆ คิว
- ซิงค์ เทียบกับ Async เกี่ยวข้องกับแหล่งที่มา
- ซีเรียล เทียบกับ พร้อมกัน เกี่ยวข้องกับปลายทาง
- การซิงค์มีความหมายเหมือนกันกับ 'การบล็อก'
- Async ส่งคืนการควบคุมไปยังผู้โทรทันที
- ซีเรียลใช้เธรดเดียวและรับประกันลำดับการดำเนินการ
- ใช้หลายเธรดพร้อมกัน และเสี่ยงต่อการระเบิดของเธรด
- นึกถึงการทำงานพร้อมกันตั้งแต่ต้นวงจรการออกแบบของคุณ
- โค้ดแบบซิงโครนัสให้เหตุผลและแก้ปัญหาได้ง่ายขึ้น
- หลีกเลี่ยงการใช้คิวพร้อมกันทั่วโลกหากเป็นไปได้
- ลองเริ่มต้นด้วยคิวซีเรียลต่อระบบย่อย
- เปลี่ยนไปใช้คิวพร้อมกันก็ต่อเมื่อคุณเห็น วัดได้ ผลประโยชน์ด้านประสิทธิภาพ
ฉันชอบคำอุปมาจากประกาศเกี่ยวกับการทำงานพร้อมกันอย่างรวดเร็วของ Swift ที่มี 'เกาะแห่งการทำให้เป็นอันดับในทะเลของการทำงานพร้อมกัน' ความรู้สึกนี้ถูกแบ่งปันในทวีตนี้โดย Matt Diephouse:
เคล็ดลับในการเขียนโค้ดพร้อมกันคือการทำให้ส่วนใหญ่เป็นแบบอนุกรม จำกัดการทำงานพร้อมกันไว้ที่ชั้นนอกขนาดเล็ก (ซีเรียลคอร์, เชลล์พร้อมกัน)
— Matt Diephouse (@mdiep) วันที่ 18 ธันวาคม 2019
เช่น. แทนที่จะใช้ล็อกเพื่อจัดการพร็อพเพอร์ตี้ 5 รายการ ให้สร้างประเภทใหม่ที่รวมไว้และใช้คุณสมบัติเดียวภายในล็อก
เมื่อคุณใช้การทำงานพร้อมกันโดยคำนึงถึงปรัชญานั้น ฉันคิดว่าสิ่งนี้จะช่วยให้คุณบรรลุรหัสที่ทำงานพร้อมกันซึ่งสามารถให้เหตุผลได้โดยไม่หลงทางในการติดต่อกลับที่ยุ่งเหยิง
หากคุณมีคำถามหรือความคิดเห็น โปรดติดต่อฉันได้ที่ Twitter
เบเชอร์ อัล มาเลห์
ภาพปกโดย Onur K บน Unsplash
ดาวน์โหลดแอปที่แสดงร่วมที่นี่:
แอป almaleh/DispatcherCompanion สำหรับบทความของฉันเกี่ยวกับการทำงานพร้อมกัน มีส่วนร่วมในการพัฒนา almaleh/Dispatcher โดยการสร้างบัญชีบน GitHub
almalehGitHub
ดูบทความอื่นๆ ของฉัน:
Fireworks — โปรแกรมแก้ไขภาพอนุภาคสำหรับ SwiftGenerate Swift code ได้ทันทีสำหรับ macOS และ iOS ในขณะที่คุณออกแบบและทำซ้ำกับเอฟเฟกต์อนุภาคของคุณ
Besher Al MalehFlawless iOS
คุณไม่จำเป็นต้อง (เสมอ) ต้องการ [ตัวตนที่อ่อนแอ] ในบทความนี้ เราจะพูดถึงตัวตนที่อ่อนแอ ด้านในของฝาปิด Swift เพื่อหลีกเลี่ยงรอบการรักษา &สำรวจกรณีที่อาจจำเป็นหรือไม่จำเป็นต้องจับภาพตัวเองอย่างอ่อนแอ
Besher Al MalehFlawless iOS
Further reading:
IntroductionExplains how to implement concurrent code paths in an application.Concurrent Programming:APIs and Challenges · objc.ioobjc.io publishes books on advanced techniques and practices for iOS and OS X development
Florian Kugler
Low-Level Concurrency APIs · objc.ioobjc.io publishes books on advanced techniques and practices for iOS and OS X development
Daniel Eggert http://khanlou.com/2016/04/the-GCD-handbook/
Concurrent vs serial queues in GCDI’m struggling to fully understand the concurrent and serial queues in GCD. I have some issues and hoping someone can answer me clearly and at the point.I’m reading that serial queues are created...
Bogdan AlexandruStack Overflow
WWDC Videos:
Modernizing Grand Central Dispatch Usage - WWDC 2017 - Videos - Apple DevelopermacOS 10.13 and iOS 11 have reinvented how Grand Central Dispatch and the Darwin kernel collaborate, enabling your applications to run...
Apple Developer
Building Responsive and Efficient Apps with GCD - WWDC 2015 - Videos - Apple DeveloperwatchOS and iOS Multitasking place increased demands on your application’s efficiency and responsiveness. With expert guidance from the...
Apple Developer