paint-brush
एकता विकास के लिए पूर्ण अतुल्यकालिक प्रोग्रामिंग प्राइमरद्वारा@dmitrii
5,860 रीडिंग
5,860 रीडिंग

एकता विकास के लिए पूर्ण अतुल्यकालिक प्रोग्रामिंग प्राइमर

द्वारा Dmitrii Ivashchenko31m2023/03/30
Read on Terminal Reader

बहुत लंबा; पढ़ने के लिए

इस लेख में हम ऐसी किसी भी समस्या से बचने के बारे में बात करने जा रहे हैं। हम इन कार्यों को एक अलग थ्रेड में करने के लिए अतुल्यकालिक प्रोग्रामिंग तकनीकों की अनुशंसा करेंगे, इस प्रकार मुख्य थ्रेड को अन्य कार्यों को करने के लिए स्वतंत्र छोड़ देंगे। यह सुचारू और उत्तरदायी गेमप्ले सुनिश्चित करने में मदद करेगा, और (उम्मीद है) संतुष्ट गेमर्स।
featured image - एकता विकास के लिए पूर्ण अतुल्यकालिक प्रोग्रामिंग प्राइमर
Dmitrii Ivashchenko HackerNoon profile picture
0-item

खेल के विकास में कुछ कार्य तुल्यकालिक नहीं हैं - वे अतुल्यकालिक हैं। इसका मतलब है कि वे गेम कोड के भीतर रैखिक रूप से निष्पादित नहीं होते हैं। इनमें से कुछ अतुल्यकालिक कार्यों को पूरा करने में काफी लंबा समय लग सकता है, जबकि अन्य गहन संगणनाओं से जुड़े हैं।


कुछ सबसे आम गेमिंग अतुल्यकालिक कार्य इस प्रकार हैं:

  • नेटवर्क अनुरोध निष्पादित करना

  • दृश्यों, संसाधनों और अन्य संपत्तियों को लोड किया जा रहा है

  • फ़ाइलें पढ़ना और लिखना

  • निर्णय लेने के लिए आर्टिफिशियल इंटेलिजेंस

  • लंबे एनिमेशन सीक्वेंस

  • बड़ी मात्रा में डेटा संसाधित करना

  • पथ खोज


अब, महत्वपूर्ण रूप से, चूंकि सभी यूनिटी कोड एक थ्रेड में चलते हैं, ऊपर बताए गए कार्यों में से कोई भी कार्य, यदि उन्हें समकालिक रूप से निष्पादित किया जाता है, तो मुख्य थ्रेड अवरुद्ध हो जाएगा, और इस प्रकार, फ्रेम ड्रॉप हो जाएगा।


सभी को नमस्कार, मेरा नाम दमित्री इवाशचेंको है और मैं MY.GAMES में डेवलपमेंट टीम का प्रमुख हूं। इस लेख में हम ऐसी किसी भी समस्या से बचने के बारे में बात करने जा रहे हैं। हम इन कार्यों को एक अलग थ्रेड में करने के लिए अतुल्यकालिक प्रोग्रामिंग तकनीकों की अनुशंसा करेंगे, इस प्रकार मुख्य थ्रेड को अन्य कार्यों को करने के लिए स्वतंत्र छोड़ देंगे। यह सुचारू और उत्तरदायी गेमप्ले सुनिश्चित करने में मदद करेगा, और (उम्मीद है) संतुष्ट गेमर्स।

कोरूटिन्स

सबसे पहले, कोरआउटीन के बारे में बात करते हैं। उन्हें 2011 में एकता में पेश किया गया था, इससे पहले कि एसिंक्स / वेट .NET में दिखाई दिया। एकता में, कोरटाइन हमें एक साथ सभी को निष्पादित करने के बजाय, कई फ़्रेमों पर निर्देशों का एक सेट निष्पादित करने की अनुमति देता है। वे धागे के समान हैं, लेकिन हल्के हैं और एकता के अपडेट लूप में एकीकृत हैं, जिससे वे खेल के विकास के लिए उपयुक्त हैं।


(वैसे, ऐतिहासिक रूप से बोलना, एकता में अतुल्यकालिक संचालन करने के लिए कोरटाइन पहला तरीका था, इसलिए इंटरनेट पर अधिकांश लेख उनके बारे में हैं।)


एक कोरटाइन बनाने के लिए, आपको IEnumerator रिटर्न प्रकार के साथ एक फ़ंक्शन घोषित करने की आवश्यकता है। इस फ़ंक्शन में कोई भी तर्क हो सकता है जिसे आप चाहते हैं कि कोरआउटिन निष्पादित हो।


एक कोरटाइन शुरू करने के लिए, आपको MonoBehaviour इंस्टेंस पर StartCoroutine मेथड को कॉल करना होगा और एक तर्क के रूप में कॉरटीन फ़ंक्शन को पास करना होगा:

 public class Example : MonoBehaviour { void Start() { StartCoroutine(MyCoroutine()); } IEnumerator MyCoroutine() { Debug.Log("Starting coroutine"); yield return null; Debug.Log("Executing coroutine"); yield return null; Debug.Log("Finishing coroutine"); } }


