Last active
November 2, 2023 10:07
-
-
Save overing/5f9c27361a223f81894e9b0d6ec9a595 to your computer and use it in GitHub Desktop.
從同步上下文捕捉 TaskScheduler 用來將外線中的工作送回主線
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Threading; | |
using System.Threading.Tasks; | |
using UnityEngine; | |
public sealed class Main : MonoBehaviour | |
{ | |
static int ThreadId => Environment.CurrentManagedThreadId; | |
GameObject _object; | |
CancellationTokenSource _cancelTokenSource; | |
TaskScheduler _scheduler; | |
void Awake() | |
{ | |
Debug.LogWarning(new { ThreadId, Method = nameof(Awake) }); | |
// 建立一個球體遊戲物件用來展示 有正確回到主線 (因為只有 Unity 主線才可以修改 gameObject.transform) | |
_object = GameObject.CreatePrimitive(PrimitiveType.Sphere); | |
// 從目前 Unity 的同步上下文擷取工作排程器用來給其他線可以將工作排回主線執行 | |
_scheduler = TaskScheduler.FromCurrentSynchronizationContext(); | |
} | |
void Start() | |
{ | |
Debug.LogWarning(new { ThreadId, Method = nameof(Start) }); | |
// 生成取消權杖源用來在物件被銷毀時取消所有工作 | |
_cancelTokenSource = new CancellationTokenSource(); | |
var token = _cancelTokenSource.Token; | |
// 將非同步方法 BackgroundWorkAsync 排入 ThreadPool (表示脫離 Unity 主線; 在外線進行計算) | |
Task.Run(() => BackgroundWorkAsync(token), token); | |
} | |
/// <summary> | |
/// 將被丟進外線 (ThreadPool) 執行的方法 | |
/// </summary> | |
async Task BackgroundWorkAsync(CancellationToken token) | |
{ | |
Debug.LogWarning(new { ThreadId, Method = nameof(BackgroundWorkAsync) }); | |
var random = new System.Random(); | |
while (!token.IsCancellationRequested) // 如果權杖還沒被取消表示要繼續執行 | |
{ | |
// 亂數產生 移動速度 與 二維偏移量 作為移動遊戲物件的參數 | |
var offset = new Vector2(random.Next(-300, 300) / 100f, random.Next(-300, 300) / 100f); | |
var speed = (float)(random.NextDouble() * 3 + 3); | |
// 將非同步方法 MainThreadWorkAsync 用主線排程器排回主線執行 | |
// 並等待傳回值 表示在兩個不同線的方法可以雙向傳遞資料 (參數過去 + 返回值) | |
var result = await Task.CompletedTask.ContinueWith(_ => MainThreadWorkAsync(offset, speed, token), _scheduler).Unwrap(); | |
Debug.LogWarning(new { ThreadId, Method = nameof(BackgroundWorkAsync) + " after main thread work done: " + result }); | |
await Task.Delay(TimeSpan.FromSeconds(3), token); // 等 3 秒才開始下一次移動 | |
} | |
} | |
/// <summary> | |
/// 將從外線觸發排程回主線執行的方法 | |
/// </summary> | |
async Task<Vector2> MainThreadWorkAsync(Vector2 offset, float speed, CancellationToken token) | |
{ | |
Debug.LogWarning(new { ThreadId, Method = nameof(MainThreadWorkAsync), offset, speed }); | |
var targetPosition = _object.transform.position + (Vector3)offset; | |
while (!token.IsCancellationRequested) // 如果權杖還沒被取消表示要繼續執行 | |
{ | |
// 將遊戲物件用參數指定的速度跟偏移量進行移動 | |
var step = speed * Time.deltaTime; | |
// Unity 只允許 gameObject.transform 在主線上被修改; 這證明這方法已經回到主線 | |
_object.transform.position = Vector3.MoveTowards(_object.transform.position, targetPosition, step); | |
// 如果與目標點的差距小於浮點偏差就當作已經到達目的地 退出迴圈 | |
if (Vector3.Distance(_object.transform.position, targetPosition) < Mathf.Epsilon) | |
break; | |
await Task.Yield(); // 等一個中斷讓畫面更新; 類似 coroutine 寫法的 yield return null | |
} | |
return _object.transform.position; // 傳回目前物件位置 | |
} | |
void OnDestroy() | |
{ | |
Debug.LogWarning(new { ThreadId, Method = nameof(OnDestroy) }); | |
_cancelTokenSource.Cancel(); // 取消權杖源讓所有工作停止 | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
參考 output