跳至内容
Unity Async غير المتزامن، ما الفرق حقًا مع .Net الأصلي؟

Unity Async غير المتزامن، ما الفرق حقًا مع .Net الأصلي؟

2024-12-05 19:55

لن أشرح ما هو Async/Await، فهو يشبه المواكبات (Coroutines) ولكنه أكثر إيجازًا، وأفترض هنا أن لديك معرفة بالبرمجة غير المتزامنة الحديثة.

لخص التنفيذ الداخلي لـ Unity لتسهيل فهم ما يحدث بالضبط. ببساطة، يقوم Unity بمعالجات داخلية لضمان نقطتين، عندما يكون المتصل هو الخيط الرئيسي: 1. لن يحدث تبديل خيط للمتصل، 2. لن تُنفَّذ الدالة غير المتزامنة المُستدعاة على خيط آخر.

هدف Unity من نمط Async

نمط Async أكثر حداثة وهو الاتجاه المستقبلي، ولكن Unity لا تخطط حاليًا لاستبدال المواكبات (IEnumerators) بـ Async، والهدف الرئيسي هو استخدامه في عمليات من نوع الإدخال/الإخراج (IO) وما شابه.

أداء Unity Async أعلى من المواكبات، لكن درجة التوازي أقل

مناسب للاستخدام من قبل كائنات مفردة/رئيسية، مثل الكاميرا، الواجهة، طرق IO، إلخ. عدد قليل من Async يكون أداؤه أعلى من المواكبات، بالنسبة للكود الذي يحتاج إلى تشغيل متوازي لعدد كبير من الكائنات (Object)، لا يزال يجب استخدام المواكبات.

قسم الأداء

حاول إرجاع Awaitable المغلف من Unity، وليس Task (Unity6)

يستخدم .Net Async Task للقيام بذلك، كما يلي:

  async Task function() {
        await Task.Delay(1000);
  }

إن Task الأصلي من .Net ينشئ كائنًا جديدًا في كل مرة يُرجع فيها، بينما كائن Awaitable المغلف من Unity هو مشترك، وبالتالي ضغط GC أقل. كما أن لـ Awaitable ميزة “مجانية” (بدون خسائر) لتبديل الخيوط، انظر القسم التالي.

  async Awaitable function() {
        while(true) {
            await Awaitable.NextFrameAsync();
        }
  }

للطرق التي تحتاج إلى await بشكل متكرر، مشابهة لرمز المثال أعلاه، يرجى استخدام القيمة المرجعة Awaitable. بالطبع، الطرق التي تستخدم await لمرة واحدة مثل IO لا تهم.

نظرًا لأن كائن Awaitable مشترك (يُستخدم من قبل الآخرين بعد await)، لا يمكن إعادة await عليه (كما في الكود أدناه)، مما سيؤدي إلى تعليق (deadlock).

async Awaitable function() {
        while(true) {
            var returned_awaitable = Awaitable.NextFrameAsync();
            await returned_awaitable;
            await returned_awaitable; //deadlock
        }
  }

Unity تدير سياق Async (التبديل) داخليًا، وهذا يختلف عن .Net

المفهوم الرئيسي لـ Async، الاستئناف Resume، يشير إلى العودة والاستمرار في تنفيذ الكود بعد await.

“السياق” يحدد كيفية استئناف كود await. يستخدم .Net مجموعة خيوط (thread pool) للتبديل بين الأكواد غير المتزامنة المختلفة. مثل كود .Net التالي:

using System;

async Task function() {
    Console.WriteLine($"task start: Tid {Environment.CurrentManagedThreadId}");
    await Task.Delay(1000);
    Console.WriteLine($"task end: Tid {Environment.CurrentManagedThreadId}");
}

Console.WriteLine($"caller start, Tid{Environment.CurrentManagedThreadId}");
await function();
Console.WriteLine($"caller end, Tid{Environment.CurrentManagedThreadId}");

نتيجة التنفيذ:

caller start, Tid12
task start: Tid 12
task end: Tid 38
caller end, Tid38

يمكن ملاحظة أن تبديل الخيط حدث لكل من المتصل والمنفذ.

أما Unity، لضمان أن الطرق المستدعاة من الخيط الرئيسي تُنفَّذ فقط في الخيط الرئيسي، قامت بتغليف طريقة سياق تسمى UnitySynchronizationContext (هذا تنفيذ داخلي، لا نحتاج للتعامل معه، فقط نعرفه).

لذلك لا داعي للقلق بشأن مشاكل الخيوط، سواء كانت القيمة المرجعة هي Awaitable أو Task من .Net، طالما تم الاستدعاء من الخيط الرئيسي، فسيتم إدارتها بواسطة UnitySynchronizationContext وضمان تنفيذها على الخيط الرئيسي.

