إنتقل إلى المحتوى الرئيسي

لماذا Valkarn Tasks

async/await بدون تخصيص للذاكرة في Unity. أسرع من UniTask. أذكى من Awaitable.


المشكلة: البرمجة غير المتزامنة في Unity معطوبة

يحتاج مطورو Unity إلى العمليات غير المتزامنة في كل مكان: تحميل المشاهد، تنزيل الأصول، انتظار الرسوم المتحركة، تأخير الإنتاج، التواصل مع الخوادم. اليوم، الخيارات المتاحة هي:

الخيارالمشكلة
Coroutinesلا قيم إرجاع، ولا معالجة للأخطاء، ولا إلغاء، ولا تركيب
System.Threading.Tasksيخصص ذاكرة عند كل استدعاء async (~144–232 بايت)، يُشغّل جامع القمامة GC، ولا وعي بدورة حياة Unity
UniTaskجيد — لكن: تصادم الرمز المميز بعد 18 دقيقة، لا تشخيصات في وقت الترجمة، الإبلاغ عن الأخطاء يعتمد على finalizer
Unity Awaitable (2023+)قائم على الكلاس (يخصص ذاكرة)، لا مدمجات combinators، لا قنوات channels، لا دعم للاختبار

يحل Valkarn Tasks جميع هذه المشاكل في حزمة واحدة مُولَّدة من الكود المصدري بدون تخصيص للذاكرة.


صفر تخصيصات — لماذا كل بايت مهم

ما يعنيه تخصيص الذاكرة للألعاب

كل new object() أو new List<T>() أو استدعاء async Task يخصص ذاكرة على الكومة المُدارة، التي يتتبعها جامع القمامة. يستخدم Unity نظام Boehm GC الذي يعاني من مشكلتين حرجتين:

  1. توقف العالم stop-the-world — عندما يعمل GC، تتجمد لعبتك. توقف مدته 2 مللي ثانية عند 60 إطاراً في الثانية يستهلك 12% من ميزانية الإطار؛ وعند 120 إطاراً في الثانية (VR) يستهلك 24%.
  2. التوقيت غير المتوقع — قد يُشغَّل GC أثناء معركة بوس، أو مشهد سينمائي، أو مباراة تنافسية.

ما يفعله Valkarn Tasks بشكل مختلف

السيناريوSystem.TaskUniTaskValkarn Tasks
async Method() — يكتمل بشكل متزامن144 بايت0 بايت0 بايت
async Method() — يتوقف مرة واحدة232+ بايت0 بايت (pooled)0 بايت (pooled)
WhenAll(a, b)232 بايت0 بايت (pooled)0 بايت (source-gen pooled)
WhenAny(a, b)144 بايت72 بايت0 بايت
Promise (إكمال يدوي)144 بايت104 بايت88 بايت
Pooled Promise (قابل لإعادة الاستخدام)144 بايت0 بايت0 بايت

في إطار لعبة نموذجي مع 50–100 عملية async، يُولّد System.Task من 7 إلى 23 كيلوبايت من القمامة. يُولّد Valkarn Tasks صفراً.

ما يعنيه ذلك للعبتك

  • لا تعثّرات GC — معدل إطارات ثابت بدون انقطاعات من العمليات غير المتزامنة
  • آمن للواقع الافتراضي VR — أهداف 90/120 إطار في الثانية بدون ارتفاعات مفاجئة لـ GC
  • مناسب للهواتف المحمولة — ضغط ذاكرة أقل على الأجهزة ذات RAM المحدودة
  • معتمد للكونسول — سلوك ذاكرة متوقع يساعد في اجتياز متطلبات الاعتماد

الأداء — مقارنة بالأفضل

المعايير: BenchmarkDotNet v0.14.0، .NET 9.0، Intel Core i7-10875H.

Core async/await — أسرع بمقدار 2× من ValueTask

المعيارValueTaskUniTaskValkarn Tasksمقابل ValueTask
100 مهمة بالجملة956 ns508 ns489 ns1.95×
1,000 مهمة بالجملة9,697 ns5,016 ns4,728 ns2.05×
مع CancellationToken38.8 ns36.2 ns29.6 ns1.31×
معالجة الاستثناءات10,399 ns8,247 ns9,248 ns1.12×

