Files
XRLib/Assets/Scripts/UVC/Threading/MainThreadDispatcher.cs
2025-06-25 18:50:19 +09:00

269 lines
9.2 KiB
C#

#nullable enable
using Cysharp.Threading.Tasks;
using System;
using System.Collections.Concurrent;
using System.Threading;
using UnityEngine;
namespace UVC.Threading
{
/// <summary>
/// Unity 메인 스레드에서 작업을 실행하기 위한 디스패처 클래스입니다.
/// </summary>
/// <remarks>
/// 이 클래스는 싱글톤 패턴으로 구현되어 있으며, 백그라운드 스레드에서 메인 스레드(UI 스레드)로
/// 작업을 디스패치하는 기능을 제공합니다.
/// </remarks>
/// <example>
/// <code>
/// // 백그라운드 스레드에서 UI 업데이트를 해야 하는 경우 (Task 사용)
/// await Task.Run(() =>
/// {
/// // 백그라운드 작업 수행
/// var result = ComputeIntensiveTask();
///
/// // UI 업데이트는 메인 스레드에서 수행 (Fire-and-forget)
/// MainThreadDispatcher.Instance.SendToMainThread(() => {
/// UpdateUIWithResult(result);
/// });
/// });
///
/// // UniTask를 사용한 비동기 작업과 메인 스레드 디스패치 예제
/// async UniTask ProcessDataInBackgroundAsync()
/// {
/// // 스레드풀에서 실행
/// await UniTask.SwitchToThreadPool();
///
/// try
/// {
/// // 백그라운드에서 시간이 걸리는 작업 수행
/// var data = await FetchDataFromServerAsync();
/// var processedData = ProcessLargeData(data);
///
/// // 결과를 메인 스레드로 전달하여 UI 업데이트하고 완료까지 대기
/// await MainThreadDispatcher.Instance.SendToMainThreadAsync(() => {
/// // 여기에서 UI 컴포넌트를 안전하게 업데이트
/// UpdateUI(processedData);
/// ShowSuccessMessage("데이터 처리 완료");
/// });
/// }
/// catch (Exception ex)
/// {
/// // 오류 발생 시에도 메인 스레드에서 UI 관련 작업 처리
/// await MainThreadDispatcher.Instance.SendToMainThreadAsync(() => {
/// ShowErrorDialog($"오류 발생: {ex.Message}");
/// Debug.LogException(ex);
/// });
/// }
/// finally
/// {
/// // 작업 완료 후 필요한 경우 다시 메인 스레드로 전환
/// await UniTask.SwitchToMainThread();
/// // 여기서부터는 다시 메인 스레드에서 실행됨
/// CleanupResources();
/// }
/// }
///
/// // MainThreadDispatcher와 UniTask를 함께 활용한 이벤트 핸들러 예제
/// public async void OnButtonClick()
/// {
/// // UI 상태 업데이트
/// loadingIndicator.SetActive(true);
///
/// try
/// {
/// // 스레드풀로 전환하여 무거운 작업 수행
/// await UniTask.SwitchToThreadPool();
///
/// // 시간이 걸리는 작업 수행
/// var result = await PerformHeavyComputationAsync();
///
/// // 결과를 UI에 반영하고 완료까지 대기 (메인 스레드에서)
/// await MainThreadDispatcher.Instance.SendToMainThreadAsync(() => {
/// resultText.text = result.ToString();
/// loadingIndicator.SetActive(false);
/// });
/// }
/// catch (Exception ex)
/// {
/// // 예외 처리도 메인 스레드에서
/// await MainThreadDispatcher.Instance.SendToMainThreadAsync(() => {
/// errorText.text = ex.Message;
/// loadingIndicator.SetActive(false);
/// });
/// }
/// }
/// </code>
/// </example>
public class MainThreadDispatcher : MonoBehaviour
{
private static MainThreadDispatcher _instance;
private static bool _instantiated;
private static SynchronizationContext _mainThreadContext;
/// <summary>
/// MainThreadDispatcher의 단일 인스턴스를 가져옵니다.
/// </summary>
/// <remarks>
/// 인스턴스가 아직 생성되지 않은 경우, 새로운 GameObject를 생성하고
/// 그 GameObject에 MainThreadDispatcher 컴포넌트를 추가합니다.
/// </remarks>
public static MainThreadDispatcher Instance
{
get
{
if (!_instantiated)
{
Initialize();
}
return _instance;
}
}
/// <summary>
/// 메인 스레드 디스패처를 초기화합니다.
/// </summary>
private static void Initialize()
{
if (!_instantiated)
{
// 이미 존재하는지 확인
_instance = FindFirstObjectByType<MainThreadDispatcher>();
// 없으면 새로 생성
if (_instance == null)
{
var go = new GameObject("MainThreadDispatcher");
_instance = go.AddComponent<MainThreadDispatcher>();
DontDestroyOnLoad(go); // 씬 전환 시에도 유지
}
_instantiated = true;
_mainThreadContext = SynchronizationContext.Current;
}
}
private readonly ConcurrentQueue<Action> _actionQueue = new ConcurrentQueue<Action>();
/// <summary>
/// 메인 스레드에서 실행할 액션을 큐에 추가합니다.
/// </summary>
/// <param name="action">메인 스레드에서 실행할 액션</param>
public void Enqueue(Action action)
{
if (action == null) return;
// 현재 스레드가 메인 스레드면 바로 실행
if (Thread.CurrentThread.ManagedThreadId == 1)
{
action.Invoke();
return;
}
// 아니면 큐에 추가하여 나중에 실행
_actionQueue.Enqueue(action);
}
/// <summary>
/// 메인 스레드에서 액션을 실행합니다. (Fire-and-forget)
/// </summary>
/// <param name="action">메인 스레드에서 실행할 액션</param>
public void SendToMainThread(Action action)
{
if (action == null) return;
// SynchronizationContext가 있으면 사용
if (_mainThreadContext != null)
{
_mainThreadContext.Post(_ => action(), null);
}
else
{
// SynchronizationContext가 없으면 큐에 추가
Enqueue(action);
}
}
/// <summary>
/// 메인 스레드에서 액션을 비동기적으로 실행하고, 해당 액션의 완료를 나타내는 UniTask를 반환합니다.
/// </summary>
/// <param name="action">메인 스레드에서 실행할 액션입니다.</param>
/// <returns>액션의 실행이 완료되면 완료되는 UniTask입니다.</returns>
public UniTask SendToMainThreadAsync(Action action)
{
if (action == null) return UniTask.CompletedTask;
// 현재 스레드가 메인 스레드인 경우 즉시 실행
if (_mainThreadContext != null && SynchronizationContext.Current == _mainThreadContext)
{
try
{
action();
return UniTask.CompletedTask;
}
catch (Exception ex)
{
return UniTask.FromException(ex);
}
}
var utcs = new UniTaskCompletionSource();
Action wrappedAction = () =>
{
try
{
action();
utcs.TrySetResult();
}
catch (Exception e)
{
utcs.TrySetException(e);
}
};
if (_mainThreadContext != null)
{
_mainThreadContext.Post(_ => wrappedAction(), null);
}
else
{
// SynchronizationContext가 없는 경우 큐 사용
Enqueue(wrappedAction);
}
return utcs.Task;
}
private void Update()
{
// 큐에 있는 모든 액션 처리
while (_actionQueue.TryDequeue(out Action action))
{
try
{
action.Invoke();
}
catch (Exception e)
{
Debug.LogException(e);
}
}
}
private void Awake()
{
if (_instance == null)
{
_instance = this;
_mainThreadContext = SynchronizationContext.Current;
_instantiated = true;
DontDestroyOnLoad(gameObject);
}
else if (_instance != this)
{
Destroy(gameObject);
}
}
}
}