269 lines
9.2 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
} |