لماذا 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 الذي يعاني من مشكلتين حرجتين:
- توقف العالم stop-the-world — عندما يعمل GC، تتجمد لعبتك. توقف مدته 2 مللي ثانية عند 60 إطاراً في الثانية يستهلك 12% من ميزانية الإطار؛ وعند 120 إطاراً في الثانية (VR) يستهلك 24%.
- التوقيت غير المتوقع — قد يُشغَّل GC أثناء معركة بوس، أو مشهد سينمائي، أو مباراة تنافسية.
ما يفعله Valkarn Tasks بشكل مختلف
| السيناريو | System.Task | UniTask | Valkarn 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
| المعيار | ValueTask | UniTask | Valkarn Tasks | مقابل ValueTask |
|---|---|---|---|---|
| 100 مهمة بالجملة | 956 ns | 508 ns | 489 ns | 1.95× |
| 1,000 مهمة بالجملة | 9,697 ns | 5,016 ns | 4,728 ns | 2.05× |
| مع CancellationToken | 38.8 ns | 36.2 ns | 29.6 ns | 1.31× |
| معالجة الاستثناءات | 10,399 ns | 8,247 ns | 9,248 ns | 1.12× |
جميع المسارات: 0 بايت مخصصة.
المدمجات Combinators — أسرع بمقدار 9.6× من Task
| المعيار | Task | UniTask | Valkarn Tasks | مقابل Task |
|---|---|---|---|---|
| WhenAll (مهمتان) | 117 ns / 232B | 13.6 ns / 0B | 12.1 ns / 0B | 9.6× |
| WhenAll (5 مهام) | 156 ns / 272B | 25.1 ns / 0B | 25.3 ns / 0B | 6.2× |
| WhenAny (مهمتان) | 39.0 ns / 144B | 60.0 ns / 72B | 11.6 ns / 0B | 3.4× |
| Pooled Promise | 59.5 ns / 144B | 53.6 ns / 0B | 38.3 ns / 0B | 1.55× |
WhenAny أسرع بـ 5.2× من UniTask ولا يخصص أي بايت.
مجمع الكائنات Object pool — 4.3 نانوثانية
| العملية | الوقت | التخصيص |
|---|---|---|
| استعارة + إرجاع الفتحة السريعة في الخيط الرئيسي | 4.3 ns | 0 بايت |
| Treiber stack عبر الخيوط | ~15 ns | 0 بايت |
صفر عمليات ذرية atomic على الخيط الرئيسي — أمر حيوي لـ IL2CPP حيث يكون Volatile.Read أبطأ بمقدار 9.2×.
التأثير في العالم الحقيقي (50 عملية async/إطار عند 60 fps)
| المكتبة | الوقت/إطار | ميزانية الإطار | GC/ثانية |
|---|---|---|---|
| System.Task | ~48 µs | 0.29% | ~430 KB/s |
| UniTask | ~25 µs | 0.15% | ~3.6 KB/s |
| Valkarn Tasks | ~24 µs | 0.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(); // قابل للإلغاء مجدداً
}
تكتمل عمليات الكتابة على قاعدة البيانات وطلبات الشبكة وحفظ الملفات حتى عندما يغادر اللاعب أو تُفرَّغ مشهد. لا حفظ تالف. لا تحليلات نصف مكتوبة. لا إيصالات مفقودة.
التشخيصات في وقت الترجمة
| التشخيص | ما يكتشفه |
|---|---|
| TT001 | Double-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 Awaitable→async ValkarnTaskAwaitable.NextFrameAsync()→ValkarnTask.NextFrame()Awaitable.BackgroundThreadAsync()→ValkarnTask.SwitchToThreadPool()Awaitable.MainThreadAsync()→ محذوف (يعمل Valkarn على الخيط الرئيسي افتراضياً)
مصفوفة المقارنة
| الميزة | System.Task | UniTask | Awaitable | Valkarn 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 بدون تعثرات.