एकता में कई उपज निर्देश उपलब्ध हैं, जैसे कि WaitForSeconds , WaitForEndOfFrame , WaitForFixedUpdate , WaitForSecondsRealtime , WaitUntil और साथ ही कुछ अन्य। यह याद रखना महत्वपूर्ण है कि उनका उपयोग करने से आवंटन होता है, इसलिए जहां भी संभव हो उनका पुन: उपयोग किया जाना चाहिए।


उदाहरण के लिए, दस्तावेज़ीकरण से इस विधि पर विचार करें:

 IEnumerator Fade() { Color c = renderer.material.color; for (float alpha = 1f; alpha >= 0; alpha -= 0.1f) { ca = alpha; renderer.material.color = c; yield return new WaitForSeconds(.1f); } }


लूप के प्रत्येक पुनरावृत्ति के साथ, new WaitForSeconds(.1f) का एक नया उदाहरण बनाया जाएगा। इसके बजाय, हम निर्माण को लूप के बाहर स्थानांतरित कर सकते हैं और आवंटन से बच सकते हैं:

 IEnumerator Fade() { Color c = renderer.material.color; **var waitForSeconds = new WaitForSeconds(0.2f);** for (float alpha = 1f; alpha >= 0; alpha -= 0.1f) { ca = alpha; renderer.material.color = c; yield return **waitForSeconds**; } }


नोट करने के लिए एक और महत्वपूर्ण विशेषता यह है कि yield return उपयोग यूनिटी द्वारा प्रदान की गई सभी Async विधियों के साथ किया जा सकता है क्योंकि AsyncOperation YieldInstruction के वंशज हैं:

 yield return SceneManager.LoadSceneAsync("path/to/scene.unity");

कोरटाइन के कुछ संभावित नुकसान

यह सब कहा जा रहा है, कोरटाइन में कुछ कमियां भी हैं:


  • लंबे ऑपरेशन के परिणाम को वापस करना असंभव है। आपको अभी भी कॉलबैक की आवश्यकता है जो कोरटाइन को पास किया जाएगा और जब यह समाप्त हो जाएगा तो इससे किसी भी डेटा को निकालने के लिए कॉल किया जाएगा।
  • एक कोरटाइन सख्ती से MonoBehaviour से जुड़ा होता है जो इसे लॉन्च करता है। यदि GameObject बंद या नष्ट कर दिया जाता है, तो कोरआउटिन संसाधित होना बंद हो जाता है।
  • यील्ड सिंटैक्स की उपस्थिति के कारण try-catch-finally संरचना का उपयोग नहीं किया जा सकता है।
  • अगला कोड निष्पादित होने से पहले yield return के बाद कम से कम एक फ्रेम पास हो जाएगा।
  • लैम्ब्डा और कॉरआउटिन का आवंटन

वादे

वादे अतुल्यकालिक संचालन को अधिक पठनीय बनाने और व्यवस्थित करने के लिए एक पैटर्न हैं। वे कई तृतीय-पक्ष जावास्क्रिप्ट पुस्तकालयों में उनके उपयोग के कारण लोकप्रिय हो गए हैं, और ES6 के बाद से, मूल रूप से लागू किए गए हैं।


वादों का उपयोग करते समय, हम तुरंत आपके अतुल्यकालिक फ़ंक्शन से एक वस्तु लौटाते हैं। यह कॉल करने वाले को ऑपरेशन के समाधान (या त्रुटि) की प्रतीक्षा करने की अनुमति देता है।


अनिवार्य रूप से, यह ऐसा बनाता है कि अतुल्यकालिक तरीके मूल्यों को वापस कर सकते हैं और सिंक्रोनस तरीकों की तरह "कार्य" कर सकते हैं: अंतिम मूल्य को तुरंत वापस करने के बजाय, वे एक "वादा" देते हैं कि वे भविष्य में कभी-कभी एक मूल्य वापस करेंगे।


एकता के लिए कई वादे लागू होते हैं:


प्रॉमिस के साथ इंटरैक्ट करने का मुख्य तरीका कॉलबैक फ़ंक्शंस के माध्यम से है।


आप एक कॉलबैक फ़ंक्शन को परिभाषित कर सकते हैं जिसे कॉल किया जाएगा जब वादा हल हो जाएगा, और एक और कॉलबैक फ़ंक्शन जिसे वादा अस्वीकार कर दिया गया है तो कॉल किया जाएगा। ये कॉलबैक अतुल्यकालिक ऑपरेशन के परिणाम को तर्क के रूप में प्राप्त करते हैं, जिसका उपयोग आगे के संचालन के लिए किया जा सकता है।


प्रॉमिस/ए+ संगठन के इन विनिर्देशों के अनुसार, एक प्रॉमिस तीन राज्यों में से एक में हो सकता है:


  • Pending : प्रारंभिक अवस्था, इसका मतलब है कि अतुल्यकालिक ऑपरेशन अभी भी प्रगति पर है, और ऑपरेशन का परिणाम अभी तक ज्ञात नहीं है।
  • Fulfilled ( Resolved ): हल की गई स्थिति एक मान के साथ होती है जो ऑपरेशन के परिणाम का प्रतिनिधित्व करती है।
  • Rejected : यदि एसिंक्रोनस ऑपरेशन किसी भी कारण से विफल हो जाता है, तो वादा "अस्वीकृत" कहा जाता है। अस्वीकृत स्थिति विफलता के कारण के साथ है।