لكن دالة Async التي ترجع قيمة Task، عند استئناف Resume، يمكنها الاستئناف فقط في الإطار التالي وليس فورًا، كما أن Unity تحتاج إلى خطوة إضافية لاستبدال سياق .Net، وبالتالي يكون أداء Awaitable أعلى.

بالطبع، دوال Async المستدعاة من خيوط أخرى والتي لا تحتاج إلى قيود الخيط الرئيسي، ستستمر في استخدام مجموعة خيوط .Net.

أنماط الاستخدام

نمط “أطلق وانس” (Fire-and-Forget)

عند استدعاء دالة Async، يمكن عدم استخدام await، وسيستمر النظام داخليًا في تنفيذ دالة Async تلك.

async Awaitable worker() {
    await Awaitable.WaitForSecondsAsync(5);
    Debug.Log($"worker end, frame {Time.frameCount}");
}

void caller() {
    _ = worker();
    Debug.Log($"caller end, frame {Time.frameCount}");
}

النتيجة:

caller end, frame 1
worker end, frame 292

لم يكتب caller await، لكن worker سيستمر في التنفيذ، فقط دون انتظار اكتماله.

إذا كان في .Net، فسيستمر تنفيذ worker في مجموعة الخيوط، أما في Unity فيستمر تنفيذه بواسطة حلقة الخيط الرئيسي (main thread loop).

يُستخدم هذا النمط أيضًا بشكل شائع في استدعاء دالة Async غير متزامنة من داخل دالة Sync متزامنة عادية.

ملاحظة: هذه الطريقة تعادل async void، تُنفَّذ في أعلى مستوى (تفقد المكدس)، ولن تكون الاستثناءات في الكود مرئية، وتظهر كما لو أن الكود لم يُنفَّذ، وليس أن هذا النمط غير قابل للاستخدام.

يمكن لـ Unity استخدام Async لتنفيذ برمجة متعددة الخيوط بسهولة

من خلال هذا يمكن تنفيذ كود متعدد الخيوط بسهولة، دون الحاجة إلى Job وما شابه. لجعل أي جزء من الكود ينفذ على خيط مختلف.

إذا كنت ترغب في التبديل إلى مجموعة خيوط الخلفية (مشابهة لتنفيذ مجموعة خيوط .Net)، أو التبديل إلى الخيط الرئيسي، يمكنك التحديد يدويًا. مثل:

async Awaitable function() {
    await Awaitable.BackgroundThreadAsync();
    ...
    return null;
}

الكود بعد سطر await هذا، حتى نهاية الدالة، سينفذ على مجموعة خيوط .Net المُدارة في الخلفية.

إذا كان المتصل هو الخيط الرئيسي، فلن يغير التبديل خيط المتصل، وهذا مريح للغاية. لاحظ أن التبديل يحتاج على الأقل إلى إطار واحد من الوقت.

التبديل إلى الخيط الرئيسي يكون بـ await Awaitable.MainThreadAsync();، وعمومًا لا حاجة لاستدعائه.

ولكن لسوء الحظ، في WebGL (wasm)، لا يمكنك استخدام تعدد الخيوط في .Net، بما في ذلك أي طريقة تستخدم Task، وكذلك Awaitable.BackgroundThreadAsync().

يمكن تعريف دالة Async على أنها void، لتسهيل استدعاءات event المختلفة

إذا كنت تريد أن ينفذ رد الاتصال (callback) دالتك Async، فلن تحتاج إلى تغليفها بدالة عادية لاستدعاء دالتك غير المتزامنة، يمكن استخدام async void، كما يلي:

async void function(CallbackContext ctx) {
    try{
        ...do something...
        await Awaitable.WaitForSecondsAsync(5);
    }catch (Exception e){
        Debug.LogError(e);
    }
}

void Start() {
    InputAction.performed += function;
}

لاحظ أن async void تُنفَّذ في أعلى مستوى، لذلك لن تُكتشف استثناءات الدالة من الخارج، ويجب عليك معالجتها بنفسك باستخدام catch.

الشكل غير المتزامن لأحداث MonoBehaviour

يمكن استخدام Start، Update، وغيرها في MonoBehaviour بنمط async، مثل: async Awaitable Start() {}، لكن تذكر أن Start أو Awake غير المتزامنين، قبل اكتمال تنفيذهما، سيتم استدعاء Update.

آخر تعديل
hugo-builder
hugo-builder · · 自动翻译 2024-12-05... · 55a10c6
مساهمون آخرون
...