การทำงานพร้อมกันขั้นสูงใน Swift ด้วย HoneyBee
เผยแพร่แล้ว: 2022-03-11การออกแบบ ทดสอบ และบำรุงรักษาอัลกอริธึมที่ทำงานพร้อมกันใน Swift นั้นยาก และการได้รับรายละเอียดที่ถูกต้องเป็นสิ่งสำคัญอย่างยิ่งต่อความสำเร็จของแอพของคุณ อัลกอริธึมที่ทำงานพร้อมกัน (เรียกอีกอย่างว่าการเขียนโปรแกรมแบบขนาน) เป็นอัลกอริธึมที่ออกแบบมาเพื่อดำเนินการหลาย ๆ อย่าง (อาจจะหลาย ๆ อย่าง) พร้อมกันเพื่อใช้ประโยชน์จากทรัพยากรฮาร์ดแวร์มากขึ้นและลดเวลาดำเนินการโดยรวม
บนแพลตฟอร์มของ Apple วิธีดั้งเดิมในการเขียนอัลกอริทึมพร้อมกันคือ NSOperation การออกแบบ NSOperation เชิญชวนให้โปรแกรมเมอร์แยกย่อยอัลกอริทึมที่ทำงานพร้อมกันเป็นงานแบบอะซิงโครนัสที่ใช้เวลานานแต่ละรายการ แต่ละงานจะถูกกำหนดในคลาสย่อยของ NSOperation และอินสแตนซ์ของคลาสเหล่านั้นจะถูกรวมเข้าด้วยกันผ่าน API วัตถุประสงค์เพื่อสร้างลำดับงานบางส่วนที่รันไทม์ วิธีการออกแบบอัลกอริธึมที่ทำงานพร้อมกันนี้เป็นความล้ำหน้าบนแพลตฟอร์มของ Apple เป็นเวลาเจ็ดปี
ในปี 2014 Apple ได้เปิดตัว Grand Central Dispatch (GCD) เพื่อเป็นความก้าวหน้าอย่างมากในการแสดงออกถึงการทำงานพร้อมกัน GCD พร้อมด้วยบล็อกคุณลักษณะภาษาใหม่ที่มาพร้อมกับและขับเคลื่อน ให้วิธีการอธิบายตัวจัดการการตอบสนองแบบอะซิงโครนัสอย่างกระชับทันทีหลังจากเริ่มต้นคำขอ async ที่เริ่มต้น ไม่ได้รับการสนับสนุนให้โปรแกรมเมอร์กระจายคำจำกัดความของงานที่เกิดขึ้นพร้อมกันในหลายไฟล์ในคลาสย่อย NSOperation จำนวนมากอีกต่อไป ตอนนี้ อัลกอริธึมที่ทำงานพร้อมกันทั้งหมดสามารถเขียนได้ภายในวิธีเดียว การแสดงออกที่เพิ่มขึ้นและความปลอดภัยของประเภทเป็นการเปลี่ยนแปลงทางแนวคิดที่สำคัญ อัลกอริทึมทั่วไปของวิธีการเขียนนี้อาจมีลักษณะดังนี้:
func processImageData(completion: (result: Image?, error: Error?) -> Void) { loadWebResource("dataprofile.txt") { (dataResource, error) in guard let dataResource = dataResource else { completion(nil, error) return } loadWebResource("imagedata.dat") { (imageResource, error) in guard let imageResource = imageResource else { completion(nil, error) return } decodeImage(dataResource, imageResource) { (imageTmp, error) in guard let imageTmp = imageTmp else { completion(nil, error) return } dewarpAndCleanupImage(imageTmp) { imageResult in guard let imageResult = imageResult else { completion(nil, error) return } completion(imageResult, nil) } } } } }
ลองแยกอัลกอริทึมนี้ออกเล็กน้อย ฟังก์ชัน processImageData
เป็นฟังก์ชันแบบอะซิงโครนัสที่ทำให้การเรียกแบบอะซิงโครนัสสี่ตัวเป็นของตัวเองเพื่อให้งานเสร็จสมบูรณ์ การเรียกใช้ async สี่รายการซ้อนอยู่ภายในอีกอันหนึ่งในลักษณะที่เป็นธรรมชาติที่สุดสำหรับการจัดการ async แบบบล็อก บล็อคผลลัพธ์แต่ละรายการมีพารามิเตอร์ Error ที่เป็นตัวเลือก และทั้งหมดนี้มีพารามิเตอร์ทางเลือกเพิ่มเติมซึ่งระบุถึงผลลัพธ์ของการดำเนินการ aysnc
รูปร่างของบล็อกโค้ดด้านบนอาจดูเหมือนคุ้นเคยกับนักพัฒนา Swift ส่วนใหญ่ แต่วิธีการนี้ผิดอย่างไร? รายการจุดปวดต่อไปนี้น่าจะคุ้นเคยพอๆ กัน
- รูปร่าง "พีระมิดแห่งความพินาศ" ของบล็อกโค้ดที่ซ้อนกันนี้สามารถเทอะทะได้อย่างรวดเร็ว จะเกิดอะไรขึ้นหากเราเพิ่มการดำเนินการ async อีกสองรายการ สี่? แล้วการดำเนินการตามเงื่อนไขล่ะ? ลองใหม่พฤติกรรมหรือการป้องกันการจำกัดทรัพยากรเป็นอย่างไร รหัสโลกแห่งความจริงไม่เคยสะอาดและเรียบง่ายเหมือนตัวอย่างในโพสต์บล็อก เอฟเฟกต์ “พีระมิดแห่งความพินาศ” สามารถส่งผลให้โค้ดอ่านยาก ดูแลรักษายาก และมีแนวโน้มที่จะเกิดข้อผิดพลาดได้ง่าย
- ความพยายามในการจัดการข้อผิดพลาดในตัวอย่างข้างต้น แม้ว่าที่จริงแล้ว Swifty จะยังไม่สมบูรณ์ โปรแกรมเมอร์สันนิษฐานว่าบล็อกการเรียกกลับแบบอะซิงโครนัสแบบ Objective-C แบบสองพารามิเตอร์จะมีพารามิเตอร์หนึ่งในสองพารามิเตอร์เสมอ พวกเขาจะไม่มีวันเป็นศูนย์ในเวลาเดียวกัน นี่ไม่ใช่สมมติฐานที่ปลอดภัย อัลกอริธึมที่ทำงานพร้อมกันนั้นขึ้นชื่อในเรื่องการเขียนและแก้ไขจุดบกพร่องได้ยาก และการสันนิษฐานที่ไม่มีมูลก็เป็นส่วนหนึ่งของเหตุผล การจัดการข้อผิดพลาดที่สมบูรณ์และถูกต้องเป็นสิ่งจำเป็นที่หลีกเลี่ยงไม่ได้สำหรับอัลกอริธึมที่ทำงานพร้อมกันซึ่งตั้งใจจะใช้งานในโลกแห่งความเป็นจริง
- นำความคิดนี้ให้ดียิ่งขึ้น บางทีโปรแกรมเมอร์ที่เขียนฟังก์ชัน async ที่เรียกว่าไม่ได้มีหลักการเหมือนคุณ เกิดอะไรขึ้นถ้ามีเงื่อนไขที่ฟังก์ชันที่เรียกไม่สามารถโทรกลับได้? หรือโทรกลับมากกว่าหนึ่งครั้ง? เกิดอะไรขึ้นกับความถูกต้องของ processImageData ภายใต้สถานการณ์เหล่านี้ มืออาชีพไม่ฉวยโอกาส หน้าที่ที่สำคัญต่อภารกิจจะต้องถูกต้องแม้ว่าจะอาศัยหน้าที่ที่เขียนโดยบุคคลที่สามก็ตาม
- บางทีอัลกอริธึม async ที่พิจารณาแล้วอาจมีความน่าสนใจมากที่สุดนั้นถูกสร้างขึ้นมาอย่างไม่เหมาะสม การดำเนินการ async สองรายการแรกเป็นการดาวน์โหลดทรัพยากรระยะไกลทั้งคู่ แม้ว่าจะไม่มีการพึ่งพาอาศัยกัน แต่อัลกอริธึมด้านบนจะดำเนินการดาวน์โหลดตามลำดับและไม่ขนานกัน เหตุผลนี้ชัดเจน ไวยากรณ์ของบล็อกที่ซ้อนกันส่งเสริมความสิ้นเปลืองดังกล่าว ตลาดที่มีการแข่งขันไม่ทนต่อความล่าช้าโดยไม่จำเป็น หากแอปของคุณไม่ดำเนินการแบบอะซิงโครนัสโดยเร็วที่สุด แอปอื่นจะดำเนินการ
เราจะทำได้ดีกว่านี้ได้อย่างไร? HoneyBee เป็นไลบรารีฟิวเจอร์ส/สัญญาที่ทำให้การเขียนโปรแกรมพร้อมกัน Swift ง่าย แสดงออก และปลอดภัย มาเขียนอัลกอริทึม async ด้านบนใหม่กับ HoneyBee และตรวจสอบผลลัพธ์:
func processImageData(completion: (result: Image?, error: Error?) -> Void) { HoneyBee.start() .setErrorHandler { completion(nil, $0) } .branch { stem in stem.chain(loadWebResource =<< "dataprofile.txt") + stem.chain(loadWebResource =<< "imagedata.dat") } .chain(decodeImage) .chain(dewarpAndCleanupImage) .chain { completion($0, nil) } }
บรรทัดแรกที่เริ่มใช้งานนี้คือสูตรใหม่ของ HoneyBee บรรทัดที่สองสร้างตัวจัดการข้อผิดพลาดเริ่มต้น การจัดการข้อผิดพลาดไม่ได้เป็นทางเลือกในสูตรของ HoneyBee หากมีสิ่งผิดปกติเกิดขึ้น อัลกอริธึมจะต้องจัดการกับมัน บรรทัดที่สามเปิดสาขาที่อนุญาตให้ดำเนินการแบบขนาน loadWebResource
สองสายจะดำเนินการพร้อมกันและผลลัพธ์จะถูกรวมเข้าด้วยกัน (บรรทัดที่ 5) ค่าที่รวมกันของทรัพยากรที่โหลดทั้งสองจะถูกส่งต่อไปยัง decodeImage
และต่อเนื่องไปเรื่อยๆ จนกว่าจะเสร็จสิ้น
มาดูรายการจุดปวดด้านบนและดูว่า HoneyBee ได้ปรับปรุงโค้ดนี้อย่างไร การรักษาฟังก์ชันนี้ทำได้ง่ายขึ้นอย่างมาก สูตร HoneyBee ดูเหมือนอัลกอริทึมที่แสดงออกมา รหัสสามารถอ่านได้ เข้าใจได้ และแก้ไขได้อย่างรวดเร็ว การออกแบบของ HoneyBee ช่วยให้มั่นใจได้ว่าการสั่งคำสั่งผิดพลาดส่งผลให้เกิดข้อผิดพลาดในการคอมไพล์ ไม่ใช่ข้อผิดพลาดรันไทม์ ฟังก์ชันนี้มีความอ่อนไหวต่อจุดบกพร่องและข้อผิดพลาดของมนุษย์น้อยลงมาก
ข้อผิดพลาดรันไทม์ที่เป็นไปได้ทั้งหมดได้รับการจัดการอย่างสมบูรณ์ ทุกฟังก์ชั่นลายเซ็นที่ HoneyBee รองรับ (มี 38 รายการ) มั่นใจได้ว่าจะได้รับการจัดการอย่างเต็มที่ ในตัวอย่างของเรา การเรียกกลับแบบสองพารามิเตอร์ของสไตล์ Objective-C จะสร้างข้อผิดพลาดที่ไม่ใช่ศูนย์ซึ่งจะถูกส่งต่อไปยังตัวจัดการข้อผิดพลาด หรือจะสร้างค่าที่ไม่ใช่ศูนย์ซึ่งจะคืบหน้าตามสาย หรือหากทั้งสองอย่าง ค่าเป็นศูนย์ HoneyBee จะสร้างข้อผิดพลาดที่อธิบายว่าการเรียกกลับของฟังก์ชันไม่เป็นไปตามสัญญา
HoneyBee ยังจัดการความถูกต้องตามสัญญาสำหรับจำนวนครั้งที่เรียกใช้ฟังก์ชันเรียกกลับ หากฟังก์ชันไม่สามารถเรียกใช้การเรียกกลับได้ HoneyBee จะสร้างความล้มเหลวเชิงพรรณนา หากฟังก์ชันเรียกใช้การเรียกกลับมากกว่าหนึ่งครั้ง HoneyBee จะระงับการเรียกใช้เสริมและคำเตือนบันทึก การตอบสนองข้อบกพร่องทั้งสองนี้ (และอื่น ๆ ) สามารถปรับแต่งให้เข้ากับความต้องการส่วนบุคคลของโปรแกรมเมอร์ได้
หวังว่าจะเป็นที่แน่ชัดแล้วว่ารูปแบบของ processImageData
นี้จะทำการดาวน์โหลดทรัพยากรแบบคู่ขนานกันอย่างเหมาะสมเพื่อให้เกิดประสิทธิภาพสูงสุด เป้าหมายการออกแบบที่แข็งแกร่งที่สุดอย่างหนึ่งของ HoneyBee คือสูตรควรมีลักษณะเหมือนอัลกอริธึมที่แสดงออกมา
ดีขึ้นมาก ใช่ไหม? แต่ HoneyBee มีอะไรอีกมากมายที่จะนำเสนอ
ได้รับการเตือน: กรณีศึกษาต่อไปไม่เหมาะสำหรับคนใจไม่สู้ พิจารณาคำอธิบายปัญหาต่อไปนี้: แอพมือถือของคุณใช้ CoreData
เพื่อรักษาสถานะ คุณมีโมเดล NSManagedObject
ชื่อ Media ซึ่งแสดงถึงเนื้อหาสื่อที่อัปโหลดไปยังเซิร์ฟเวอร์ส่วนหลังของคุณ ผู้ใช้จะได้รับอนุญาตให้เลือกรายการสื่อหลายสิบรายการพร้อมกันและอัปโหลดเป็นชุดไปยังระบบแบ็กเอนด์ สื่อจะแสดงครั้งแรกผ่านสตริงอ้างอิง ซึ่งต้องแปลงเป็นวัตถุสื่อ โชคดีที่แอปของคุณมีวิธีช่วยเหลือที่ทำอย่างนั้นอยู่แล้ว:
func export(_ mediaRef: String, completion: @escaping (Media?, Error?) -> Void) { // transcoding stuff completion(Media(context: managedObjectContext), nil) }
หลังจากแปลงการอ้างอิงสื่อเป็นวัตถุสื่อ คุณต้องอัปโหลดรายการสื่อไปยังส่วนหลัง คุณมีฟังก์ชันตัวช่วยที่พร้อมทำสิ่งเครือข่ายอีกครั้ง
func upload(_ media: Media, completion: @escaping (Error?) -> Void) { // network stuff completion(nil) }
เนื่องจากผู้ใช้สามารถเลือกรายการสื่อได้หลายสิบรายการในคราวเดียว นักออกแบบ UX ได้ระบุความคิดเห็นจำนวนมากพอสมควรเกี่ยวกับความคืบหน้าในการอัปโหลด ความต้องการได้รับการกลั่นเป็นสี่หน้าที่ดังต่อไปนี้:
/// Called if anything goes wrong in the upload func errorHandler(_ error: Error) { // do the right thing } /// Called once per mediaRef, after either a successful or unsuccessful upload func singleUploadCompletion(_ mediaRef: String) { // update a progress indicator } /// Called once per successful upload func singleUploadSuccess(_ media: Media) { // do celebratory things } /// Called if the entire batch was considered to be uploaded successfully. func totalProcessSuccess() { // declare victory }
อย่างไรก็ตาม เนื่องจากแอปของคุณเป็นแหล่งอ้างอิงสื่อที่บางครั้งอาจหมดอายุ ผู้จัดการธุรกิจจึงตัดสินใจส่งข้อความ "ความสำเร็จ" ให้กับผู้ใช้หากการอัปโหลดอย่างน้อยครึ่งหนึ่งประสบความสำเร็จ กล่าวคือ กระบวนการที่เกิดขึ้นพร้อมกันควรประกาศชัยชนะ—และเรียก totalProcessSuccess
หากความพยายามอัปโหลดล้มเหลวน้อยกว่าครึ่ง นี่คือข้อกำหนดที่มอบให้คุณในฐานะนักพัฒนา แต่ในฐานะโปรแกรมเมอร์ที่มีประสบการณ์ คุณตระหนักดีว่ายังมีข้อกำหนดอื่นๆ ที่ต้องนำไปใช้
แน่นอน ธุรกิจต้องการให้การอัปโหลดเป็นชุดเกิดขึ้นโดยเร็วที่สุด ดังนั้นการอัปโหลดแบบอนุกรมจึงไม่เป็นปัญหา การอัปโหลดจะต้องดำเนินการควบคู่กันไป
แต่ไม่มากเกินไป หากคุณไม่ซิงโคร async
ซ์ทั้งกลุ่มโดยไม่ได้ตั้งใจ การอัปโหลดหลายสิบรายการพร้อมกันจะทำให้ NIC บนมือถือ (การ์ดอินเทอร์เฟซเครือข่าย) ล่ม และการอัปโหลดจะดำเนินการช้ากว่าแบบต่อเนื่องจริง ๆ ไม่เร็วขึ้น
การเชื่อมต่อเครือข่ายมือถือไม่ถือว่าเสถียร แม้แต่ธุรกรรมสั้น ๆ ก็อาจล้มเหลวเนื่องจากการเปลี่ยนแปลงในการเชื่อมต่อเครือข่ายเท่านั้น ในการประกาศว่าการอัปโหลดล้มเหลวอย่างแท้จริง เราจำเป็นต้องลองอัปโหลดอีกครั้งอย่างน้อยหนึ่งครั้ง
นโยบายการลองใหม่ไม่ควรรวมการดำเนินการส่งออก เนื่องจากไม่อยู่ภายใต้ความล้มเหลวชั่วคราว
กระบวนการเอ็กซ์พอร์ตเป็นแบบผูกกับคอมพิวเตอร์ ดังนั้นจึงต้องดำเนินการนอกเธรดหลัก
เนื่องจากการส่งออกมีการประมวลผลแบบผูกมัด จึงควรมีจำนวนอินสแตนซ์ที่เกิดขึ้นพร้อมกันน้อยกว่ากระบวนการอัปโหลดที่เหลือเพื่อหลีกเลี่ยงการรบกวนตัวประมวลผล
ฟังก์ชันการโทรกลับทั้งสี่ที่อธิบายไว้ข้างต้นทั้งหมดจะอัปเดต UI และต้องเรียกใช้ทั้งหมดในเธรดหลัก
สื่อคือ NSManagedObject
ซึ่งมาจาก NSManagedObjectContext
และมีข้อกำหนดเกี่ยวกับเธรดของตัวเองซึ่งต้องได้รับการปฏิบัติตาม
สเปคของปัญหานี้ดูคลุมเครือหรือไม่? อย่าแปลกใจถ้าคุณพบปัญหาเช่นนี้ในอนาคตของคุณ เคยเจอแบบนี้ในงานของตัวเอง อันดับแรก ให้ลองแก้ปัญหานี้ด้วยเครื่องมือแบบเดิมๆ หัวเข็มขัดขึ้นจะไม่สวย
/// An enum describing specific problems that the algorithm might encounter. enum UploadingError : Error { case invalidResponse case tooManyFailures } /// A semaphore to prevent flooding the NIC let outerLimit = DispatchSemaphore(value: 4) /// A semaphore to prevent thrashing the processor let exportLimit = DispatchSemaphore(value: 1) /// The number of times to retry the upload if it fails let uploadRetries = 1 /// Dispatch group to keep track of when the entire process is finished let fullProcessDispatchGroup = DispatchGroup() /// How many of the uploads fully completed. var uploadSuccesses = 0 // this notify block is called when the full process has completed. fullProcessDispatchGroup.notify(queue: DispatchQueue.main) { let successRate = Float(uploadSuccesses) / Float(mediaReferences.count) if successRate > 0.5 { totalProcessSuccess() } else { errorHandler(UploadingError.tooManyFailures) } } // start in the background DispatchQueue.global().async { for mediaRef in mediaReferences { // alert the group that we're starting a process fullProcessDispatchGroup.enter() // wait until it's safe to start uploading outerLimit.wait() /// common cleanup operations needed later func finalizeMediaRef() { singleUploadCompletion(mediaRef) fullProcessDispatchGroup.leave() outerLimit.signal() } // wait until it's safe to start exporting exportLimit.wait() export(mediaRef) { (media, error) in // allow another export to begin exportLimit.signal() if let error = error { DispatchQueue.main.async { errorHandler(error) finalizeMediaRef() } } else { guard let media = media else { DispatchQueue.main.async { errorHandler(UploadingError.invalidResponse) finalizeMediaRef() } return } // the export was successful var uploadAttempts = 0 /// define the upload process and its retry behavior func doUpload() { // respect Media's threading requirements managedObjectContext.perform { upload(media) { error in if let error = error { if uploadAttempts < uploadRetries { uploadAttempts += 1 doUpload() // retry } else { DispatchQueue.main.async { // too many upload failures errorHandler(error) finalizeMediaRef() } } } else { DispatchQueue.main.async { uploadSuccesses += 1 singleUploadSuccess(media) finalizeMediaRef() } } } } } // kick off the first upload doUpload() } } } }
ว้าว! ไม่มีความคิดเห็น นั่นคือประมาณ 75 บรรทัด คุณทำตามเหตุผลตลอดทางหรือไม่? คุณจะรู้สึกอย่างไรถ้าคุณเจอสัตว์ประหลาดตัวนี้ในสัปดาห์แรกในงานใหม่? คุณพร้อมจะดูแลมันหรือปรับเปลี่ยนมันไหม? คุณจะรู้ว่ามันมีข้อผิดพลาดหรือไม่? มันมีข้อผิดพลาดหรือไม่?

ตอนนี้ พิจารณาทางเลือกของ HoneyBee:
HoneyBee.start(on: DispatchQueue.main) .setErrorHandler(errorHandler) .insert(mediaReferences) .setBlockPerformer(DispatchQueue.global()) .each(limit: 4, acceptableFailure: .ratio(0.5)) { elem in elem.finally { link in link.setBlockPerformer(DispatchQueue.main) .chain(singleUploadCompletion) } .limit(1) { link in link.chain(export) } .setBlockPerformer(managedObjectContext) .retry(1) { link in link.chain(upload) // subject to transient failure } .setBlockPerformer(DispatchQueue.main) .chain(singleUploadSuccess) } .setBlockPerformer(DispatchQueue.main) .drop() .chain(totalProcessSuccess)
แบบฟอร์มนี้ตีคุณอย่างไร? มาทำงานทีละชิ้นกันเถอะ ในบรรทัดแรก เราเริ่มสูตร HoneyBee โดยเริ่มจากหัวข้อหลัก โดยการเริ่มต้นในเธรดหลัก เรามั่นใจว่าข้อผิดพลาดทั้งหมดจะถูกส่งไปยัง errorHandler (บรรทัดที่ 2) บนเธรดหลัก บรรทัดที่ 3 แทรกอาร์เรย์ mediaReferences
ลงในห่วงโซ่กระบวนการ ต่อไป เราเปลี่ยนไปใช้คิวพื้นหลังส่วนกลางเพื่อเตรียมพร้อมสำหรับการขนานกัน ในบรรทัดที่ 5 เราเริ่มต้นการวนซ้ำแบบคู่ขนานกับแต่ละ mediaReferences
เราจำกัดความขนานนี้ให้สูงสุด 4 การดำเนินการพร้อมกัน นอกจากนี้เรายังประกาศด้วยว่าการทำซ้ำแบบเต็มจะถือว่าสำเร็จหากอย่างน้อยครึ่งหนึ่งของ subchains สำเร็จ (อย่าผิดพลาด) บรรทัดที่ 6 ประกาศลิงค์ finally
ซึ่งจะถูกเรียกไม่ว่า subchain ด้านล่างจะสำเร็จหรือล้มเหลว ในลิงก์ finally
เราสลับไปที่เธรดหลัก (บรรทัดที่ 7) และเรียก singleUploadCompletion
(บรรทัดที่ 8) ในบรรทัดที่ 10 เราตั้งค่าการขนานสูงสุด 1 (การดำเนินการครั้งเดียว) รอบการดำเนินการส่งออก (บรรทัดที่ 11) บรรทัดที่ 13 สลับไปยังคิวส่วนตัวที่เป็นของอินสแตนซ์ managedObjectContext
ของเรา บรรทัดที่ 14 ประกาศการลองใหม่อีกครั้งสำหรับการดำเนินการอัปโหลด (บรรทัดที่ 15) Line 17 สลับไปที่เธรดหลักอีกครั้งและ 18 เรียกใช้ singleUploadSuccess
เมื่อถึงเส้นเวลา 20 การวนซ้ำแบบคู่ขนานทั้งหมดเสร็จสิ้นแล้ว หากการวนซ้ำล้มเหลวน้อยกว่าครึ่ง บรรทัดที่ 20 จะสลับไปที่คิวหลักเป็นครั้งสุดท้าย (เรียกคืนแต่ละรายการถูกเรียกใช้บนคิวพื้นหลัง) 21 จะลดค่าขาเข้า (ยังคง mediaReferences
) และ 22 เรียกใช้ totalProcessSuccess
แบบฟอร์มของ HoneyBee นั้นชัดเจนกว่า สะอาดกว่า และอ่านง่ายกว่า และยังดูแลรักษาง่ายกว่าอีกด้วย จะเกิดอะไรขึ้นกับรูปแบบยาวของอัลกอริธึมนี้หากจำเป็นต้องวนซ้ำเพื่อรวมออบเจ็กต์สื่อเข้าในอาร์เรย์เช่นฟังก์ชันแผนที่ หลังจากที่คุณทำการเปลี่ยนแปลงแล้ว คุณจะมั่นใจเพียงใดว่ายังคงเป็นไปตามข้อกำหนดของอัลกอริทึมทั้งหมด ในรูปแบบ HoneyBee การเปลี่ยนแปลงนี้จะแทนที่แต่ละรายการด้วยแผนที่เพื่อใช้ฟังก์ชันแผนที่คู่ขนาน (ใช่ ลดแล้วด้วย)
HoneyBee เป็นไลบรารีฟิวเจอร์สที่ทรงพลังสำหรับ Swift ที่ทำให้การเขียนอัลกอริธึมแบบอะซิงโครนัสและพร้อมกันง่ายขึ้น ปลอดภัยขึ้น และแสดงออกมากขึ้น ในบทความนี้ เราได้เห็นว่า HoneyBee สามารถทำให้อัลกอริทึมของคุณง่ายต่อการบำรุงรักษา ถูกต้องมากขึ้น และเร็วขึ้นได้อย่างไร HoneyBee ยังรองรับกระบวนทัศน์ async ที่สำคัญอื่นๆ เช่น การสนับสนุนการลองใหม่ ตัวจัดการข้อผิดพลาดหลายตัว การปกป้องทรัพยากร และการประมวลผลการรวบรวม (รูปแบบ async ของแผนที่ ตัวกรอง และลด) สำหรับรายการคุณสมบัติทั้งหมด โปรดดูที่เว็บไซต์ หากต้องการเรียนรู้เพิ่มเติมหรือถามคำถาม โปรดดูที่ฟอรัมชุมชนใหม่ล่าสุด
ภาคผนวก: รับรองความถูกต้องตามสัญญาของฟังก์ชัน Async
การตรวจสอบความถูกต้องตามสัญญาของฟังก์ชันถือเป็นหลักการพื้นฐานของวิทยาการคอมพิวเตอร์ มากเสียจนคอมไพเลอร์สมัยใหม่แทบทั้งหมดมีการตรวจสอบเพื่อให้แน่ใจว่าฟังก์ชันที่ประกาศคืนค่าจะส่งกลับเพียงครั้งเดียว การส่งคืนน้อยกว่าหรือมากกว่าหนึ่งครั้งจะถือเป็นข้อผิดพลาด และป้องกันการรวบรวมทั้งหมดอย่างเหมาะสม
แต่ความช่วยเหลือคอมไพเลอร์นี้มักใช้ไม่ได้กับฟังก์ชันแบบอะซิงโครนัส พิจารณาตัวอย่าง (ขี้เล่น) ต่อไปนี้:
func generateIcecream(from int: Int, completion: (String) -> Void) { if int > 5 { if int < 20 { completion("Chocolate") } else if int < 10 { completion("Strawberry") } completion("Pistachio") } else if int < 2 { completion("Vanilla") } }
ฟังก์ชัน generateIcecream
ยอมรับ Int และส่งคืนสตริงแบบอะซิงโครนัส คอมไพเลอร์แบบรวดเร็วยอมรับแบบฟอร์มข้างต้นอย่างมีความสุขว่าถูกต้อง แม้ว่าจะมีปัญหาที่ชัดเจนอยู่บ้าง เมื่อได้รับอินพุตบางอย่าง ฟังก์ชันนี้อาจเรียกความสมบูรณ์เป็นศูนย์ หนึ่งหรือสองครั้ง โปรแกรมเมอร์ที่เคยทำงานกับฟังก์ชัน async มักจะนึกถึงตัวอย่างของปัญหานี้ในงานของตนเอง พวกเราทำอะไรได้บ้าง? แน่นอน เราสามารถจัดโครงสร้างโค้ดใหม่เพื่อให้เรียบร้อยยิ่งขึ้น (สวิตช์ที่มีตัวพิมพ์ช่วงจะทำงานที่นี่) แต่บางครั้งความซับซ้อนในการใช้งานก็ลดได้ยาก จะดีกว่าไหมถ้าคอมไพเลอร์สามารถช่วยเราในการตรวจสอบความถูกต้องเหมือนกับที่ทำกับฟังก์ชันที่ส่งคืนเป็นประจำ
ปรากฎว่ามีวิธี สังเกตคาถา Swifty ต่อไปนี้:
func generateIcecream(from int: Int, completion: (String) -> Void) { let finalResult: String defer { completion(finalResult) } let completion: Void = Void() defer { completion } if int > 5 { if int < 20 { completion("Chocolate") } else if int < 10 { completion("Strawberry") } // else completion("Pistachio") } else if int < 2 { completion("Vanilla") } }
สี่บรรทัดที่แทรกที่ด้านบนของฟังก์ชันนี้บังคับให้คอมไพเลอร์ตรวจสอบว่าการเรียกกลับที่เสร็จสมบูรณ์นั้นถูกเรียกใช้เพียงครั้งเดียว ซึ่งหมายความว่าฟังก์ชันนี้จะไม่คอมไพล์อีกต่อไป เกิดอะไรขึ้น? ในบรรทัดแรก เราประกาศแต่ไม่ได้เริ่มต้นผลลัพธ์ที่เราต้องการให้ฟังก์ชันนี้สร้างในท้ายที่สุด การปล่อยไว้โดยไม่ได้กำหนดไว้ เรามั่นใจว่าจะต้องกำหนดให้ใช้เพียงครั้งเดียวก่อนจึงจะสามารถใช้งานได้ และโดยการประกาศจะทำให้มั่นใจได้ว่าจะไม่มีการกำหนดให้ใช้ซ้ำได้อีกเป็นครั้งที่สอง บรรทัดที่สองคือการเลื่อนซึ่งจะดำเนินการเป็นการกระทำสุดท้ายของฟังก์ชันนี้ มันเรียกใช้บล็อกการสำเร็จด้วย finalResult
- หลังจากที่ถูกกำหนดโดยฟังก์ชันที่เหลือ บรรทัดที่ 3 สร้างค่าคงที่ใหม่ที่เรียกว่าความสมบูรณ์ ซึ่งปิดบังพารามิเตอร์การโทรกลับ เสร็จสิ้นใหม่เป็นประเภทเป็นโมฆะซึ่งประกาศไม่มี API สาธารณะ บรรทัดนี้ช่วยให้แน่ใจว่าการใช้การเสร็จสิ้นหลังจากบรรทัดนี้จะเป็นข้อผิดพลาดของคอมไพเลอร์ การเลื่อนเวลาในบรรทัดที่ 2 เป็นการใช้บล็อกเสร็จสิ้นเท่านั้นที่ได้รับอนุญาต บรรทัดที่ 4 ลบคำเตือนของคอมไพเลอร์ที่อาจแสดงเกี่ยวกับค่าคงที่การเติมใหม่ที่ไม่ได้ใช้งาน
ดังนั้นเราจึงบังคับคอมไพเลอร์แบบรวดเร็วให้รายงานว่าฟังก์ชันอะซิงโครนัสนี้ไม่เป็นไปตามสัญญา มาดูขั้นตอนเพื่อแก้ไขกัน ขั้นแรก ให้แทนที่การเข้าถึงการโทรกลับโดยตรงทั้งหมดด้วยการมอบหมายให้เป็น finalResult
func generateIcecream(from int: Int, completion: (String) -> Void) { let finalResult: String defer { completion(finalResult) } let completion: Void = Void() defer { completion } if int > 5 { if int < 20 { finalResult = "Chocolate" } else if int < 10 { finalResult = "Strawberry" } // else finalResult = "Pistachio" } else if int < 2 { finalResult = "Vanilla" } }
ตอนนี้คอมไพเลอร์กำลังรายงานปัญหาสองประการ:
error: AsyncCorrectness.playground:1:8: error: constant 'finalResult' used before being initialized defer { completion(finalResult) } ^ error: AsyncCorrectness.playground:11:3: error: immutable value 'finalResult' may only be initialized once finalResult = "Pistachio"
ตามที่คาดไว้ ฟังก์ชันมีเส้นทางที่กำหนด finalResult
เป็นศูนย์ครั้งและเส้นทางที่กำหนดมากกว่าหนึ่งครั้ง เราแก้ไขปัญหาเหล่านี้ดังนี้:
func generateIcecream(from int: Int, completion: (String) -> Void) { let finalResult: String defer { completion(finalResult) } let completion: Void = Void() defer { completion } if int > 5 { if int < 20 { finalResult = "Chocolate" } else if int < 10 { finalResult = "Strawberry" } else { finalResult = "Pistachio" } } else if int < 2 { finalResult = "Vanilla" } else { finalResult = "Neapolitan" } }
คำว่า “Pistachio” ถูกย้ายไปยังส่วนอื่นที่เหมาะสม และเราตระหนักดีว่าเราล้มเหลวในการครอบคลุมกรณีทั่วไป ซึ่งแน่นอนว่าคือ “Neapolitan”
รูปแบบที่เพิ่งอธิบายสามารถปรับได้อย่างง่ายดายเพื่อส่งกลับค่าทางเลือก ข้อผิดพลาดทางเลือก หรือประเภทที่ซับซ้อน เช่น Result enum ทั่วไป โดยการบังคับคอมไพเลอร์ให้ตรวจสอบว่ามีการเรียกใช้การเรียกกลับเพียงครั้งเดียว เราสามารถยืนยันความถูกต้องและความสมบูรณ์ของฟังก์ชันแบบอะซิงโครนัสได้