جميع المسارات: 0 بايت مخصصة.

المدمجات Combinators — أسرع بمقدار 9.6× من Task

المعيارTaskUniTaskValkarn Tasksمقابل Task
WhenAll (مهمتان)117 ns / 232B13.6 ns / 0B12.1 ns / 0B9.6×
WhenAll (5 مهام)156 ns / 272B25.1 ns / 0B25.3 ns / 0B6.2×
WhenAny (مهمتان)39.0 ns / 144B60.0 ns / 72B11.6 ns / 0B3.4×
Pooled Promise59.5 ns / 144B53.6 ns / 0B38.3 ns / 0B1.55×

WhenAny أسرع بـ 5.2× من UniTask ولا يخصص أي بايت.

مجمع الكائنات Object pool — 4.3 نانوثانية

العمليةالوقتالتخصيص
استعارة + إرجاع الفتحة السريعة في الخيط الرئيسي4.3 ns0 بايت
Treiber stack عبر الخيوط~15 ns0 بايت

صفر عمليات ذرية atomic على الخيط الرئيسي — أمر حيوي لـ IL2CPP حيث يكون Volatile.Read أبطأ بمقدار 9.2×.

التأثير في العالم الحقيقي (50 عملية async/إطار عند 60 fps)

المكتبةالوقت/إطارميزانية الإطارGC/ثانية
System.Task~48 µs0.29%~430 KB/s
UniTask~25 µs0.15%~3.6 KB/s
Valkarn Tasks~24 µs0.14%0 KB/s

خلال 10 دقائق، يُولّد System.Task ~258 ميغابايت من القمامة غير المتزامنة. يُولّد Valkarn Tasks صفراً.


ميزات لا تمتلكها أي مكتبة أخرى

إلغاء دورة الحياة التلقائي

public partial class EnemySpawner : MonoBehaviour
{
async ValkarnTask Start()
{
while (true)
{
await ValkarnTask.Delay(3000);
SpawnWave();
}
// يُلغى تلقائياً عند تدمير هذا الـ GameObject.
// لا CancellationToken. لا تسرب ذاكرة. لا مهام زومبي.
}
}

لا مزيد من MissingReferenceException من طرق async تعمل بعد التدمير. لا تنظيف يدوي في OnDestroy. لا CancellationTokenSource منسية لم يتم التخلص منها.

الأقسام الحرجة

async ValkarnTask SaveProgress()
{
var data = await LoadPlayerData(); // قابل للإلغاء

await using (ValkarnTask.Critical())
{
await db.Insert(data); // يكتمل حتى لو تم تدمير GO
await db.Commit();
} // يُطبَّق الإلغاء المعلق الآن

await SendNotification(); // قابل للإلغاء مجدداً
}

تكتمل عمليات الكتابة على قاعدة البيانات وطلبات الشبكة وحفظ الملفات حتى عندما يغادر اللاعب أو تُفرَّغ مشهد. لا حفظ تالف. لا تحليلات نصف مكتوبة. لا إيصالات مفقودة.

التشخيصات في وقت الترجمة

التشخيصما يكتشفه
TT001Double-await على ValkarnTask (ثغرة use-after-free)
TT002نسيان انتظار مهمة (فشل صامت)
TT012حلقات async بدون فحوصات إلغاء (حلقات زومبي)
TT013مهمة مُرجَعة لكن لم تُنتظر (ثغرة fire-and-forget)
TT016طريقة async بدون await (عبء غير ضروري)
TT017[FireAndForget] على ValkarnTask<T> (تجاهل نتيجة)

أخطاء يتم اكتشافها في بيئة التطوير IDE كخطوط حمراء — وليس أعطال وقت التشغيل بعد 20 دقيقة من الاختبار.

Result<T> — معالجة الأخطاء بدون try/catch

var result = await loadTask.AsResult();

if (result.Succeeded) Use(result.Value);
else if (result.IsCanceled) ShowRetryDialog();
else Debug.LogError(result.Error);

كل مسار خطأ صريح. لا استثناءات مُبتلَعة. لا معالجات مفقودة.

القنوات مع backpressure

var channel = ValkarnTask.Channel.CreateBounded<EnemySpawnRequest>(capacity: 10);

// المنتج (منطق اللعبة)
await channel.Writer.WriteAsync(new EnemySpawnRequest { type = EnemyType.Dragon });

