การทำงานพร้อมกันใน 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 iOSFurther 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 Eggerthttps://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