Create API to await multiple PlayerLoopTiming without any allocation

pull/156/head
Ram.Type-0 2020-09-01 00:12:48 +09:00
parent 4d554a6718
commit dc235aa3cf
10 changed files with 644 additions and 1 deletions

View File

@ -154,6 +154,7 @@ namespace Cysharp.Threading.Tasks.Internal
[System.Diagnostics.DebuggerHidden] [System.Diagnostics.DebuggerHidden]
void RunCore() void RunCore()
{ {
PlayerLoopHelper.SetCurrentPlayerLoopTiming(timing);
{ {
bool lockTaken = false; bool lockTaken = false;
try try

View File

@ -134,6 +134,7 @@ namespace Cysharp.Threading.Tasks.Internal
[System.Diagnostics.DebuggerHidden] [System.Diagnostics.DebuggerHidden]
void RunCore() void RunCore()
{ {
PlayerLoopHelper.SetCurrentPlayerLoopTiming(timing);
lock (runningAndQueueLock) lock (runningAndQueueLock)
{ {
running = true; running = true;

View File

@ -0,0 +1,325 @@
using System;
using System.Threading;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Cysharp.Threading.Tasks.Internal
{
internal sealed class UniTaskPlayerLoopSubSystem : IPlayerLoopItem
{
public UniTaskPlayerLoopSubSystem()
{
this.unhandledExceptionCallback = ex => Debug.LogException(ex);
#if UNITY_EDITOR
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
#endif
}
const int MaxArrayLength = 0X7FEFFFFF;
const int InitialSize = 16;
void Run()
{
RunContinuations();
RunLoopItems();
}
void Clear()
{
ClearContinuations();
ClearLoopItems();
}
public bool MoveNext()
{
Run();
return true;
}
#if UNITY_EDITOR
void OnPlayModeStateChanged(PlayModeStateChange state)
{
if (state == PlayModeStateChange.EnteredEditMode || state == PlayModeStateChange.EnteredPlayMode)
{
return;
}
Clear();
}
#endif
#region ContinuationQueue
SpinLock gate = new SpinLock();
bool dequing = false;
int actionListCount = 0;
Action[] actionList = new Action[InitialSize];
int waitingListCount = 0;
Action[] waitingList = new Action[InitialSize];
public void Enqueue(Action continuation)
{
bool lockTaken = false;
try
{
gate.Enter(ref lockTaken);
if (dequing)
{
// Ensure Capacity
if (waitingList.Length == waitingListCount)
{
var newLength = waitingListCount * 2;
if ((uint)newLength > MaxArrayLength) newLength = MaxArrayLength;
var newArray = new Action[newLength];
Array.Copy(waitingList, newArray, waitingListCount);
waitingList = newArray;
}
waitingList[waitingListCount] = continuation;
waitingListCount++;
}
else
{
// Ensure Capacity
if (actionList.Length == actionListCount)
{
var newLength = actionListCount * 2;
if ((uint)newLength > MaxArrayLength) newLength = MaxArrayLength;
var newArray = new Action[newLength];
Array.Copy(actionList, newArray, actionListCount);
actionList = newArray;
}
actionList[actionListCount] = continuation;
actionListCount++;
}
}
finally
{
if (lockTaken) gate.Exit(false);
}
}
void RunContinuations()
{
{
bool lockTaken = false;
try
{
gate.Enter(ref lockTaken);
if (actionListCount == 0) return;
dequing = true;
}
finally
{
if (lockTaken) gate.Exit(false);
}
}
for (int i = 0; i < actionListCount; i++)
{
var action = actionList[i];
actionList[i] = null;
try
{
action();
}
catch (Exception ex)
{
UnityEngine.Debug.LogException(ex);
}
}
{
bool lockTaken = false;
try
{
gate.Enter(ref lockTaken);
dequing = false;
var swapTempActionList = actionList;
actionListCount = waitingListCount;
actionList = waitingList;
waitingListCount = 0;
waitingList = swapTempActionList;
}
finally
{
if (lockTaken) gate.Exit(false);
}
}
}
void ClearContinuations()
{
actionListCount = 0;
actionList = new Action[InitialSize];
waitingListCount = 0;
waitingList = new Action[InitialSize];
}
#endregion
#region PlayerLoopRunner
readonly object runningAndQueueLock = new object();
readonly object arrayLock = new object();
readonly Action<Exception> unhandledExceptionCallback;
int tail = 0;
bool running = false;
IPlayerLoopItem[] loopItems = new IPlayerLoopItem[InitialSize];
MinimumQueue<IPlayerLoopItem> waitQueue = new MinimumQueue<IPlayerLoopItem>(InitialSize);
public void AddAction(IPlayerLoopItem item)
{
lock (runningAndQueueLock)
{
if (running)
{
waitQueue.Enqueue(item);
return;
}
}
lock (arrayLock)
{
// Ensure Capacity
if (loopItems.Length == tail)
{
Array.Resize(ref loopItems, checked(tail * 2));
}
loopItems[tail++] = item;
}
}
void RunLoopItems()
{
lock (runningAndQueueLock)
{
running = true;
}
lock (arrayLock)
{
var j = tail - 1;
var loopItems = this.loopItems;
// eliminate array-bound check for i
for (int i = 0; i < loopItems.Length; i++)
{
var action = loopItems[i];
if (action != null)
{
try
{
if (!action.MoveNext())
{
loopItems[i] = null;
}
else
{
continue; // next i
}
}
catch (Exception ex)
{
loopItems[i] = null;
try
{
unhandledExceptionCallback(ex);
}
catch { }
}
}
// find null, loop from tail
while (i < j)
{
var fromTail = loopItems[j];
if (fromTail != null)
{
try
{
if (!fromTail.MoveNext())
{
loopItems[j] = null;
j--;
continue; // next j
}
else
{
// swap
loopItems[i] = fromTail;
loopItems[j] = null;
j--;
goto NEXT_LOOP; // next i
}
}
catch (Exception ex)
{
loopItems[j] = null;
j--;
try
{
unhandledExceptionCallback(ex);
}
catch { }
continue; // next j
}
}
else
{
j--;
}
}
tail = i; // loop end
break; // LOOP END
NEXT_LOOP:
continue;
}
lock (runningAndQueueLock)
{
running = false;
while (waitQueue.Count != 0)
{
if (loopItems.Length == tail)
{
Array.Resize(ref loopItems, checked(tail * 2));
}
loopItems[tail++] = waitQueue.Dequeue();
}
}
}
}
void ClearLoopItems()
{
lock (arrayLock)
{
for (var index = 0; index < loopItems.Length; index++)
{
loopItems[index] = null;
}
}
}
#endregion
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 21e9b6c323ae3574cab6ff0bf6906a99
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -8,6 +8,7 @@ using System.Threading;
#if UNITY_2019_3_OR_NEWER #if UNITY_2019_3_OR_NEWER
using UnityEngine.LowLevel; using UnityEngine.LowLevel;
using System.Collections.Generic;
#else #else
using UnityEngine.Experimental.LowLevel; using UnityEngine.Experimental.LowLevel;
#endif #endif
@ -97,11 +98,34 @@ namespace Cysharp.Threading.Tasks
public static bool IsMainThread => Thread.CurrentThread.ManagedThreadId == mainThreadId; public static bool IsMainThread => Thread.CurrentThread.ManagedThreadId == mainThreadId;
static int mainThreadId; static int mainThreadId;
[ThreadStatic]
static int syncState;
/// <summary>
/// In main thread,(<see cref="PlayerLoopTiming"/>)(<see cref="SyncState"/> - 1) is current <see cref="PlayerLoopTiming"/>.Otherwise, it is 0.
/// </summary>
internal static int SyncState => syncState;
internal static void SetCurrentPlayerLoopTiming(PlayerLoopTiming timing) => syncState = (int)timing + 1;
/// <summary>
/// If we are in main thread,returns current <see cref="PlayerLoopTiming"/>. Otherwise,returns <see langword="null"/>.
/// </summary>
/// <returns></returns>
public static PlayerLoopTiming? TryGetCurrentPlayerLoopTiming()
{
var value = SyncState - 1;
return value == -1 ? (PlayerLoopTiming?)null : (PlayerLoopTiming)value;
}
static string applicationDataPath; static string applicationDataPath;
static SynchronizationContext unitySynchronizationContetext; static SynchronizationContext unitySynchronizationContetext;
static ContinuationQueue[] yielders; static ContinuationQueue[] yielders;
static PlayerLoopRunner[] runners; static PlayerLoopRunner[] runners;
static readonly Dictionary<SyncParams, UniTaskPlayerLoopSubSystem> subSystems = new Dictionary<SyncParams, UniTaskPlayerLoopSubSystem>();//TODO:Replace this with much faster dictionary.
static SpinLock subSystemsLock;
static PlayerLoopSystem[] InsertRunner(PlayerLoopSystem loopSystem, static PlayerLoopSystem[] InsertRunner(PlayerLoopSystem loopSystem,
Type loopRunnerYieldType, ContinuationQueue cq, Type lastLoopRunnerYieldType, ContinuationQueue lastCq, Type loopRunnerYieldType, ContinuationQueue cq, Type lastLoopRunnerYieldType, ContinuationQueue lastCq,
Type loopRunnerType, PlayerLoopRunner runner, Type lastLoopRunnerType, PlayerLoopRunner lastRunner) Type loopRunnerType, PlayerLoopRunner runner, Type lastLoopRunnerType, PlayerLoopRunner lastRunner)
@ -229,8 +253,9 @@ namespace Cysharp.Threading.Tasks
#else #else
PlayerLoop.GetDefaultPlayerLoop(); PlayerLoop.GetDefaultPlayerLoop();
#endif #endif
Initialize(ref playerLoop); Initialize(ref playerLoop);
SetCurrentPlayerLoopTiming(PlayerLoopTiming.Initialization);
} }
@ -278,6 +303,32 @@ namespace Cysharp.Threading.Tasks
#endif #endif
internal static UniTaskPlayerLoopSubSystem GetOrCreateSubSystem(SyncParams syncParams)
{
var lockTaken = false;
try
{
subSystemsLock.Enter(ref lockTaken);
if (!subSystems.TryGetValue(syncParams, out var subSystem))
{
subSystem = new UniTaskPlayerLoopSubSystem();
foreach (var timing in syncParams.EnumeratePlayerLoopTimings())
{
AddAction(timing, subSystem);
}
subSystems.Add(syncParams, subSystem);
}
return subSystem;
}
finally
{
if (lockTaken) subSystemsLock.Exit(false);
}
}
public static void Initialize(ref PlayerLoopSystem playerLoop) public static void Initialize(ref PlayerLoopSystem playerLoop)
{ {
yielders = new ContinuationQueue[14]; yielders = new ContinuationQueue[14];
@ -333,11 +384,33 @@ namespace Cysharp.Threading.Tasks
runners[(int)timing].AddAction(action); runners[(int)timing].AddAction(action);
} }
internal static void AddAction(SyncParams syncParams, IPlayerLoopItem action)
{
if(syncParams== SyncParams.ThreadPool)
{
throw new ArgumentException();
}
else
{
GetOrCreateSubSystem(syncParams).AddAction(action);
}
}
public static void AddContinuation(PlayerLoopTiming timing, Action continuation) public static void AddContinuation(PlayerLoopTiming timing, Action continuation)
{ {
yielders[(int)timing].Enqueue(continuation); yielders[(int)timing].Enqueue(continuation);
} }
internal static void AddContinuation(SyncParams syncParams, Action continuation)
{
if(syncParams== SyncParams.ThreadPool)
{
new SwitchToThreadPoolAwaitable.Awaiter().OnCompleted(continuation);//TODO:Should we use UnsafeOnCompleted at here?
}
GetOrCreateSubSystem(syncParams).Enqueue(continuation);
}
// Diagnostics helper // Diagnostics helper
#if UNITY_2019_3_OR_NEWER #if UNITY_2019_3_OR_NEWER

View File

@ -0,0 +1,90 @@
using Cysharp.Threading.Tasks;
using System;
using System.Runtime.CompilerServices;
namespace Cysharp.Threading.Tasks
{
[Flags]
public enum SyncParams
{
ThreadPool = 0,
#region PlayerLoopTiming
Initialization = 1 << PlayerLoopTiming.Initialization,
LastInitialization = 1 << PlayerLoopTiming.LastInitialization,
EarlyUpdate = 1 << PlayerLoopTiming.EarlyUpdate,
LastEarlyUpdate = 1 << PlayerLoopTiming.LastEarlyUpdate,
FixedUpdate = 1 << PlayerLoopTiming.FixedUpdate,
LastFixedUpdate = 1 << PlayerLoopTiming.LastFixedUpdate,
PreUpdate = 1 << PlayerLoopTiming.PreUpdate,
LastPreUpdate = 1 << PlayerLoopTiming.LastPreUpdate,
Update = 1 << PlayerLoopTiming.Update,
LastUpdate = 1 << PlayerLoopTiming.LastUpdate,
PreLateUpdate = 1 << PlayerLoopTiming.PreLateUpdate,
LastPreLateUpdate = 1 << PlayerLoopTiming.LastPreLateUpdate,
PostLateUpdate = 1 << PlayerLoopTiming.PostLateUpdate,
LastPostLateUpdate = 1 << PlayerLoopTiming.LastPostLateUpdate,
#endregion
MainThread = EarlyUpdate | PreUpdate | Update | PreLateUpdate | PostLateUpdate,
}
public struct SyncParamsPlayerLoopTimingEnumerable
{
internal SyncParams syncParams;
public SyncParamsPlayerLoopTimingEnumerator GetEnumerator() => new SyncParamsPlayerLoopTimingEnumerator() { syncParams = syncParams };
}
public static class SyncParamsHelpers
{
static readonly byte[] TrailingZeroCountDeBruijn =
{
00, 01, 28, 02, 29, 14, 24, 03,
30, 22, 20, 15, 25, 17, 04, 08,
31, 27, 13, 23, 21, 19, 16, 07,
26, 12, 18, 06, 11, 05, 10, 09
};
/// <summary>
/// From System.Numerics.BitOperations
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
/// <remarks>License:https://github.com/dotnet/runtime/blob/6072e4d3a7a2a1493f514cdf4be75a3d56580e84/LICENSE.TXT </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int TrailingZeroCount(uint value)
{
// Using deBruijn sequence, k=2, n=5 (2^5=32) : 0b_0000_0111_0111_1100_1011_0101_0011_0001u
//TODO:Avoid array bounds check
return TrailingZeroCountDeBruijn[(int)(((value & (uint)-(int)value) * 0x077CB531u) >> 27)];
}
public static SyncParamsPlayerLoopTimingEnumerable EnumeratePlayerLoopTimings(this SyncParams syncParams) => new SyncParamsPlayerLoopTimingEnumerable() { syncParams = syncParams };
public static SyncParams FromPlayerLoopTiming(PlayerLoopTiming playerLoopTiming) => (SyncParams)(1 << (int)playerLoopTiming);
}
public struct SyncParamsPlayerLoopTimingEnumerator
{
internal SyncParams syncParams;
public bool MoveNext()
{
if (syncParams == SyncParams.ThreadPool)
{
return false;
}
var value = (int)syncParams;
var tzcnt = SyncParamsHelpers.TrailingZeroCount((uint)value);
Current = (PlayerLoopTiming)tzcnt;
syncParams = (SyncParams)(value ^ (1 << tzcnt));
return true;
}
public PlayerLoopTiming Current { get; private set; }
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ebb14e764a35bbf42add491f1035c8a0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -28,6 +28,12 @@ namespace Cysharp.Threading.Tasks
return new SwitchToMainThreadAwaitable(timing, cancellationToken); return new SwitchToMainThreadAwaitable(timing, cancellationToken);
} }
public static SwitchToSyncParamsAwaitable SwitchToSyncParams(SyncParams syncParams,CancellationToken cancellationToken = default)
{
return new SwitchToSyncParamsAwaitable(syncParams, cancellationToken);
}
/// <summary> /// <summary>
/// Return to mainthread(same as await SwitchToMainThread) after using scope is closed. /// Return to mainthread(same as await SwitchToMainThread) after using scope is closed.
/// </summary> /// </summary>
@ -140,6 +146,67 @@ namespace Cysharp.Threading.Tasks
} }
} }
public struct SwitchToSyncParamsAwaitable
{
readonly SyncParams syncParams;
readonly CancellationToken cancellationToken;
internal SwitchToSyncParamsAwaitable(SyncParams syncParams, CancellationToken cancellationToken)
{
this.syncParams = syncParams;
this.cancellationToken = cancellationToken;
}
public Awaiter GetAwaiter() => new Awaiter(syncParams, cancellationToken);
public struct Awaiter : ICriticalNotifyCompletion
{
readonly SyncParams syncParams;
readonly CancellationToken cancellationToken;
internal Awaiter(SyncParams syncParams, CancellationToken cancellationToken)
{
this.syncParams = syncParams;
this.cancellationToken = cancellationToken;
}
public bool IsCompleted
{
get
{
if(PlayerLoopHelper.TryGetCurrentPlayerLoopTiming() is PlayerLoopTiming timing)
{
foreach (var syncTiming in syncParams.EnumeratePlayerLoopTimings())
{
if (syncTiming == timing)
{
return true;
}
}
}
else if (syncParams == SyncParams.ThreadPool)
{
return true;
}
return false;
}
}
public void GetResult() { cancellationToken.ThrowIfCancellationRequested(); }
public void OnCompleted(Action continuation)
{
PlayerLoopHelper.AddContinuation(syncParams, continuation);
}
public void UnsafeOnCompleted(Action continuation)
{
PlayerLoopHelper.AddContinuation(syncParams, continuation);
}
}
}
public struct ReturnToMainThread public struct ReturnToMainThread
{ {
readonly PlayerLoopTiming playerLoopTiming; readonly PlayerLoopTiming playerLoopTiming;

View File

@ -0,0 +1,53 @@
using Cysharp.Threading.Tasks;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.TestTools;
using FluentAssertions;
namespace Cysharp.Threading.TasksTests
{
public class SwitchToSyncParamsTest
{
[UnityTest]
public IEnumerator SyncParamsTest()
{
return UniTask.ToCoroutine(async () =>
{
var switchToMainThread = UniTask.SwitchToSyncParams(SyncParams.MainThread);
switchToMainThread.GetAwaiter().IsCompleted.Should().BeTrue();
await switchToMainThread;
var switchToThreadPool = UniTask.SwitchToSyncParams(SyncParams.ThreadPool);
switchToThreadPool.GetAwaiter().IsCompleted.Should().BeFalse();
await switchToThreadPool;
PlayerLoopHelper.IsMainThread.Should().BeFalse();
await switchToMainThread;
PlayerLoopHelper.IsMainThread.Should().BeTrue();
await UniTask.Yield(PlayerLoopTiming.Initialization);
switchToThreadPool.GetAwaiter().IsCompleted.Should().BeFalse();
await switchToMainThread;
PlayerLoopHelper.TryGetCurrentPlayerLoopTiming().Should().Be(PlayerLoopTiming.EarlyUpdate);
await UniTask.Yield(PlayerLoopTiming.LastEarlyUpdate);
switchToThreadPool.GetAwaiter().IsCompleted.Should().BeFalse();
await switchToMainThread;
PlayerLoopHelper.TryGetCurrentPlayerLoopTiming().Should().Be(PlayerLoopTiming.PreUpdate);
await UniTask.Yield(PlayerLoopTiming.LastPreUpdate);
switchToThreadPool.GetAwaiter().IsCompleted.Should().BeFalse();
await switchToMainThread;
PlayerLoopHelper.TryGetCurrentPlayerLoopTiming().Should().Be(PlayerLoopTiming.Update);
await UniTask.Yield(PlayerLoopTiming.LastUpdate);
switchToThreadPool.GetAwaiter().IsCompleted.Should().BeFalse();
await switchToMainThread;
PlayerLoopHelper.TryGetCurrentPlayerLoopTiming().Should().Be(PlayerLoopTiming.PreLateUpdate);
await UniTask.Yield(PlayerLoopTiming.LastPreLateUpdate);
switchToThreadPool.GetAwaiter().IsCompleted.Should().BeFalse();
await switchToMainThread;
PlayerLoopHelper.TryGetCurrentPlayerLoopTiming().Should().Be(PlayerLoopTiming.PostLateUpdate);
await UniTask.Yield(PlayerLoopTiming.LastPostLateUpdate);
switchToThreadPool.GetAwaiter().IsCompleted.Should().BeFalse();
await switchToMainThread;
PlayerLoopHelper.TryGetCurrentPlayerLoopTiming().Should().Be(PlayerLoopTiming.EarlyUpdate);
});
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d3694a1d095bc654c8fdcfe5d79cb78b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: