IOS में Concurrency एक बड़ा विषय है। इसलिए इस लेख में मैं कतारों और ग्रैंड सेंट्रल डिस्पैच (जीसीडी) ढांचे से संबंधित एक उप-विषय पर ज़ूम इन करना चाहता हूं।
विशेष रूप से, मैं धारावाहिक और समवर्ती कतारों के बीच के अंतरों के साथ-साथ सिंक्रोनस और एसिंक्रोनस निष्पादन के बीच के अंतरों का पता लगाना चाहता हूं।
यदि आपने पहले कभी GCD का उपयोग नहीं किया है, तो यह लेख शुरू करने के लिए एक बेहतरीन जगह है। यदि आपके पास जीसीडी के साथ कुछ अनुभव है, लेकिन ऊपर वर्णित विषयों के बारे में अभी भी उत्सुक हैं, तो मुझे लगता है कि आप इसे अभी भी उपयोगी पाएंगे। और मुझे आशा है कि आप रास्ते में एक या दो नई चीजें उठाएंगे।
मैंने इस आलेख में अवधारणाओं को दृष्टि से प्रदर्शित करने के लिए एक स्विफ्टयूआई साथी ऐप बनाया है। ऐप में एक मजेदार लघु प्रश्नोत्तरी भी है जिसे मैं आपको इस लेख को पढ़ने से पहले और बाद में प्रयास करने के लिए प्रोत्साहित करता हूं। यहां स्रोत कोड डाउनलोड करें, या यहां सार्वजनिक बीटा प्राप्त करें।
मैं जीसीडी के परिचय के साथ शुरुआत करूंगा, इसके बाद सिंक, एसिंक, सीरियल और समवर्ती पर विस्तृत विवरण दूंगा। बाद में, मैं संगामिति के साथ काम करते समय कुछ नुकसानों को कवर करूंगा। अंत में, मैं एक सारांश और कुछ सामान्य सलाह के साथ अपनी बात समाप्त करूंगा।
परिचय
आइए GCD और प्रेषण कतारों के संक्षिप्त परिचय के साथ शुरुआत करें। बेझिझक सिंक बनाम Async पर जाएं अनुभाग यदि आप पहले से ही इस विषय से परिचित हैं।
Concurrency and Grand Central Dispatch
Concurrency आपको इस तथ्य का लाभ उठाने देता है कि आपके डिवाइस में कई CPU कोर हैं। इन कोर का उपयोग करने के लिए, आपको कई थ्रेड्स का उपयोग करने की आवश्यकता होगी। हालांकि, थ्रेड्स एक निम्न-स्तरीय टूल हैं, और प्रभावी तरीके से मैन्युअल रूप से थ्रेड्स को प्रबंधित करना अत्यंत कठिन है।
ग्रैंड सेंट्रल डिस्पैच को ऐप्पल द्वारा 10 साल पहले एक अमूर्त के रूप में बनाया गया था ताकि डेवलपर्स को मैन्युअल रूप से थ्रेड्स को बनाए और प्रबंधित किए बिना मल्टी-थ्रेडेड कोड लिखने में मदद मिल सके।
GCD के साथ, Apple ने एसिंक्रोनस डिज़ाइन दृष्टिकोण अपनाया समस्या को। सीधे थ्रेड बनाने के बजाय, आप कार्य कार्यों को शेड्यूल करने के लिए GCD का उपयोग करते हैं, और सिस्टम अपने संसाधनों का सर्वोत्तम उपयोग करके आपके लिए इन कार्यों को निष्पादित करेगा। GCD अपेक्षित थ्रेड बनाने का काम संभालेगा और उन थ्रेड्स पर आपके कार्यों को शेड्यूल करेगा, थ्रेड प्रबंधन के बोझ को डेवलपर से सिस्टम पर स्थानांतरित कर देगा।
जीसीडी का एक बड़ा फायदा यह है कि जब आप अपना समवर्ती कोड लिखते हैं तो आपको हार्डवेयर संसाधनों के बारे में चिंता करने की ज़रूरत नहीं है। GCD आपके लिए एक थ्रेड पूल का प्रबंधन करता है, और यह सिंगल-कोर Apple वॉच से लेकर कई-कोर MacBook Pro तक विस्तृत होगा।
डिस्पैच कतारें
ये जीसीडी के मुख्य निर्माण खंड हैं जो आपको परिभाषित मापदंडों के एक सेट का उपयोग करके कोड के मनमाने ब्लॉकों को निष्पादित करने देते हैं। प्रेषण कतारों में कार्य हमेशा पहले-पहले, पहले-बाहर (फीफो) फैशन में शुरू होते हैं। ध्यान दें कि मैंने कहा शुरू किया , क्योंकि आपके कार्यों का पूरा होने का समय कई कारकों पर निर्भर करता है, और फीफो होने की गारंटी नहीं है (उस पर बाद में अधिक।)
मोटे तौर पर, आपके लिए तीन प्रकार की कतारें उपलब्ध हैं:
- मुख्य प्रेषण कतार (धारावाहिक, पूर्व-निर्धारित)
- वैश्विक कतारें (समवर्ती, पूर्व-निर्धारित)
- निजी कतार (धारावाहिक या समवर्ती हो सकती हैं, आप उन्हें बनाते हैं)
प्रत्येक ऐप एक मुख्य कतार के साथ आता है, जो एक धारावाहिक . है कतार जो मुख्य धागे पर कार्यों को निष्पादित करती है। यह कतार आपके एप्लिकेशन के 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 कार्य मुख्य कतार पर निष्पादित किए जाने चाहिए। जब भी पृष्ठभूमि थ्रेड पर UI कार्य निष्पादित होता है, तो चेतावनियां प्राप्त करने के लिए आप Xcode में मुख्य थ्रेड चेकर विकल्प चालू कर सकते हैं।
मुख्य कतार के अलावा, प्रत्येक ऐप कई पूर्व-निर्धारित समवर्ती कतारों के साथ आता है जिनमें सेवा की गुणवत्ता के विभिन्न स्तर होते हैं (जीसीडी में प्राथमिकता की एक अमूर्त धारणा।)
उदाहरण के लिए, उपयोगकर्ता सहभागी . को अतुल्यकालिक रूप से कार्य सबमिट करने के लिए कोड यहां दिया गया है (सर्वोच्च प्राथमिकता) क्यूओएस कतार:
DispatchQueue.global(qos: .userInteractive).async {
print("We're on a global concurrent queue!")
}
वैकल्पिक रूप से, आप डिफ़ॉल्ट प्राथमिकता . को कॉल कर सकते हैं इस तरह क्यूओएस निर्दिष्ट न करके वैश्विक कतार:
DispatchQueue.global().async {
print("Generic global queue")
}
इसके अतिरिक्त, आप निम्न सिंटैक्स का उपयोग करके अपनी निजी क्यू बना सकते हैं:
let serial = DispatchQueue(label: "com.besher.serial-queue")
serial.async {
print("Private serial queue")
}
निजी कतार बनाते समय, यह एक वर्णनात्मक लेबल (जैसे रिवर्स डीएनएस नोटेशन) का उपयोग करने में मदद करता है, क्योंकि यह एक्सकोड के नेविगेटर, एलएलडीबी और इंस्ट्रूमेंट्स में डिबगिंग करते समय आपकी सहायता करेगा:
डिफ़ॉल्ट रूप से, निजी क्यू धारावाहिक हैं (मैं समझाता हूँ कि इसका क्या अर्थ है, शीघ्र ही वादा करें!) यदि आप एक निजी समवर्ती बनाना चाहते हैं कतार, आप वैकल्पिक विशेषताओं . के माध्यम से ऐसा कर सकते हैं पैरामीटर:
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")
}
भले ही आप सिंक्रोनस या एसिंक्रोनस रूप से प्रेषण करें, और चाहे आप एक सीरियल या समवर्ती कतार चुनें, एक ही कार्य के अंदर सभी कोड लाइन से लाइन निष्पादित करेंगे। Concurrency केवल प्रासंगिक है जब एकाधिक . का मूल्यांकन किया जाता है कार्य।
उदाहरण के लिए, यदि आपके पास समान . के अंदर 3 लूप हैं कार्य, ये लूप हमेशा होंगे क्रम में निष्पादित करें:
DispatchQueue.global().async {
for i in 0..<10 {
print(i)
}
for _ in 0..<10 {
print("?")
}
for _ in 0..<10 {
print("?")
}
}
यह कोड हमेशा 0 से 9 तक दस अंक प्रिंट करता है, उसके बाद दस नीले घेरे, उसके बाद दस टूटे हुए दिल, चाहे आप उस क्लोजर को कैसे भेजते हैं।
अलग-अलग कार्यों का अपना QoS स्तर भी हो सकता है (डिफ़ॉल्ट रूप से वे अपनी कतार की प्राथमिकता का उपयोग करते हैं।) कतार QoS और कार्य QoS के बीच यह अंतर कुछ दिलचस्प व्यवहार की ओर ले जाता है जिसके बारे में हम प्राथमिकता उलटा अनुभाग में चर्चा करेंगे।
अब तक आप सोच रहे होंगे कि धारावाहिक . क्या है और समवर्ती सब के बारे में हैं। आप sync
. के बीच अंतर के बारे में भी सोच रहे होंगे और async
अपने कार्यों को सबमिट करते समय। यह हमें इस लेख की जड़ तक ले आता है, तो आइए इसमें डुबकी लगाते हैं!
सिंक बनाम Async
जब आप किसी कार्य को कतार में भेजते हैं, तो आप sync
का उपयोग करके इसे सिंक्रोनाइज़ या एसिंक्रोनस रूप से करना चुन सकते हैं और async
प्रेषण कार्य। सिंक और एसिंक्स मुख्य रूप से स्रोत . को प्रभावित करते हैं सबमिट किए गए कार्य में, वह कतार है जहां इसे से . सबमिट किया जा रहा है ।
जब आपका कोड sync
. पर पहुंच जाता है कथन, यह कार्य पूरा होने तक वर्तमान कतार को अवरुद्ध कर देगा। एक बार जब कार्य वापस आ जाता है / पूरा हो जाता है, तो कॉल करने वाले को नियंत्रण वापस कर दिया जाता है, और कोड जो sync
का अनुसरण करता है कार्य जारी रहेगा।
sync
के बारे में सोचें 'अवरुद्ध' के समानार्थी के रूप में।
एक async
दूसरी ओर, कथन, वर्तमान कतार के संबंध में अतुल्यकालिक रूप से निष्पादित होगा, और async
की सामग्री की प्रतीक्षा किए बिना तुरंत कॉल करने वाले को नियंत्रण वापस कर देगा निष्पादित करने के लिए बंद। इस बात की कोई गारंटी नहीं है कि वास्तव में उस एसिंक्स क्लोजर के अंदर का कोड कब निष्पादित होगा।
वर्तमान कतार?
यह स्पष्ट नहीं हो सकता है कि स्रोत क्या है, या वर्तमान , क्यू है, क्योंकि यह हमेशा कोड में स्पष्ट रूप से परिभाषित नहीं होता है।
उदाहरण के लिए, यदि आप अपने sync
. पर कॉल करते हैं viewDidLoad के अंदर कथन, आपकी वर्तमान कतार मुख्य प्रेषण कतार होगी। यदि आप उसी फ़ंक्शन को URLSession पूर्णता हैंडलर के अंदर कॉल करते हैं, तो आपकी वर्तमान कतार एक पृष्ठभूमि कतार होगी।
सिंक बनाम async पर वापस जा रहे हैं, आइए इस उदाहरण को लेते हैं:
DispatchQueue.global().sync {
print("Inside")
}
print("Outside")
// Console output:
// Inside
// Outside
उपरोक्त कोड वर्तमान क्यू को ब्लॉक कर देगा, क्लोजर में प्रवेश करेगा और "आउटसाइड" प्रिंट करने के लिए आगे बढ़ने से पहले "इनसाइड" प्रिंट करके ग्लोबल क्यू पर अपना कोड निष्पादित करेगा। इस आदेश की गारंटी है।
देखते हैं क्या होता है अगर हम async
. को आजमाते हैं इसके बजाय:
DispatchQueue.global().async {
print("Inside")
}
print("Outside")
// Potential console output (based on QoS):
// Outside
// Inside
हमारा कोड अब वैश्विक कतार को बंद कर देता है, फिर अगली पंक्ति को चलाने के लिए तुरंत आगे बढ़ता है। यह संभावना होगा "अंदर" से पहले "बाहर" प्रिंट करें, लेकिन इस आदेश की गारंटी नहीं है। यह स्रोत और गंतव्य कतारों के क्यूओएस के साथ-साथ सिस्टम द्वारा नियंत्रित अन्य कारकों पर निर्भर करता है।
जीसीडी में थ्रेड्स एक कार्यान्वयन विवरण हैं —हम उन पर प्रत्यक्ष नियंत्रण नहीं रखते हैं और केवल कतार के सार का उपयोग करके उनसे निपट सकते हैं। फिर भी, मुझे लगता है कि जीसीडी के साथ आने वाली कुछ चुनौतियों को समझने के लिए थ्रेड व्यवहार पर 'कवर के नीचे झांकना' उपयोगी हो सकता है।
उदाहरण के लिए, जब आप sync
. का उपयोग करके कोई कार्य सबमिट करते हैं , GCD वर्तमान थ्रेड (कॉलर) पर उस कार्य को निष्पादित करके प्रदर्शन को अनुकूलित करता है।
हालांकि, एक अपवाद है, जब आप मुख्य कतार में एक सिंक कार्य सबमिट करते हैं — ऐसा करने से कार्य हमेशा मुख्य थ्रेड पर चलेगा, न कि कॉलर पर। इस व्यवहार के कुछ प्रभाव हो सकते हैं जिन्हें हम प्राथमिकता उलटा अनुभाग में तलाशेंगे।
कौन-सा उपयोग करना है?
क्यू में काम सबमिट करते समय, ऐप्पल सिंक्रोनस निष्पादन पर एसिंक्रोनस निष्पादन का उपयोग करने की अनुशंसा करता है। हालांकि, ऐसी स्थितियां हैं जहां sync
बेहतर विकल्प हो सकता है, जैसे दौड़ की स्थिति से निपटने के दौरान, या बहुत छोटा कार्य करते समय। मैं जल्द ही इन स्थितियों को कवर करूंगा।
किसी फ़ंक्शन के अंदर अतुल्यकालिक रूप से कार्य करने का एक बड़ा परिणाम यह है कि फ़ंक्शन अब सीधे अपने मान वापस नहीं कर सकता है (यदि वे किए जा रहे async कार्य पर निर्भर करते हैं)। इसके बजाय परिणाम देने के लिए इसे क्लोजर/पूर्णता हैंडलर पैरामीटर का उपयोग करना चाहिए।
इस अवधारणा को प्रदर्शित करने के लिए, आइए एक छोटा सा कार्य करें जो छवि डेटा स्वीकार करता है, छवि को संसाधित करने के लिए कुछ महंगी गणना करता है, फिर परिणाम देता है:
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
}
}
इस कोड के साथ दो मुद्दे हैं। सबसे पहले, रिटर्न स्टेटमेंट एसिंक्स क्लोजर के अंदर है, इसलिए यह अब 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)
}
}
जैसा कि इस उदाहरण में स्पष्ट है, फ़ंक्शन को एसिंक्रोनस बनाने के लिए परिवर्तन ने अपने कॉलर को प्रचारित किया है, जिसे अब क्लोजर में पास करना होगा और परिणामों को एसिंक्रोनस रूप से भी संभालना होगा। एक अतुल्यकालिक कार्य शुरू करके, आप संभावित रूप से कई कार्यों की एक श्रृंखला को संशोधित कर सकते हैं।
जैसा कि हमने अभी देखा, समवर्ती और अतुल्यकालिक निष्पादन आपकी परियोजना में जटिलता जोड़ते हैं। यह संकेत डिबगिंग को और अधिक कठिन बना देता है। यही कारण है कि यह वास्तव में आपके डिजाइन में समेकन के बारे में सोचने के लिए भुगतान करता है-यह ऐसा कुछ नहीं है जिसे आप अपने डिजाइन चक्र के अंत में करना चाहते हैं।
इसके विपरीत, तुल्यकालिक निष्पादन जटिलता में वृद्धि नहीं करता है। इसके बजाय, यह आपको रिटर्न स्टेटमेंट का उपयोग जारी रखने की अनुमति देता है जैसा आपने पहले किया था। एक फ़ंक्शन जिसमें sync
. होता है कार्य तब तक वापस नहीं आएगा जब तक कि उस कार्य के अंदर का कोड पूरा नहीं हो जाता। इसलिए इसे पूर्ण करने वाले हैंडलर की आवश्यकता नहीं है।
यदि आप एक छोटा कार्य सबमिट कर रहे हैं (उदाहरण के लिए, मान अपडेट करना), तो इसे समकालिक रूप से करने पर विचार करें। यह न केवल आपके कोड को सरल बनाए रखने में आपकी मदद करता है, बल्कि यह बेहतर प्रदर्शन भी करेगा - माना जाता है कि Async को एक ओवरहेड लगता है जो कार्य को अतुल्यकालिक रूप से छोटे कार्यों के लिए करने के लाभ से अधिक होता है जिसे पूरा करने में 1ms से कम समय लगता है।
यदि आप एक बड़ा कार्य सबमिट कर रहे हैं, हालांकि, जैसा कि हमने ऊपर किया गया इमेज प्रोसेसिंग, तो कॉलर को बहुत लंबे समय तक ब्लॉक करने से बचने के लिए इसे अतुल्यकालिक रूप से करने पर विचार करें।
एक ही कतार में प्रेषण
हालांकि किसी कार्य को कतार से अपने आप में अतुल्यकालिक रूप से भेजना सुरक्षित है (उदाहरण के लिए, आप वर्तमान कतार पर .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("?") }
}
यहां दोनों कतारें धारावाहिक हैं, लेकिन परिणाम एक दूसरे के संबंध में समवर्ती रूप से निष्पादित होने के कारण उलझे हुए हैं। तथ्य यह है कि वे प्रत्येक धारावाहिक (या समवर्ती) हैं, इस परिणाम पर कोई प्रभाव नहीं पड़ता है। उनका क्यूओएस स्तर निर्धारित करता है कि कौन आम तौर पर पहले समाप्त करें (आदेश की गारंटी नहीं है)।
अगर हम यह सुनिश्चित करना चाहते हैं कि दूसरा लूप शुरू करने से पहले पहला लूप पहले खत्म हो जाए, तो हम कॉलर से पहले टास्क को सिंक्रोनाइज़ कर सकते हैं:
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
को निष्पादित करते समय उसी कतार में कार्य सबमिट करने का प्रयास किया है कथन, वह कार्य करेगा हमारे यहां जो कुछ भी चल रहा है, उसके साथ-साथ चलाएं, क्योंकि यह अभी भी एक समवर्ती कतार है।
कौन सा उपयोग करना है?
सीरियल क्यू सीपीयू अनुकूलन और कैशिंग का लाभ उठाते हैं, और संदर्भ स्विचिंग को कम करने में मदद करते हैं।
ऐप्पल आपके ऐप में प्रति सबसिस्टम एक सीरियल कतार से शुरू करने की सिफारिश करता है - उदाहरण के लिए नेटवर्किंग के लिए एक, फ़ाइल संपीड़न के लिए एक, आदि। यदि आवश्यकता होती है, तो आप बाद में सेटटार्ग विधि या वैकल्पिक लक्ष्य का उपयोग करके प्रति सबसिस्टम कतारों के पदानुक्रम में विस्तार कर सकते हैं। कतार बनाते समय पैरामीटर।
यदि आप एक प्रदर्शन बाधा में आते हैं, तो अपने ऐप के प्रदर्शन को मापें और देखें कि एक समवर्ती कतार मदद करती है या नहीं। यदि आपको कोई मापन योग्य लाभ दिखाई नहीं देता है, तो बेहतर होगा कि आप लगातार कतारों में बने रहें।
नुकसान
वरीयता उलटा और सेवा की गुणवत्ता
प्राथमिकता उलटा तब होता है जब उच्च प्राथमिकता वाले कार्य को कम प्राथमिकता वाले कार्य द्वारा चलने से रोका जाता है, प्रभावी रूप से उनकी सापेक्ष प्राथमिकताओं को उलट दिया जाता है।
यह स्थिति अक्सर तब होती है जब एक उच्च 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 मंडल नीली होती हैं।
यह समझने के लिए कि ऐसा क्यों होता है, हमें इस तथ्य पर फिर से विचार करने की आवश्यकता है कि कॉलर थ्रेड पर सिंक्रोनस कार्य निष्पादित किया जाता है (जब तक कि आप मुख्य कतार में सबमिट नहीं कर रहे हों।)
ऊपर दिए गए हमारे उदाहरण में, कॉलर (स्टार्टर क्यू) के पास शीर्ष QoS (userInteractive) है। इसलिए, यह प्रतीत होता है कि हानिरहित sync
है कार्य न केवल स्टार्टर कतार को अवरुद्ध कर रहा है, बल्कि यह स्टार्टर के उच्च QoS थ्रेड पर भी चल रहा है। इसलिए कार्य उच्च QoS के साथ चलता है, लेकिन इसके आगे दो अन्य कार्य उसी पृष्ठभूमि कतार पर हैं जिनकी पृष्ठभूमि है क्यूओएस। प्राथमिकता उलटने का पता चला!
जैसा कि अपेक्षित था, GCD उच्च QoS कार्य से अस्थायी रूप से मेल खाने के लिए संपूर्ण कतार के QoS को ऊपर उठाकर इस व्युत्क्रमण का समाधान करता है। नतीजतन, पृष्ठभूमि कतार के सभी कार्य अंत में उपयोगकर्ता इंटरैक्टिव . पर चल रहे हैं QoS, जो उपयोगिता . से अधिक है क्यूओएस। और इसीलिए उपयोगिता कार्य अंतिम रूप से समाप्त होते हैं!
साइड-नोट:यदि आप उस उदाहरण से स्टार्टर कतार को हटाते हैं और इसके बजाय मुख्य कतार से सबमिट करते हैं, तो आपको समान परिणाम मिलेंगे, क्योंकि मुख्य कतार में उपयोगकर्ता इंटरैक्टिव भी है। क्यूओएस.
इस उदाहरण में प्राथमिकता उलटने से बचने के लिए, हमें sync
. के साथ स्टार्टर क्यू को ब्लॉक करने से बचना होगा बयान। async
का उपयोग करना उस समस्या का समाधान करेंगे।
हालांकि यह हमेशा आदर्श नहीं होता है, आप निजी क्यू बनाते समय या वैश्विक समवर्ती कतार में भेजते समय डिफ़ॉल्ट क्यूओएस से चिपके हुए प्राथमिकता व्युत्क्रम को कम कर सकते हैं।
थ्रेड विस्फोट
जब आप समवर्ती कतार का उपयोग करते हैं, तो यदि आप सावधान नहीं हैं तो आप थ्रेड विस्फोट का जोखिम उठाते हैं। ऐसा तब हो सकता है जब आप कार्यों को समवर्ती कतार में सबमिट करने का प्रयास करते हैं जो वर्तमान में अवरुद्ध है (उदाहरण के लिए सेमाफोर, सिंक, या किसी अन्य तरीके से।) आपके कार्य होगा चलते हैं, लेकिन सिस्टम इन नए कार्यों को समायोजित करने के लिए नए थ्रेड्स को स्पिन करना समाप्त कर देगा, और थ्रेड सस्ते नहीं होंगे।
यही कारण है कि ऐप्पल आपके ऐप में प्रति सबसिस्टम सीरियल कतार से शुरू करने का सुझाव देता है, क्योंकि प्रत्येक सीरियल कतार केवल एक थ्रेड का उपयोग कर सकती है। याद रखें कि सीरियल की कतारें एक साथ होती हैं करने के लिए अन्य कतारें, इसलिए जब आप अपने काम को कतार में उतारते हैं, तब भी आपको प्रदर्शन लाभ मिलता है, भले ही वह समवर्ती न हो।
रेस कंडीशन
स्विफ्ट एरेज़, डिक्शनरी, स्ट्रक्चर और अन्य मान प्रकार डिफ़ॉल्ट रूप से थ्रेड-सुरक्षित नहीं हैं। उदाहरण के लिए, जब आपके पास एक से अधिक थ्रेड हैं जो एक्सेस करने और संशोधित . करने का प्रयास कर रहे हैं वही सरणी, आप परेशानी में पड़ना शुरू कर देंगे।
पाठक-लेखक की समस्या के विभिन्न समाधान हैं, जैसे ताले या सेमाफोर का उपयोग करना। लेकिन मैं यहां जिस प्रासंगिक समाधान पर चर्चा करना चाहता हूं, वह एक आइसोलेशन कतार का उपयोग है।
मान लें कि हमारे पास पूर्णांकों की एक सरणी है, और हम इस सरणी को संदर्भित करने वाले अतुल्यकालिक कार्य सबमिट करना चाहते हैं। जब तक हमारा काम केवल पढ़ता है सरणी और इसे संशोधित नहीं करता है, हम सुरक्षित हैं। लेकिन जैसे ही हम अपने एसिंक्रोनस कार्यों में से एक में सरणी को संशोधित करने का प्रयास करते हैं, हम अपने ऐप में अस्थिरता का परिचय देंगे।
यह एक मुश्किल समस्या है क्योंकि आपका ऐप बिना किसी समस्या के 10 बार चल सकता है, और फिर यह 11वीं बार क्रैश हो जाता है। इस स्थिति के लिए एक बहुत ही उपयोगी उपकरण Xcode में Thread Sanitizer है। इस विकल्प को सक्षम करने से आपको अपने ऐप में संभावित दौड़ स्थितियों की पहचान करने में मदद मिलेगी।
समस्या को प्रदर्शित करने के लिए, आइए इसे (स्वीकार्य रूप से काल्पनिक) उदाहरण लेते हैं:
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
. हो सकता है , क्योंकि लिखने के दौरान हमें कॉलर को ब्लॉक करने की आवश्यकता नहीं है।
हम दौड़ की स्थिति को हल करने के लिए बिना किसी बाधा के सीरियल कतार का उपयोग कर सकते थे, लेकिन तब हम सरणी में समवर्ती पढ़ने की पहुंच का लाभ खो देंगे। शायद यह आपके मामले में समझ में आता है, आपको फैसला करना है।
निष्कर्ष
यहाँ तक पढ़ने के लिए आपका बहुत-बहुत धन्यवाद! मुझे आशा है कि आपने इस लेख से कुछ नया सीखा है। मैं आपको एक सारांश और कुछ सामान्य सलाह देता हूँ:
सारांश
- कतार हमेशा शुरू करें फीफो क्रम में उनके कार्य
- कतार हमेशा अन्य . के सापेक्ष समवर्ती होती हैं कतारें
- सिंक करें बनाम Async स्रोत से संबंधित है
- धारावाहिक बनाम समवर्ती गंतव्य से संबंधित है
- सिंक 'ब्लॉकिंग' का पर्याय है
- Async तुरंत कॉलर को नियंत्रण लौटाता है
- सीरियल एकल थ्रेड का उपयोग करता है, और निष्पादन के आदेश की गारंटी देता है
- समवर्ती बहु-धागे का उपयोग करता है, और थ्रेड विस्फोट का जोखिम उठाता है
- अपने डिजाइन चक्र में समेकन के बारे में जल्दी सोचें
- सिंक्रोनस कोड के बारे में तर्क करना और डीबग करना आसान है
- यदि संभव हो तो वैश्विक समवर्ती कतारों पर निर्भर रहने से बचें
- प्रति सबसिस्टम एक सीरियल क्यू से शुरू करने पर विचार करें
- समवर्ती कतार में तभी स्विच करें जब आपको मापन योग्य . दिखाई दे प्रदर्शन लाभ
मुझे स्विफ्ट कंसुरेंसी मेनिफेस्टो से 'समरूपता के समुद्र में क्रमबद्धता का द्वीप' होने का रूपक पसंद है। मैट डाईहाउस के इस ट्वीट में भी यही भावना साझा की गई:
समवर्ती कोड लिखने का रहस्य यह है कि इसका अधिकांश भाग धारावाहिक बना दिया जाए। संगामिति को एक छोटी, बाहरी परत तक सीमित करें। (सीरियल कोर, समवर्ती शेल।)
- मैट डाइफहाउस (@mdiep) दिसंबर 18, 2019
जैसे 5 गुणों को प्रबंधित करने के लिए लॉक का उपयोग करने के बजाय, एक नया प्रकार बनाएं जो उन्हें लपेटता है और लॉक के अंदर एकल संपत्ति का उपयोग करता है।
जब आप उस दर्शन को ध्यान में रखते हुए संगामिति को लागू करते हैं, तो मुझे लगता है कि यह समवर्ती कोड प्राप्त करने में आपकी मदद करेगा, जिसके बारे में कॉलबैक के झंझट में खोए बिना तर्क किया जा सकता है।
यदि आपका कोई प्रश्न या टिप्पणी है, तो बेझिझक मुझसे ट्विटर पर संपर्क करें
बेशर अल मालेह
अनस्प्लैश पर ओनूर के द्वारा कवर फ़ोटो
सहयोगी ऐप यहां से डाउनलोड करें:
almaleh/DispatcherCompanion ऐप को समवर्ती पर मेरे लेख के लिए। GitHub पर एक खाता बनाकर अलमलेह/डिस्पैचर विकास में योगदान करें। almalehGitHubमेरे कुछ अन्य लेख देखें:
आतिशबाजी — जैसे ही आप अपने कण प्रभावों को डिजाइन और पुनरावृति करते हैं, macOS और iOS के लिए स्विफ्ट कोड उत्पन्न करने के लिए एक दृश्य कण संपादक। बेशर अल मालेहफ्लॉलेस आईओएस आपको (हमेशा) जरूरत नहीं है [कमजोर आत्म] इस लेख में, हम कमजोर आत्म के बारे में बात करेंगे inside of Swift closures to avoid retain cycles &explore cases where it may or may not be necessary to capture self weakly. 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