// المستهلك (نظام الإنتاج)
await foreach (var request in channel.Reader.ReadAllAsync())
await SpawnEnemy(request);

فصل الأنظمة بشكل نظيف. تحديد معدل الإنتاج. وضع رسائل الشبكة في قائمة انتظار. تخزين أحداث الإدخال مؤقتاً. يتباطأ المنتج عندما لا يستطيع المستهلك مواكبته — مما يمنع ارتفاعات الذاكرة.

الاختبار الحتمي مع TestClock

[Test]
public void Respawn_WaitsThreeSeconds()
{
var clock = new TestClock();
var task = spawner.RespawnEnemy();

clock.Advance(TimeSpan.FromSeconds(2));
Assert.IsFalse(task.IsCompleted);

clock.Advance(TimeSpan.FromSeconds(1));
Assert.IsTrue(task.IsCompleted);
}

اختبر المنطق الزمني فورياً. لا yield return new WaitForSeconds(3) في الاختبارات. لا توقيت CI غير موثوق.

أمان الرمز المميز الجيلي

يستخدم UniTask رمزاً مميزاً short (16 بت). بعد 65,536 دورة pool (~18 دقيقة من العمل غير المتزامن النشط)، تقرأ مرجع قديم بصمت نتيجة مهمة أخرى — ثغرة use-after-free يكاد يكون من المستحيل إعادة إنتاجها.

يستخدم Valkarn Tasks عداد جيل uint لكل فتحة pool: 4,294,967,296 دورة لكل فتحة قبل التصادم. مستحيل في أي سيناريو واقعي.


الترحيل في دقائق، لا أسابيع

من UniTask

الخطوة 1: ثبّت Valkarn Tasks
الخطوة 2: تظهر مصابيح صفراء على استخدامات UniTask في بيئة التطوير
الخطوة 3: انقر بزر الماوس الأيمن ← "Fix all occurrences in Solution" (Ctrl+.)
الخطوة 4: أزل مرجع حزمة UniTask

15 تشخيصاً للترحيل (MIG001–MIG015) تغطي كل واجهة برمجية لـ UniTask تلقائياً. يُرحَّل مشروع نموذجي يحتوي على 500–2,000 طريقة async في أقل من 5 دقائق. أكثر من 95% آلي بالكامل.

من Unity Awaitable

نفس الترحيل بنقرة واحدة:

  • async Awaitableasync ValkarnTask
  • Awaitable.NextFrameAsync()ValkarnTask.NextFrame()
  • Awaitable.BackgroundThreadAsync()ValkarnTask.SwitchToThreadPool()
  • Awaitable.MainThreadAsync() → محذوف (يعمل Valkarn على الخيط الرئيسي افتراضياً)

مصفوفة المقارنة

الميزةSystem.TaskUniTaskAwaitableValkarn Tasks
مسار متزامن بدون تخصيصلانعملانعم
مدمجات بدون تخصيصلالالانعم (source gen)
قائم على structلانعملانعم
إلغاء تلقائي لدورة الحياةلايدويجزئيتلقائي
لا إلغاء للمهام الشقيقةلالالانعم
الأقسام الحرجةلالالانعم
Result<T> (بدون رمي استثناء)لاجزئيلانعم
TestClockلالالانعم
جسر نظام Jobلالالانعم
تشخيصات وقت الترجمةلالالانعم (17 قاعدة)
مجمعات محدودة + قصلالالا/ينطبقنعم
إبلاغ حتمي عن الأخطاءلالا (finalizer)جزئينعم (pool return)
قنوات كاملةنعم (.NET)محدودلانعم
جسر Awaitableلا/ينطبقمحدودأصليشفاف
تجميع IL2CPP محسّنلالا (Volatile لكل عملية)لا/ينطبقنعم (صفر ذري)
أمان تصادم الرمز المميزلا/ينطبق18 دقيقة (short)لا/ينطبقأبداً (uint gen)
ترحيل تلقائي من UniTaskلا/ينطبقلا/ينطبقلا/ينطبقنعم (15 إصلاح)
ترحيل تلقائي من Awaitableلا/ينطبقلا/ينطبقلا/ينطبقنعم (8 إصلاحات)

لعبتك تستحق async بدون تعثرات.