वादों पर अधिक

इसके अतिरिक्त, वादों को एक साथ जोड़ा जा सकता है, ताकि एक वचन के परिणाम का उपयोग दूसरे वचन के परिणाम को निर्धारित करने के लिए किया जा सके।


उदाहरण के लिए, आप एक वादा बना सकते हैं जो सर्वर से कुछ डेटा प्राप्त करता है, और फिर उस डेटा का उपयोग एक और वादा बनाने के लिए करता है जो कुछ गणना और अन्य क्रियाएं करता है:

 var promise = MakeRequest("https://some.api") .Then(response => Parse(response)) .Then(result => OnRequestSuccess(result)) .Then(() => PlaySomeAnimation()) .Catch(exception => OnRequestFailed(exception));


एसिंक्रोनस ऑपरेशन करने वाली विधि को व्यवस्थित करने का एक उदाहरण यहां दिया गया है:

 public IPromise<string> MakeRequest(string url) { // Create a new promise object var promise = new Promise<string>(); // Create a new web client using var client = new WebClient(); // Add a handler for the DownloadStringCompleted event client.DownloadStringCompleted += (sender, eventArgs) => { // If an error occurred, reject the promise if (eventArgs.Error != null) { promise.Reject(eventArgs.Error); } // Otherwise, resolve the promise with the result else { promise.Resolve(eventArgs.Result); } }; // Start the download asynchronously client.DownloadStringAsync(new Uri(url), null); // Return the promise return promise; }


हम एक Promise में कोरआउटिन भी लपेट सकते हैं:

 void Start() { // Load the scene and then show the intro animation LoadScene("path/to/scene.unity") .Then(() => ShowIntroAnimation()) .Then( ... ); } // Load a scene and return a promise Promise LoadScene(string sceneName) { // Create a new promise var promise = new Promise(); // Start a coroutine to load the scene StartCoroutine(LoadSceneRoutine(promise, sceneName)); // Return the promise return promise; } IEnumerator LoadSceneRoutine(Promise promise, string sceneName) { // Load the scene asynchronously yield return SceneManager.LoadSceneAsync(sceneName); // Resolve the promise once the scene is loaded promise.Resolve(); }


और हां, आप ThenAll / Promise.All और ThenRace / Promise.Race का उपयोग करके प्रॉमिस एक्जीक्यूशन ऑर्डर के किसी भी संयोजन को व्यवस्थित कर सकते हैं:

 // Execute the following two promises in sequence Promise.Sequence( () => Promise.All( // Execute the two promises in parallel RunAnimation("Foo"), PlaySound("Bar") ), () => Promise.Race( // Execute the two promises in a race RunAnimation("One"), PlaySound("Two") ) );

वादों के "अप्रमाणिक" भाग

उपयोग की सभी सुविधाओं के बावजूद, वादों में कुछ कमियाँ भी हैं:


  • ओवरहेड : वादों को बनाने में अतुल्यकालिक प्रोग्रामिंग के अन्य तरीकों की तुलना में अतिरिक्त ओवरहेड शामिल होता है, जैसे कि कोरआउट्स। कुछ मामलों में, इससे प्रदर्शन में कमी आ सकती है।
  • डिबगिंग : डिबगिंग प्रॉमिस अन्य एसिंक्रोनस प्रोग्रामिंग पैटर्न को डिबग करने की तुलना में अधिक कठिन हो सकता है। निष्पादन प्रवाह का पता लगाना और बग के स्रोत की पहचान करना कठिन हो सकता है।
  • एक्सेप्शन हैंडलिंग : अन्य अतुल्यकालिक प्रोग्रामिंग पैटर्न की तुलना में एक्सेप्शन हैंडलिंग प्रॉमिस के साथ अधिक जटिल हो सकती है। प्रॉमिस चेन के भीतर होने वाली त्रुटियों और अपवादों को प्रबंधित करना मुश्किल हो सकता है।

Async/प्रतीक्षा कार्य

Async/प्रतीक्षा सुविधा संस्करण 5.0 (2012) के बाद से C# का हिस्सा रही है, और इसे .NET 4.x रनटाइम के कार्यान्वयन के साथ यूनिटी 2017 में पेश किया गया था।


.NET के इतिहास में, निम्न चरणों को अलग किया जा सकता है:


  1. ईएपी (ईवेंट-आधारित एसिंक्रोनस पैटर्न): यह दृष्टिकोण उन घटनाओं पर आधारित है जो एक ऑपरेशन के पूरा होने पर ट्रिगर होते हैं और एक नियमित विधि जो इस ऑपरेशन को लागू करती है।
  2. APM (एसिंक्रोनस प्रोग्रामिंग मॉडल): यह दृष्टिकोण दो विधियों पर आधारित है। BeginSmth विधि IAsyncResult इंटरफ़ेस लौटाती है। EndSmth विधि IAsyncResult लेती है; यदि EndSmth कॉल के समय ऑपरेशन पूरा नहीं होता है, तो थ्रेड ब्लॉक हो जाता है।
  3. TAP (टास्क-बेस्ड एसिंक्रोनस पैटर्न): इस अवधारणा को async/wait और Task और Task<TResult> टाइप करके बेहतर बनाया गया था।


पिछले दृष्टिकोण की सफलता के कारण पिछले दृष्टिकोण अप्रचलित हो गए थे।

एक अतुल्यकालिक विधि बनाने के लिए, विधि को कीवर्ड async के साथ चिह्नित किया जाना चाहिए, इसमें एक await शामिल है, और वापसी मान Task , Task<T> या void (अनुशंसित नहीं) होना चाहिए।

 public async Task Example() { SyncMethodA(); await Task.Delay(1000); // the first async operation SyncMethodB(); await Task.Delay(2000); // the second async operation SyncMethodC(); await Task.Delay(3000); // the third async operation }


इस उदाहरण में, निष्पादन इस प्रकार होगा:


  1. सबसे पहले, पहले एसिंक्रोनस ऑपरेशन ( SyncMethodA ) को कॉल करने से पहले का कोड निष्पादित किया जाएगा।
  2. पहला एसिंक्रोनस ऑपरेशन await Task.Delay(1000) लॉन्च किया गया है और इसके निष्पादित होने की उम्मीद है। इस बीच, एसिंक्रोनस ऑपरेशन पूरा होने पर कॉल किया जाने वाला कोड ("निरंतरता") सहेजा जाएगा।
  3. पहला एसिंक्रोनस ऑपरेशन पूरा होने के बाद, "निरंतरता" - अगले एसिंक्रोनस ऑपरेशन तक कोड ( SyncMethodB ) निष्पादित करना शुरू कर देगा।
  4. दूसरा एसिंक्रोनस ऑपरेशन ( await Task.Delay(2000) ) लॉन्च किया गया है और इसके निष्पादित होने की उम्मीद है। उसी समय, निरंतरता - दूसरे एसिंक्रोनस ऑपरेशन ( SyncMethodC ) के बाद का कोड संरक्षित रहेगा।
  5. दूसरे एसिंक्रोनस ऑपरेशन के पूरा होने के बाद, SyncMethodC निष्पादित किया जाएगा, इसके बाद निष्पादन और तीसरे एसिंक्रोनस ऑपरेशन के await Task.Delay(3000) इंतजार किया जाएगा।


यह एक सरलीकृत स्पष्टीकरण है, क्योंकि वास्तव में async/प्रतीक्षा सिंटैक्टिक चीनी है जो एसिंक्रोनस विधियों को सुविधाजनक रूप से कॉल करने और उनके पूरा होने की प्रतीक्षा करने की अनुमति देता है।


आप WhenAll और WhenAny उपयोग करके निष्पादन आदेशों के किसी भी संयोजन को व्यवस्थित कर सकते हैं:

 var allTasks = Task.WhenAll( Task.Run(() => { /* ... */ }), Task.Run(() => { /* ... */ }), Task.Run(() => { /* ... */ }) ); allTasks.ContinueWith(t => { Console.WriteLine("All the tasks are completed"); }); var anyTask = Task.WhenAny( Task.Run(() => { /* ... */ }), Task.Run(() => { /* ... */ }), Task.Run(() => { /* ... */ }) ); anyTask.ContinueWith(t => { Console.WriteLine("One of tasks is completed"); });

IAsyncStateMachine

C# कंपाइलर async/प्रतीक्षा कॉल को IAsyncStateMachine स्टेट मशीन में बदल देता है, जो क्रियाओं का एक अनुक्रमिक सेट है जिसे एसिंक्रोनस ऑपरेशन को पूरा करने के लिए किया जाना चाहिए।


हर बार जब आप एक वेट ऑपरेशन कहते हैं, तो स्टेट मशीन अपना काम पूरा कर लेती है और उस ऑपरेशन के पूरा होने का इंतजार करती है, जिसके बाद वह अगले ऑपरेशन को अंजाम देती रहती है। यह अतुल्यकालिक संचालन को मुख्य थ्रेड को अवरुद्ध किए बिना पृष्ठभूमि में निष्पादित करने की अनुमति देता है, और एसिंक्रोनस विधि कॉल को सरल और अधिक पठनीय भी बनाता है।


इस प्रकार, Example विधि एनोटेशन [AsyncStateMachine(typeof(ExampleStateMachine))] के साथ राज्य मशीन बनाने और प्रारंभ करने में परिवर्तित हो जाती है, और राज्य मशीन में स्वयं कई राज्य हैं जो प्रतीक्षा कॉल की संख्या के बराबर हैं।


  • रूपांतरित विधि Example का उदाहरण

     [AsyncStateMachine(typeof(ExampleStateMachine))] public /*async*/ Task Example() { // Create a new instance of the ExampleStateMachine class ExampleStateMachine stateMachine = new ExampleStateMachine(); // Create a new AsyncTaskMethodBuilder and assign it to the taskMethodBuilder property of the stateMachine instance stateMachine.taskMethodBuilder = AsyncTaskMethodBuilder.Create(); // Set the currentState property of the stateMachine instance to -1 stateMachine.currentState = -1; // Start the stateMachine instance stateMachine.taskMethodBuilder.Start(ref stateMachine); // Return the Task property of the taskMethodBuilder return stateMachine.taskMethodBuilder.Task; }


  • जनरेट की गई स्टेट मशीन का उदाहरण ExampleStateMachine

     [CompilerGenerated] private sealed class ExampleStateMachine : IAsyncStateMachine { public int currentState; public AsyncTaskMethodBuilder taskMethodBuilder; private TaskAwaiter taskAwaiter; public int paramInt; private int localInt; void IAsyncStateMachine.MoveNext() { int num = currentState; try { TaskAwaiter awaiter3; TaskAwaiter awaiter2; TaskAwaiter awaiter; switch (num) { default: localInt = paramInt; // Call the first synchronous method SyncMethodA(); // Create a task awaiter for a delay of 1000 milliseconds awaiter3 = Task.Delay(1000).GetAwaiter(); // If the task is not completed, set the current state to 0 and store the awaiter if (!awaiter3.IsCompleted) { currentState = 0; taskAwaiter = awaiter3; // Store the current state machine ExampleStateMachine stateMachine = this; // Await the task and pass the state machine taskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter3, ref stateMachine); return; } // If the task is completed, jump to the label after the first await goto Il_AfterFirstAwait; case 0: // Retrieve the awaiter from the taskAwaiter field awaiter3 = taskAwaiter; // Reset the taskAwaiter field taskAwaiter = default(TaskAwaiter); currentState = -1; // Jump to the label after the first await goto Il_AfterFirstAwait; case 1: // Retrieve the awaiter from the taskAwaiter field awaiter2 = taskAwaiter; // Reset the taskAwaiter field taskAwaiter = default(TaskAwaiter); currentState = -1; // Jump to the label after the second await goto Il_AfterSecondAwait; case 2: // Retrieve the awaiter from the taskAwaiter field awaiter = taskAwaiter; // Reset the taskAwaiter field taskAwaiter = default(TaskAwaiter); currentState = -1; break; Il_AfterFirstAwait: awaiter3.GetResult(); // Call the second synchronous method SyncMethodB(); // Create a task awaiter for a delay of 2000 milliseconds awaiter2 = Task.Delay(2000).GetAwaiter(); // If the task is not completed, set the current state to 1 and store the awaiter if (!awaiter2.IsCompleted) { currentState = 1; taskAwaiter = awaiter2; // Store the current state machine ExampleStateMachine stateMachine = this; // Await the task and pass the state machine taskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter2, ref stateMachine); return; } // If the task is completed, jump to the label after the second await goto Il_AfterSecondAwait; Il_AfterSecondAwait: // Get the result of the second awaiter awaiter2.GetResult(); // Call the SyncMethodC SyncMethodC(); // Create a new awaiter with a delay of 3000 milliseconds awaiter = Task.Delay(3000).GetAwaiter(); // If the awaiter is not completed, set the current state to 2 and store the awaiter if (!awaiter.IsCompleted) { currentState = 2; taskAwaiter = awaiter; // Set the stateMachine to this ExampleStateMachine stateMachine = this; // Await the task and pass the state machine taskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); return; } break; } // Get the result of the awaiter awaiter.GetResult(); } catch (Exception exception) { currentState = -2; taskMethodBuilder.SetException(exception); return; } currentState = -2; taskMethodBuilder.SetResult(); } void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { /*...*/ } }

तुल्यकालन प्रसंग

AwaitUnsafeOnCompleted कॉल में, वर्तमान सिंक्रनाइज़ेशन संदर्भ SynchronizationContext प्राप्त किया जाएगा। सिंक्रोनाइज़ेशन कॉन्टेक्स्ट सी # में एक अवधारणा है जो एक संदर्भ का प्रतिनिधित्व करने के लिए प्रयोग किया जाता है जो एसिंक्रोनस ऑपरेशंस के एक सेट के निष्पादन को नियंत्रित करता है। इसका उपयोग कई थ्रेड्स में कोड के निष्पादन को समन्वित करने और यह सुनिश्चित करने के लिए किया जाता है कि कोड एक विशिष्ट क्रम में निष्पादित हो। सिंक्रोनाइज़ेशन कॉन्टेक्स्ट का मुख्य उद्देश्य मल्टीथ्रेडेड वातावरण में एसिंक्रोनस ऑपरेशंस के शेड्यूलिंग और निष्पादन को नियंत्रित करने का एक तरीका प्रदान करना है।


विभिन्न वातावरणों में, SynchronizationContext अलग-अलग कार्यान्वयन हैं। उदाहरण के लिए, .NET में, हैं:


  • WPF : System.Windows.Threading.DispatcherSynchronizationContext
  • WinForms : System.Windows.Forms.WindowsFormsSynchronizationContext
  • WinRT : System.Threading.WinRTSynchronizationContext
  • ASP.NET : System.Web.AspNetSynchronizationContext


यूनिटी का अपना सिंक्रोनाइज़ेशन संदर्भ भी है, UnitySynchronizationContext , जो हमें प्लेयरलूप एपीआई के साथ बाइंडिंग के साथ एसिंक्रोनस ऑपरेशंस का उपयोग करने में सक्षम बनाता है। निम्न कोड उदाहरण दिखाता है कि Task.Yield() उपयोग करके प्रत्येक फ्रेम में किसी वस्तु को कैसे घुमाया जाए:

 private async void Start() { while (true) { transform.Rotate(0, Time.deltaTime * 50, 0); await Task.Yield(); } }


नेटवर्क अनुरोध करने के लिए एकता में async/प्रतीक्षा का उपयोग करने का एक और उदाहरण:

 using UnityEngine; using System.Net.Http; using System.Threading.Tasks; public class NetworkRequestExample : MonoBehaviour { private async void Start() { string response = await GetDataFromAPI(); Debug.Log("Response from API: " + response); } private async Task<string> GetDataFromAPI() { using (var client = new HttpClient()) { var response = await client.GetStringAsync("https://api.example.com/data"); return response; } } }


UnitySynchronizationContext के लिए धन्यवाद, हम एसिंक्रोनस ऑपरेशन पूरा होने के ठीक बाद UnityEngine विधियों (जैसे Debug.Log() ) का सुरक्षित रूप से उपयोग कर सकते हैं, क्योंकि इस कोड का निष्पादन मुख्य यूनिटी थ्रेड में जारी रहेगा।

कार्य पूर्णता स्रोत <टी>

यह वर्ग आपको Task ऑब्जेक्ट प्रबंधित करने की अनुमति देता है। इसे TAP के लिए पुराने अतुल्यकालिक तरीकों को अनुकूलित करने के लिए बनाया गया था, लेकिन यह तब भी बहुत उपयोगी होता है जब हम किसी घटना पर लंबे समय तक चलने वाले ऑपरेशन के आसपास Task लपेटना चाहते हैं।


निम्नलिखित उदाहरण में, taskCompletionSource अंदर Task ऑब्जेक्ट प्रारंभ से 3 सेकंड के बाद पूरा हो जाएगा, और हम इसका परिणाम Update विधि में प्राप्त करेंगे:

 using System.Threading.Tasks; using UnityEngine; public class Example : MonoBehaviour { private TaskCompletionSource<int> taskCompletionSource; private void Start() { // Create a new TaskCompletionSource taskCompletionSource = new TaskCompletionSource<int>(); // Start a coroutine to wait 3 seconds // and then set the result of the TaskCompletionSource StartCoroutine(WaitAndComplete()); } private IEnumerator WaitAndComplete() { yield return new WaitForSeconds(3); // Set the result of the TaskCompletionSource taskCompletionSource.SetResult(10); } private async void Update() { // Await the result of the TaskCompletionSource int result = await taskCompletionSource.Task; // Log the result to the console Debug.Log("Result: " + result); } }

रद्दीकरण टोकन

एक रद्दीकरण टोकन सी # में संकेत देने के लिए प्रयोग किया जाता है कि एक कार्य या संचालन रद्द किया जाना चाहिए। कार्य या संचालन के लिए टोकन पारित किया जाता है, और कार्य या संचालन के भीतर कोड समय-समय पर टोकन की जांच कर सकता है कि कार्य या संचालन को रोका जाना चाहिए या नहीं। यह किसी कार्य या संचालन को केवल अचानक खत्म करने के बजाय साफ और सुंदर तरीके से रद्द करने की अनुमति देता है।


रद्दीकरण टोकन आमतौर पर उन स्थितियों में उपयोग किए जाते हैं जहां लंबे समय तक चलने वाले कार्य को उपयोगकर्ता द्वारा रद्द किया जा सकता है, या यदि कार्य की अब आवश्यकता नहीं है, जैसे उपयोगकर्ता इंटरफ़ेस में रद्द करें बटन।


समग्र पैटर्न TaskCompletionSource के उपयोग जैसा दिखता है। सबसे पहले, एक CancellationTokenSource बनाया जाता है, फिर उसके Token एसिंक्रोनस ऑपरेशन में पास किया जाता है:

 public class ExampleMonoBehaviour : MonoBehaviour { private CancellationTokenSource _cancellationTokenSource; private async void Start() { // Create a new CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); // Get the token from the CancellationTokenSource CancellationToken token = _cancellationTokenSource.Token; try { // Start a new Task and pass in the token await Task.Run(() => DoSomething(token), token); } catch (OperationCanceledException) { Debug.Log("Task was cancelled"); } } private void DoSomething(CancellationToken token) { for (int i = 0; i < 100; i++) { // Check if the token has been cancelled if (token.IsCancellationRequested) { // Return if the token has been cancelled return; } Debug.Log("Doing something..."); // Sleep for 1 second Thread.Sleep(1000); } } private void OnDestroy() { // Cancel the token when the object is destroyed _cancellationTokenSource.Cancel(); } }


जब ऑपरेशन रद्द कर दिया जाता है, तो एक OperationCanceledException को फेंक दिया जाएगा और Task.IsCanceled गुण को true पर सेट कर दिया जाएगा।

यूनिटी 2022.2 में नई async सुविधाएँ

यह ध्यान रखना महत्वपूर्ण है कि Task ऑब्जेक्ट्स को .NET रनटाइम द्वारा प्रबंधित किया जाता है, न कि यूनिटी द्वारा, और यदि कार्य निष्पादित करने वाली वस्तु नष्ट हो जाती है (या यदि खेल संपादक में प्ले मोड से बाहर निकलता है), तो कार्य एकता के रूप में चलता रहेगा इसे रद्द करने का कोई मतलब नहीं है।


आपको हमेशा await Task के साथ संबंधित CancellationToken की आवश्यकता होती है। यह कोड के कुछ अतिरेक की ओर जाता है, और एकता 2022.2 में MonoBehaviour स्तर पर निर्मित टोकन और संपूर्ण Application स्तर दिखाई दिया।


आइए देखें कि MonoBehaviour ऑब्जेक्ट के destroyCancellationToken का उपयोग करते समय पिछला उदाहरण कैसे बदलता है:

 using System.Threading; using System.Threading.Tasks; using UnityEngine; public class ExampleMonoBehaviour : MonoBehaviour { private async void Start() { // Get the cancellation token from the MonoBehaviour CancellationToken token = this.destroyCancellationToken; try { // Start a new Task and pass in the token await Task.Run(() => DoSomething(token), token); } catch (OperationCanceledException) { Debug.Log("Task was cancelled"); } } private void DoSomething(CancellationToken token) { for (int i = 0; i < 100; i++) { // Check if the token has been cancelled if (token.IsCancellationRequested) { // Return if the token has been cancelled return; } Debug.Log("Doing something..."); // Sleep for 1 second Thread.Sleep(1000); } } }


हमें अब मैन्युअल रूप से CancellationTokenSource बनाने और OnDestroy पद्धति में कार्य को पूरा करने की आवश्यकता नहीं है। उन कार्यों के लिए जो किसी विशेष MonoBehaviour से संबद्ध नहीं हैं, हम UnityEngine.Application.exitCancellationToken का उपयोग कर सकते हैं। प्ले मोड (संपादक में) से बाहर निकलते समय या एप्लिकेशन को छोड़ते समय यह कार्य को समाप्त कर देगा।

यूनीटास्क

उपयोग की सुविधा और .NET कार्यों द्वारा प्रदान की जाने वाली क्षमताओं के बावजूद, एकता में उपयोग किए जाने पर उनमें महत्वपूर्ण कमियां हैं:


  • Task ऑब्जेक्ट बहुत बोझिल हैं और कई आवंटन का कारण बनते हैं।
  • Task यूनिटी थ्रेडिंग (सिंगल-थ्रेड) से मेल नहीं खाता है।


UniTask लाइब्रेरी थ्रेड्स या SynchronizationContext उपयोग किए बिना इन प्रतिबंधों को बायपास करती है। यह UniTask<T> संरचना-आधारित प्रकार का उपयोग करके आवंटन की अनुपस्थिति को प्राप्त करता है।


UniTask को .NET 4.x स्क्रिप्टिंग रनटाइम संस्करण की आवश्यकता होती है, जिसमें यूनिटी 2018.4.13f1 आधिकारिक न्यूनतम समर्थित संस्करण है।


इसके अलावा, आप विस्तार विधियों के साथ सभी AsyncOperations को UnitTask में बदल सकते हैं:

 using UnityEngine; using UniTask; public class AssetLoader : MonoBehaviour { public async void LoadAsset(string assetName) { var loadRequest = Resources.LoadAsync<GameObject>(assetName); await loadRequest.AsUniTask(); var asset = loadRequest.asset as GameObject; if (asset != null) { // Do something with the loaded asset } } }


इस उदाहरण में, LoadAsset पद्धति किसी संपत्ति को एसिंक्रोनस रूप से लोड करने के लिए Resources.LoadAsync का उपयोग करती है। AsUniTask विधि का उपयोग LoadAsync द्वारा लौटाए गए AsyncOperation UniTask में परिवर्तित करने के लिए किया जाता है, जिसका इंतजार किया जा सकता है।


पहले की तरह, आप UniTask.WhenAll और UniTask.WhenAny का उपयोग करके निष्पादन क्रम के किसी भी संयोजन को व्यवस्थित कर सकते हैं:

 using System.Threading; using Cysharp.Threading.Tasks; using UnityEngine; public class Example : MonoBehaviour { private async void Start() { // Start two Tasks and wait for both to complete await UniTask.WhenAll(Task1(), Task2()); // Start two Tasks and wait for one to complete await UniTask.WhenAny(Task1(), Task2()); } private async UniTask Task1() { // Do something } private async UniTask Task2() { // Do something } }


UniTask में, SynchronizationContext का एक और कार्यान्वयन है जिसे UniTaskSynchronizationContext कहा जाता है, जिसका उपयोग बेहतर प्रदर्शन के लिए UnitySynchronizationContext बदलने के लिए किया जा सकता है।

प्रतीक्षित एपीआई

एकता 2023.1 के पहले अल्फा संस्करण में, Awaitable वर्ग पेश किया गया था। Awaitable Coroutines एकता में चलने के लिए डिज़ाइन किए गए async/प्रतीक्षा-संगत कार्य-जैसे प्रकार हैं। .NET टास्क के विपरीत, वे इंजन द्वारा प्रबंधित किए जाते हैं, रनटाइम द्वारा नहीं।

 private async Awaitable DoSomethingAsync() { // awaiting built-in events await Awaitable.EndOfFrameAsync(); await Awaitable.WaitForSecondsAsync(); // awaiting .NET Tasks await Task.Delay(2000, destroyCancellationToken); await Task.Yield(); // awaiting AsyncOperations await SceneManager.LoadSceneAsync("path/to/scene.unity"); // ... }


उन्हें प्रतीक्षा की जा सकती है और एसिंक विधि के रिटर्न प्रकार के रूप में उपयोग किया जा सकता है। System.Threading.Tasks की तुलना में, वे कम परिष्कृत हैं लेकिन एकता-विशिष्ट धारणाओं के आधार पर प्रदर्शन-बढ़ाने वाले शॉर्टकट लेते हैं।


यहाँ .NET कार्यों की तुलना में मुख्य अंतर हैं:


  • Awaitable वस्तु केवल एक बार प्रतीक्षित हो सकती है; यह एकाधिक एसिंक्स कार्यों द्वारा प्रतीक्षा नहीं की जा सकती है।
  • Awaiter.GetResults() पूरा होने तक अवरुद्ध नहीं होगा। ऑपरेशन समाप्त होने से पहले इसे कॉल करना अपरिभाषित व्यवहार है।
  • ExecutionContext को कभी भी कैप्चर न करें। सुरक्षा कारणों से, .NET कार्य अतुल्यकालिक कॉलों में प्रतिरूपण संदर्भों को प्रचारित करने के लिए प्रतीक्षा करते समय निष्पादन संदर्भों को कैप्चर करता है।
  • SynchronizationContext कभी कैप्चर न करें। Coroutine निरंतरताओं को उस कोड से समकालिक रूप से निष्पादित किया जाता है जो पूर्णता को बढ़ाता है। ज्यादातर मामलों में, यह यूनिटी मेन फ्रेम से होगा।
  • अत्यधिक आवंटन को रोकने के लिए प्रतीक्षारत पूलित वस्तुएं हैं। ये संदर्भ प्रकार हैं, इसलिए इन्हें अलग-अलग ढेरों में संदर्भित किया जा सकता है, कुशलतापूर्वक कॉपी किया जा सकता है, और इसी तरह। एसिंक्स राज्य मशीनों द्वारा उत्पन्न विशिष्ट गेट/रिलीज़ अनुक्रमों में Stack<T> बाउंड चेक से बचने के लिए ObjectPool सुधार किया गया है।


लंबे ऑपरेशन का परिणाम प्राप्त करने के लिए, आप Awaitable<T> प्रकार का उपयोग कर सकते हैं। आप AwaitableCompletionSource और AwaitableCompletionSource<T> का उपयोग करके Awaitable को पूरा करने का प्रबंधन कर सकते हैं , जो कि TaskCompletitionSource के समान है:

 using UnityEngine; using Cysharp.Threading.Tasks; public class ExampleBehaviour : MonoBehaviour { private AwaitableCompletionSource<bool> _completionSource; private async void Start() { // Create a new AwaitableCompletionSource _completionSource = new AwaitableCompletionSource<bool>(); // Start a coroutine to wait 3 seconds // and then set the result of the AwaitableCompletionSource StartCoroutine(WaitAndComplete()); // Await the result of the AwaitableCompletionSource bool result = await _completionSource.Awaitable; // Log the result to the console Debug.Log("Result: " + result); } private IEnumerator WaitAndComplete() { yield return new WaitForSeconds(3); // Set the result of the AwaitableCompletionSource _completionSource.SetResult(true); } }


कभी-कभी बड़े पैमाने पर गणना करना आवश्यक होता है जिससे गेम फ्रीज हो सकता है। इसके लिए, प्रतीक्षा योग्य विधियों का उपयोग करना बेहतर है: BackgroundThreadAsync() और MainThreadAsync() । वे आपको मुख्य धागे से बाहर निकलने और उस पर लौटने की अनुमति देते हैं।

 private async Awaitable DoCalculationsAsync() { // Awaiting execution on a ThreadPool background thread. await Awaitable.BackgroundThreadAsync(); var result = PerformSomeHeavyCalculations(); // Awaiting execution on the Unity main thread. await Awaitable.MainThreadAsync(); // Using the result in main thread Debug.Log(result); }


इस तरह, Awaitables .NET कार्य का उपयोग करने की कमियों को दूर करता है और साथ ही PlayerLoop ईवेंट और AsyncOperations की प्रतीक्षा करने की अनुमति देता है।

निष्कर्ष

जैसा कि हम देख सकते हैं, एकता के विकास के साथ, अतुल्यकालिक संचालन के आयोजन के लिए अधिक से अधिक उपकरण हैं:

एकता

कोरूटिन्स

वादे

.NET कार्य

यूनीटास्क

अंतर्निहित रद्दीकरण टोकन

प्रतीक्षित एपीआई

5.6





2017.1




2018.4



2022.2


2023.1


हमने एकता में अतुल्यकालिक प्रोग्रामिंग के सभी मुख्य तरीकों पर विचार किया। आपके कार्य की जटिलता और आपके द्वारा उपयोग की जा रही यूनिटी के संस्करण के आधार पर, आप अपने गेम में एक सहज और निर्बाध गेमप्ले सुनिश्चित करने के लिए कॉरूटीन्स और प्रॉमिस से लेकर टास्क और प्रतीक्षारत तक तकनीकों की एक विस्तृत श्रृंखला का उपयोग कर सकते हैं। पढ़ने के लिए धन्यवाद, और हम आपकी अगली उत्कृष्ट कृतियों की प्रतीक्षा कर रहे हैं।