Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Showcase Burst Lock with Timer / Timeout (Interlocked Spinlocks but safer)

Discussion in 'Burst' started by VirtusH, Aug 9, 2023.

  1. VirtusH

    VirtusH

    Joined:
    Aug 18, 2015
    Posts:
    91
    This is a bit hair-brained I feel, but I stole (read:borrowed) the spinlocks from Unity.Logging package and wrote a global timer that gets updated by a separate thread to make burst locks that are recoverable when there is a failure of some kind.

    So far.. they're working super well and the timeout has avoided heaps of deadlocks in WIP code, and these have allowed us to explore moving more things into burst in a game where we need a lot of thread-safety to support systems intercommunicating.

    I would love to hear if this has some fatal flaw or something I should fix, but otherwise I'm just sharing this for anyone who might benefit from it. I cannot guarantee safety in all cases of course, but thus far we've beat the crap out of these locks and they haven't failed yet. We did have a deadlock from a bad unsafe read that generated a corrupt burstlock structure (from reading outside an array's index range accidentally), but with some tightening of a couple checks, it now detects that problem too and avoids locking, throwing an error instead.

    • Global Burst Timer: A small script that runs a timer on another thread, storing time as a float value (could be a double, but that might be extreme??).
    • BurstSpinLock: A simple exclusive lock that allows only single serial access.
    • BurstSpinLockReadWrite: A more complex lock that allows many readers or one writer, first come first serve.
    • BurstSpinLockCheckFunctions: Utility functions for the spinlocks, holdover from Unity.Logging
    • BurstSpinLockReadWriteFunctions: Writing utility functions for the spinlocks, holdover from Unity.Logging
    • VUnsafeRef<T>: This is basically a copy of NativeReference<T> with the safety stripped out for ease of use. The idea is that it's basically just a "safer" pointer.
    • ValueStopwatch: A neat idea I borrowed from somewhere that's basically a simple alloc-free stopwatch. I added some nice floating point properties to obfuscate yucky conversions.
    How to use:
    • Launch a global burst timer thread.
    • Use the constructors to create as many locks as desired sharing the global burst timer.
    • Using statements and blocks are the preferred way to lock, much cleaner and harder to mess up.
    • Resulting variable of using statements/blocks can implicitly cast to 'bool' for a quick if check to see if the lock acquired, allowing safe early exiting behaviour.
    • OR
    • Lock and Unlock manually by calling methods on either type of lock.
    • Timeouts can be specified per locking method call with either route.

    Quick reminder: I'm not responsible for any use of this code, it's inherently pretty dangerous code. I'm sharing it for those who want to be able to do "safer" locking in burst-compatible code! (And also so I might hear from the geniuses that roam this place, if my hair-brained "safe burst lock" is flawed!)
    Alright, here's the code:

    Global Burst Timer
    Code (CSharp):
    1. /// <summary> Designed to be created once and updated at a high frequency. </summary>
    2.     public readonly struct GlobalBurstTimer : IDisposable
    3.     {
    4.         public readonly VUnsafeRef<float> time;
    5.  
    6.         public readonly bool IsCreatedAndValid => time.IsCreated && time.m_AllocatorLabel == Allocator.Persistent && time.Value >= 0;
    7.      
    8.         public readonly float Time => IsCreatedAndValid ? time.Value : throw new InvalidOperationException("GlobalBurstTimer struct invalid, time is not created.");
    9.      
    10.         public GlobalBurstTimer(bool dummyValue)
    11.         {
    12.             time = new VUnsafeRef<float>(Allocator.Persistent);
    13.         }
    14.  
    15.         public void Dispose() => time.Dispose();
    16.      
    17.         ///<summary> Launch a thread that updates the time value of a globalbursttimer </summary>
    18.         public static Thread LaunchUpdateThread(GlobalBurstTimer timer)
    19.         {
    20.             var thread = new Thread(() =>
    21.             {
    22.                 Profiler.BeginThreadProfiling("GlobalBurstTimer", "GlobalBurstTimer");
    23.              
    24.                 var stopwatch = ValueStopwatch.StartNew();
    25.                 var spinwait = new SpinWait();
    26.              
    27.                 while (true)
    28.                 {
    29.                     if (!timer.IsCreatedAndValid)
    30.                         break;
    31.                     if (timer.Time < stopwatch.ElapsedSecondsF)
    32.                     {
    33.                         var t = timer.time;
    34.                         t.Value = stopwatch.ElapsedSecondsF;
    35.                     }
    36.                     if (spinwait.NextSpinWillYield)
    37.                         spinwait.Reset();
    38.                     spinwait.SpinOnce();
    39.                 }
    40.              
    41.                 Profiler.EndThreadProfiling();
    42.             });
    43.          
    44.             thread.Start();
    45.             return thread;
    46.         }
    47.     }
    Burst Spin Lock
    Code (CSharp):
    1. //#define MARK_THREAD_OWNERS
    2.  
    3. #if ENABLE_UNITY_COLLECTIONS_CHECKS || UNITY_DOTS_DEBUG
    4. #define DEBUG_ADDITIONAL_CHECKS
    5. #endif
    6.  
    7. using System;
    8. using System.Diagnostics;
    9. using System.Runtime.CompilerServices;
    10. using System.Threading;
    11. using Unity.Burst;
    12. using Unity.Burst.Intrinsics;
    13. using Unity.Collections;
    14. using Unity.Collections.LowLevel.Unsafe;
    15. using Unity.IL2CPP.CompilerServices;
    16. using Unity.Logging;
    17.  
    18. namespace VLib
    19. {
    20.     /// <summary> Implement a very basic, Burst-compatible SpinLock that mirrors the basic .NET SpinLock API. </summary>
    21.     [BurstCompile]
    22.     [Il2CppSetOption(Option.NullChecks, false)]
    23.     [Il2CppSetOption(Option.ArrayBoundsChecks, false)]
    24.     [Il2CppSetOption(Option.DivideByZeroChecks, false)]
    25.     public struct BurstSpinLock : IDisposable
    26.     {
    27.         private UnsafeList<long> m_Locked;
    28.         GlobalBurstTimer burstTimerRef;
    29.        
    30.         /// <summary> Checks locked buffer length as well to detect corruption </summary>
    31.         public bool IsCreatedAndValid => m_Locked.IsCreated && m_Locked.Length == 1 && burstTimerRef.IsCreatedAndValid;
    32.  
    33.         /// <summary> Constructor for the spin lock </summary>
    34.         /// <param name="allocator">allocator to use for internal memory allocation. Usually should be Allocator.Persistent</param>
    35.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    36.         public BurstSpinLock(Allocator allocator, GlobalBurstTimer burstTimerRef = default) // Is optional to allow
    37.         {
    38.             m_Locked = new UnsafeList<long>(1, allocator);
    39.             m_Locked.AddNoResize(0);
    40.            
    41.             this.burstTimerRef = burstTimerRef;
    42.         }
    43.  
    44.         /// <summary> Dispose this spin lock. <see cref="IDisposable"/> </summary>
    45.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    46.         public void Dispose()
    47.         {
    48.             if (IsCreatedAndValid)
    49.             {
    50.                 if (!TryEnter(.5f))
    51.                     UnityEngine.Debug.LogError("SpinLock could not be captured for dispose!");
    52.                 m_Locked.Dispose();
    53.             }
    54.             else
    55.             {
    56.                 UnityEngine.Debug.LogError("SpinLock was not created, but you're disposing it!");
    57.             }
    58.         }
    59.  
    60.         /// <summary> Check lock status without interfering or blocking </summary>
    61.         public bool Locked => Interlocked.Read(ref m_Locked.ElementAt(0)) != 0;
    62.  
    63.         /// <summary> Use the lock in a using statement/block. Implicitly casts to bool for clean checking. </summary>
    64.         public BurstSpinLockScoped Scoped(float timeoutSeconds) => new BurstSpinLockScoped(this, timeoutSeconds);
    65.        
    66.         [Conditional("DEBUG_ADDITIONAL_CHECKS")]
    67.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    68.         private void CheckIfLockCreated()
    69.         {
    70.             if (!IsCreatedAndValid)
    71.                 throw new Exception("Lock wasn't created, but you're accessing it");
    72.         }
    73.  
    74.         /// <summary> Try to lock. Blocking until timeout or acquisition. </summary>
    75.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    76.         public bool TryEnter(float timeoutSeconds)
    77.         {
    78.             CheckIfLockCreated();
    79.  
    80. #if MARK_THREAD_OWNERS
    81.             var threadId = Baselib.LowLevel.Binding.Baselib_Thread_GetCurrentThreadId().ToInt64();
    82.             BurstSpinLockCheckFunctions.CheckForRecursiveLock(threadId, ref lockVar);
    83. #else
    84.             var threadId = 1;
    85. #endif
    86.             ref long lockVar = ref m_Locked.ElementAt(0);
    87.             var forceExitTime = burstTimerRef.Time + timeoutSeconds;
    88.             while (Interlocked.CompareExchange(ref lockVar, threadId, 0) != 0)
    89.             {
    90.                 if (burstTimerRef.Time > forceExitTime)
    91.                     return false;
    92.                
    93.                 Common.Pause();
    94.                    
    95.                 // Don't have access for some reason, thanks Unity
    96.                 //Baselib.LowLevel.Binding.Baselib_Thread_YieldExecution();
    97.             }
    98.  
    99.             return true;
    100.         }
    101.  
    102.         /// <summary> Unlock </summary>
    103.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    104.         public void Exit()
    105.         {
    106.             CheckIfLockCreated();
    107.  
    108.             ref long lockVar = ref m_Locked.ElementAt(0);
    109.             // TODO: Enhance ?
    110.             BurstSpinLockCheckFunctions.CheckWeCanExit(ref lockVar);
    111.             Interlocked.Exchange(ref lockVar, 0);
    112.         }
    113.     }
    114.  
    115.     [GenerateTestsForBurstCompatibility]
    116.     public struct BurstSpinLockScoped : IDisposable
    117.     {
    118.         BurstSpinLock spinLock;
    119.         /// <summary> Check this, or implicitly cast this struct to 'bool' to check whether the lock acquired successfully! </summary>
    120.         public bool Succeeded { get; }
    121.         public static implicit operator bool(BurstSpinLockScoped scoped) => scoped.Succeeded;
    122.  
    123.         public BurstSpinLockScoped(BurstSpinLock spinLock, float timeoutSeconds)
    124.         {
    125.             this.spinLock = spinLock;
    126.             Succeeded = spinLock.TryEnter(timeoutSeconds);
    127.         }
    128.  
    129.         public void Dispose()
    130.         {
    131.             if (Succeeded)
    132.                 spinLock.Exit();
    133.         }
    134.     }
    135. }
    Burst Spin Lock Read Write
    Code (CSharp):
    1. //#define DEBUG_DEADLOCKS
    2.  
    3. //#define MARK_THREAD_OWNERS
    4.  
    5. #if ENABLE_UNITY_COLLECTIONS_CHECKS || UNITY_DOTS_DEBUG
    6. #define DEBUG_ADDITIONAL_CHECKS
    7. #endif
    8.  
    9. using System;
    10. using System.Diagnostics;
    11. using System.Runtime.CompilerServices;
    12. using System.Threading;
    13. using Unity.Burst;
    14. using Unity.Collections;
    15. using Unity.Collections.LowLevel.Unsafe;
    16. using Unity.IL2CPP.CompilerServices;
    17. using UnityEngine;
    18.  
    19. namespace VLib
    20. {
    21.     /// <summary>
    22.     /// Implement a very basic, Burst-compatible read-write SpinLock
    23.     /// </summary>
    24.     [BurstCompile]
    25.     [Il2CppSetOption(Option.NullChecks, false)]
    26.     [Il2CppSetOption(Option.ArrayBoundsChecks, false)]
    27.     [Il2CppSetOption(Option.DivideByZeroChecks, false)]
    28.     public struct BurstSpinLockReadWrite : IDisposable
    29.     {
    30.         private const int MemorySize = 16; // * sizeof(long) == 128 byte
    31.         private const int LockLocation = 0;
    32.         private const int ReadersLocation = 8; // * sizeof(long) == 64 byte offset (cache line)
    33.  
    34.         private UnsafeList<long> m_Locked;
    35.         GlobalBurstTimer burstTimerRef;
    36.  
    37.         /// <summary>
    38.         /// Constructor for the spin lock
    39.         /// </summary>
    40.         /// <param name="allocator">allocator to use for internal memory allocation. Usually should be Allocator.Persistent</param>
    41.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    42.         public BurstSpinLockReadWrite(Allocator allocator, GlobalBurstTimer globalBurstTimerRef)
    43.         {
    44.             m_Locked = new UnsafeList<long>(MemorySize, allocator);
    45.             for (var i = 0; i < MemorySize; i++)
    46.             {
    47.                 m_Locked.AddNoResize(0);
    48.             }
    49.  
    50.             burstTimerRef = globalBurstTimerRef;
    51.         }
    52.  
    53.         /// <summary>
    54.         /// Dispose this spin lock. <see cref="IDisposable"/>
    55.         /// </summary>
    56.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    57.         public void Dispose()
    58.         {
    59.             if (IsCreatedAndValid)
    60.             {
    61.                 if (!EnterExclusive(1))
    62.                     UnityEngine.Debug.LogError("Failed to dispose BurstSpinLockReadWrite, it is still locked after 1 second");
    63.                 m_Locked.Dispose();
    64.             }
    65.             else
    66.             {
    67.                 UnityEngine.Debug.LogError("Failed to dispose BurstSpinLockReadWrite, it is not valid");
    68.             }
    69.         }
    70.  
    71.         public bool LockedExclusive => Interlocked.Read(ref m_Locked.ElementAt(LockLocation)) != 0;
    72.         public bool LockedForRead => Interlocked.Read(ref m_Locked.ElementAt(ReadersLocation)) != 0;
    73.        
    74.         /// <summary> Checks locked buffer length as well to detect corruption </summary>
    75.         public bool IsCreatedAndValid => m_Locked.IsCreated && m_Locked.Length == MemorySize && burstTimerRef.IsCreatedAndValid;
    76.  
    77.         public long Id
    78.         {
    79.             get
    80.             {
    81.                 unsafe
    82.                 {
    83.                     return (long) m_Locked.Ptr;
    84.                 }
    85.             }
    86.         }
    87.  
    88.         [Conditional("DEBUG_ADDITIONAL_CHECKS")]
    89.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    90.         private void CheckIfLockCreated()
    91.         {
    92.             // Check the burst timer as well.. found a situation where the m_locked list thought it was created but it's allocator was invalid and capacity 0...
    93.             if (!IsCreatedAndValid)
    94.                 throw new Exception("RWLock wasn't created, but you're accessing it");
    95.         }
    96.  
    97.         /// <summary> Lock Exclusive. Will block if cannot lock immediately </summary>
    98.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    99.         public bool EnterExclusive(float timeoutSeconds = .25f)
    100.         {
    101.             CheckIfLockCreated();
    102.  
    103.             return BurstSpinLockReadWriteFunctions.
    104. TryEnterExclusiveBlocking
    105. (
    106.                 ref m_Locked.ElementAt(LockLocation),
    107.                 ref m_Locked.ElementAt(ReadersLocation),
    108.                 burstTimerRef,
    109.                 timeoutSeconds);
    110.         }
    111.  
    112.         /// <summary> Try to lock Exclusive. Won't block </summary>
    113.         /// <returns>True if locked</returns>
    114.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    115.         public bool TryEnterExclusive()
    116.         {
    117.             CheckIfLockCreated();
    118.  
    119.             return BurstSpinLockReadWriteFunctions.TryEnterExclusive(ref m_Locked.ElementAt(LockLocation), ref m_Locked.ElementAt(ReadersLocation));
    120.         }
    121.  
    122.         /// <summary> Unlock </summary>
    123.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    124.         public void ExitExclusive()
    125.         {
    126.             CheckIfLockCreated();
    127.  
    128.             BurstSpinLockReadWriteFunctions.ExitExclusive(ref m_Locked.ElementAt(LockLocation));
    129.         }
    130.  
    131.         /// <summary>
    132.         /// Lock for Read. Will block if exclusive is locked
    133.         /// </summary>
    134.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    135.         public bool EnterRead(float timeoutSeconds = .25f)
    136.         {
    137.             CheckIfLockCreated();
    138.  
    139.             return BurstSpinLockReadWriteFunctions.TryEnterReadBlocking(
    140.                 ref m_Locked.ElementAt(LockLocation),
    141.                 ref m_Locked.ElementAt(ReadersLocation),
    142.                 burstTimerRef,
    143.                 timeoutSeconds);
    144.         }
    145.  
    146.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    147.         public void ExitRead()
    148.         {
    149.             CheckIfLockCreated();
    150.  
    151.             BurstSpinLockReadWriteFunctions.ExitRead(ref m_Locked.ElementAt(ReadersLocation));
    152.         }
    153.        
    154.         public ScopedExclusiveLock ScopedExclusiveLock(float timeoutSeconds = 1) => new(this, timeoutSeconds);
    155.        
    156.         public ScopedReadLock ScopedReadLock(float timeoutSeconds = 1) => new(this, timeoutSeconds);
    157.     }
    158.  
    159.     /// <summary> IDisposable scoped structure that holds <see cref="BurstSpinLockReadWrite"/> in exclusive mode. Should be using with <c>using</c> </summary>
    160.     [Il2CppSetOption(Option.NullChecks, false)]
    161.     [Il2CppSetOption(Option.ArrayBoundsChecks, false)]
    162.     [Il2CppSetOption(Option.DivideByZeroChecks, false)]
    163.     public struct ScopedExclusiveLock : IDisposable
    164.     {
    165.         private BurstSpinLockReadWrite m_parentLock;
    166.         /// <summary> Check this, or implicitly cast this struct to 'bool' to check whether the lock acquired successfully! </summary>
    167.         public bool Succeeded { get; }
    168.         public static implicit operator bool(ScopedExclusiveLock d) => d.Succeeded;
    169.  
    170.         /// <summary> Creates ScopedReadLock and locks SpinLockReadWrite in exclusive mode </summary>
    171.         /// <param name="sl">SpinLock to lock</param>
    172.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    173.         public ScopedExclusiveLock(in BurstSpinLockReadWrite sl, float timeoutSeconds = 1)
    174.         {
    175.             m_parentLock = sl;
    176.             Succeeded = m_parentLock.EnterExclusive(timeoutSeconds);
    177.         }
    178.  
    179.         /// <summary> Unlocks the lock </summary>
    180.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    181.         public void Dispose()
    182.         {
    183.             if (Succeeded)
    184.                 m_parentLock.ExitExclusive();
    185.         }
    186.     }
    187.  
    188.     /// <summary> IDisposable scoped structure that holds <see cref="BurstSpinLockReadWrite"/> in read mode. Should be using with <c>using</c> </summary>
    189.     [Il2CppSetOption(Option.NullChecks, false)]
    190.     [Il2CppSetOption(Option.ArrayBoundsChecks, false)]
    191.     [Il2CppSetOption(Option.DivideByZeroChecks, false)]
    192.     public struct ScopedReadLock : IDisposable
    193.     {
    194.         private BurstSpinLockReadWrite m_parentLock;
    195.  
    196.         /// <summary> Check this, or implicitly cast this struct to 'bool' to check whether the lock acquired successfully! </summary>
    197.         public bool Succeeded { get; }
    198.         public static implicit operator bool(ScopedReadLock d) => d.Succeeded;
    199.  
    200.         /// <summary> Creates ScopedReadLock and locks SpinLockReadWrite in read mode </summary>
    201.         /// <param name="sl">SpinLock to lock</param>
    202.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    203.         public ScopedReadLock(in BurstSpinLockReadWrite sl, float timeoutSeconds = 1)
    204.         {
    205.             m_parentLock = sl;
    206.             Succeeded = m_parentLock.EnterRead(timeoutSeconds);
    207.         }
    208.  
    209.         /// <summary> Unlocks the lock </summary>
    210.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    211.         public void Dispose()
    212.         {
    213.             if (Succeeded)
    214.                 m_parentLock.ExitRead();
    215.         }
    216.     }
    217. }
    Burst Spin Lock Check Functions
    Code (CSharp):
    1. using System;
    2. using System.Diagnostics;
    3. using System.Runtime.CompilerServices;
    4. using System.Threading;
    5. using Unity.IL2CPP.CompilerServices;
    6.  
    7. namespace VLib
    8. {
    9.     [Il2CppSetOption(Option.NullChecks, false)]
    10.     [Il2CppSetOption(Option.ArrayBoundsChecks, false)]
    11.     [Il2CppSetOption(Option.DivideByZeroChecks, false)]
    12.     public static class BurstSpinLockCheckFunctions
    13.     {
    14.         [Conditional("DEBUG_ADDITIONAL_CHECKS")]
    15.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    16.         public static void CheckForRecursiveLock(in long threadId, ref long lockVar)
    17.         {
    18. #if MARK_THREAD_OWNERS
    19.             var currentOwnerThreadId = Interlocked.Read(ref lockVar);
    20.  
    21.             if (threadId == currentOwnerThreadId)
    22.             {
    23.                 UnityEngine.Debug.LogError(string.Format("Recursive lock! Thread {0}", threadId));
    24.                 throw new Exception($"Recursive lock! Thread {threadId}");
    25.             }
    26. #endif
    27.         }
    28.  
    29.         [Conditional("DEBUG_ADDITIONAL_CHECKS")]
    30.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    31.         public static void CheckForRecursiveLock(ref long lockVar)
    32.         {
    33. #if MARK_THREAD_OWNERS
    34.             var threadId = Baselib.LowLevel.Binding.Baselib_Thread_GetCurrentThreadId().ToInt64();
    35.             CheckForRecursiveLock(threadId, ref lockVar);
    36. #endif
    37.         }
    38.  
    39.         [Conditional("DEBUG_ADDITIONAL_CHECKS")]
    40.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    41.         public static void CheckWeCanExit(ref long lockVar)
    42.         {
    43.             var currentOwnerThreadId = Interlocked.Read(ref lockVar);
    44.             if (currentOwnerThreadId == 0)
    45.                 UnityEngine.Debug.LogError("Exit is called on not locked lock");
    46.             //throw new Exception("Exit is called on not locked lock"); // No silly, just log
    47.  
    48. #if MARK_THREAD_OWNERS
    49.             var threadId = Baselib.LowLevel.Binding.Baselib_Thread_GetCurrentThreadId().ToInt64();
    50.  
    51.             if (threadId != currentOwnerThreadId)
    52.             {
    53.                 UnityEngine.Debug.LogError(string.Format("Exit is called from the other ({0}) thread, owner thread = {1}", threadId, currentOwnerThreadId));
    54.                 throw new Exception($"Exit is called from the other ({threadId}) thread, owner thread = {currentOwnerThreadId}");
    55.             }
    56. #endif
    57.         }
    58.  
    59.         [Conditional("DEBUG_ADDITIONAL_CHECKS")]
    60.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    61.         public static void CheckLocked(ref long lockVar)
    62.         {
    63.             if (Interlocked.Read(ref lockVar) == 0)
    64.                 throw new Exception("Exit is called on not locked lock");
    65.         }
    66.     }
    67. }
    Burst Spin Lock Read Write Functions
    Code (CSharp):
    1. using System;
    2. using System.Diagnostics;
    3. using System.Runtime.CompilerServices;
    4. using System.Threading;
    5. using Unity.Burst.Intrinsics;
    6. using Unity.IL2CPP.CompilerServices;
    7.  
    8. namespace VLib
    9. {
    10.     [Il2CppSetOption(Option.NullChecks, false)]
    11.     [Il2CppSetOption(Option.ArrayBoundsChecks, false)]
    12.     [Il2CppSetOption(Option.DivideByZeroChecks, false)]
    13.     public static class BurstSpinLockReadWriteFunctions
    14.     {
    15.         /// <summary> Lock Exclusive. Will block if cannot lock immediately </summary>
    16.         /// <returns>True if lock acquired, false if
    17.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    18.         public static bool TryEnterExclusiveBlocking(ref long lockVar, ref long readersVar, in GlobalBurstTimer burstTimer, float timeoutSeconds)
    19.         {
    20. #if MARK_THREAD_OWNERS
    21.             var threadId = Baselib.LowLevel.Binding.Baselib_Thread_GetCurrentThreadId().ToInt64();
    22.             BurstSpinLockCheckFunctions.CheckForRecursiveLock(threadId, ref lockVar);
    23. #else
    24.             var threadId = 1;
    25. #endif
    26.  
    27.             float exitLockTime = burstTimer.Time + timeoutSeconds;
    28.            
    29.             while (Interlocked.CompareExchange(ref lockVar, threadId, 0) != 0)
    30.             {
    31.                 if (burstTimer.Time > exitLockTime)
    32.                     return false;
    33.                 Common.Pause();
    34.                    
    35.                 // Don't have access for some reason, thanks Unity
    36.                 //Baselib.LowLevel.Binding.Baselib_Thread_YieldExecution();
    37.             }
    38.  
    39. #if DEBUG_DEADLOCKS
    40.             var deadlockGuard = 0;
    41. #endif
    42.  
    43.             // while we have readers
    44.             while (Interlocked.Read(ref readersVar) != 0)
    45.             {
    46.                 if (burstTimer.Time > exitLockTime)
    47.                     return false;
    48.                 Common.Pause();
    49.                    
    50.                 // Don't have access for some reason, thanks Unity
    51.                 //Baselib.LowLevel.Binding.Baselib_Thread_YieldExecution();
    52.  
    53. #if DEBUG_DEADLOCKS
    54.                 if (++deadlockGuard == 512)
    55.                 {
    56.                     UnityEngine.Debug.LogError("Cannot get spin lock, because of readers");
    57.                     Interlocked.Exchange(ref lockVar, 0);
    58.                     throw new Exception();
    59.                 }
    60. #endif
    61.             }
    62.  
    63.             return true;
    64.         }
    65.  
    66.         /// <summary> Try to lock Exclusive. Won't block </summary>
    67.         /// <returns>True if locked</returns>
    68.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    69.         public static bool TryEnterExclusive(ref long lockVar, ref long readersVar)
    70.         {
    71.             if (Interlocked.Read(ref readersVar) != 0)
    72.                 return false;
    73.  
    74. #if MARK_THREAD_OWNERS
    75.             var threadId = Baselib.LowLevel.Binding.Baselib_Thread_GetCurrentThreadId().ToInt64();
    76.             BurstSpinLockCheckFunctions.CheckForRecursiveLock(threadId, ref lockVar);
    77. #else
    78.             var threadId = 1;
    79. #endif
    80.  
    81.             if (Interlocked.CompareExchange(ref lockVar, threadId, 0) != 0)
    82.                 return false;
    83.  
    84.             if (Interlocked.Read(ref readersVar) != 0)
    85.             {
    86.                 Interlocked.Exchange(ref lockVar, 0);
    87.             }
    88.  
    89.             return true;
    90.         }
    91.  
    92.         /// <summary>
    93.         /// Unlock
    94.         /// </summary>
    95.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    96.         public static void ExitExclusive(ref long lockVar)
    97.         {
    98.             BurstSpinLockCheckFunctions.CheckWeCanExit(ref lockVar);
    99.             Interlocked.Exchange(ref lockVar, 0);
    100.         }
    101.  
    102.         /// <summary>
    103.         /// Lock for Read. Will block if exclusive is locked
    104.         /// </summary>
    105.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    106.         public static bool TryEnterReadBlocking(ref long lockVar, ref long readersVar, in GlobalBurstTimer burstTimer, float timeoutSeconds)
    107.         {
    108.             BurstSpinLockCheckFunctions.CheckForRecursiveLock(ref lockVar);
    109.  
    110.             var exitLockTime = burstTimer.Time + timeoutSeconds;
    111.            
    112.             // Loop until we get lock or time out
    113.             while (true)
    114.             {
    115.                 Interlocked.Increment(ref readersVar);
    116.  
    117.                 // if not locked
    118.                 if (Interlocked.Read(ref lockVar) == 0)
    119.                 {
    120.                     return true;
    121.                 }
    122.  
    123.                 // fail, it is locked
    124.                 Interlocked.Decrement(ref readersVar);
    125.  
    126.                 // while it is locked - spin
    127.                 while (Interlocked.Read(ref lockVar) != 0)
    128.                 {
    129.                     if (burstTimer.Time > exitLockTime)
    130.                         return false;
    131.                     Common.Pause();
    132.                    
    133.                     // Don't have access for some reason, thanks Unity
    134.                     //Baselib.LowLevel.Binding.Baselib_Thread_YieldExecution();
    135.                 }
    136.             }
    137.         }
    138.  
    139.         /// <summary>
    140.         /// Exit read lock. EnterRead must be called before this call by the same thread
    141.         /// </summary>
    142.         /// <param name="readersVar"></param>
    143.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    144.         public static void ExitRead(ref long readersVar)
    145.         {
    146.             Interlocked.Decrement(ref readersVar);
    147.             CheckForNegativeReaders(ref readersVar);
    148.         }
    149.  
    150.         [Conditional("DEBUG_ADDITIONAL_CHECKS")]
    151.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    152.         static void CheckForNegativeReaders(ref long readers)
    153.         {
    154.             if (Interlocked.Read(ref readers) < 0)
    155.                 throw new Exception("Reader count cannot be negative!");
    156.         }
    157.  
    158.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    159.         public static bool HasReadLock(ref long readLock)
    160.         {
    161.             return Interlocked.Read(ref readLock) > 0;
    162.         }
    163.     }
    164. }
    VUnsafeRef<T>
    Code (CSharp):
    1. using System;
    2. using System.Diagnostics;
    3. using System.Runtime.InteropServices;
    4. using Unity.Burst;
    5. using Unity.Collections;
    6. using Unity.Collections.LowLevel.Unsafe;
    7. using Unity.Jobs;
    8.  
    9. namespace VLib
    10. {
    11.     /// <summary> Essentially an unsafe lower-level version of <see cref="NativeReference{T}"/> </summary>
    12.     /// <typeparam name="T"></typeparam>
    13.     [StructLayout(LayoutKind.Sequential)]
    14.     [GenerateTestsForBurstCompatibility(GenericTypeArguments = new [] { typeof(int) })]
    15.     public unsafe struct VUnsafeRef<T> : IEquatable<VUnsafeRef<T>>
    16.         where T : unmanaged
    17.     {
    18.         [NativeDisableUnsafePtrRestriction]
    19.         T* ptr;
    20.  
    21.         internal AllocatorManager.AllocatorHandle m_AllocatorLabel;
    22.  
    23.         /// <summary>
    24.         /// Initializes and returns an instance of VUnsafeRef.
    25.         /// </summary>
    26.         /// <param name="allocator">The allocator to use.</param>
    27.         /// <param name="options">Whether newly allocated bytes should be zeroed out.</param>
    28.         public VUnsafeRef(AllocatorManager.AllocatorHandle allocator, NativeArrayOptions options = NativeArrayOptions.ClearMemory)
    29.         {
    30.             Allocate(allocator, out this);
    31.             if (options == NativeArrayOptions.ClearMemory)
    32.             {
    33.                 UnsafeUtility.MemClear(ptr, UnsafeUtility.SizeOf<T>());
    34.             }
    35.         }
    36.  
    37.         /// <summary>
    38.         /// Initializes and returns an instance of VUnsafeRef.
    39.         /// </summary>
    40.         /// <param name="allocator">The allocator to use.</param>
    41.         /// <param name="value">The initial value.</param>
    42.         public VUnsafeRef(T value, AllocatorManager.AllocatorHandle allocator)
    43.         {
    44.             Allocate(allocator, out this);
    45.             *ptr = value;
    46.         }
    47.  
    48.         static void Allocate(AllocatorManager.AllocatorHandle allocator, out VUnsafeRef<T> reference)
    49.         {
    50.             //CollectionHelper.CheckAllocator(allocator);
    51.             reference = default;
    52.             reference.ptr = (T*) UnsafeUtility.Malloc(UnsafeUtility.SizeOf<T>(), UnsafeUtility.AlignOf<T>(), allocator.ToAllocator);
    53.             reference.m_AllocatorLabel = allocator;
    54.         }
    55.  
    56.         public void* Ptr => ptr;
    57.         public T* TPtr => ptr;
    58.         /// <summary>
    59.         /// The value stored in this reference.
    60.         /// </summary>
    61.         /// <param name="value">The new value to store in this reference.</param>
    62.         /// <value>The value stored in this reference.</value>
    63.         public T Value
    64.         {
    65.             get => *(T*)ptr;
    66.             set => *(T*)ptr = value;
    67.         }
    68.        
    69.         public ref T ValueRef => ref UnsafeUtility.AsRef<T>(ptr);
    70.  
    71.         /// <summary>
    72.         /// Whether this reference has been allocated (and not yet deallocated).
    73.         /// </summary>
    74.         /// <value>True if this reference has been allocated (and not yet deallocated).</value>
    75.         public bool IsCreated => ptr != null;
    76.  
    77.         /// <summary>Releases all resources (memory and safety handles). Inherently safe, will not throw exception if already disposed.</summary>
    78.         public void Dispose()
    79.         {
    80.             if (ptr != null)
    81.             {
    82.                 UnsafeUtility.Free(ptr, m_AllocatorLabel.ToAllocator);
    83.                 m_AllocatorLabel = default;
    84.                 ptr = null;
    85.             }
    86.         }
    87.  
    88.         /// <summary>
    89.         /// Copy the value of another reference to this reference.
    90.         /// </summary>
    91.         /// <param name="reference">The reference to copy from.</param>
    92.         public void CopyFrom(VUnsafeRef<T> reference)
    93.         {
    94.             Copy(this, reference);
    95.         }
    96.  
    97.         /// <summary>
    98.         /// Copy the value of this reference to another reference.
    99.         /// </summary>
    100.         /// <param name="reference">The reference to copy to.</param>
    101.         public void CopyTo(VUnsafeRef<T> reference)
    102.         {
    103.             Copy(reference, this);
    104.         }
    105.  
    106.         /// <summary>
    107.         /// Returns true if the value stored in this reference is equal to the value stored in another reference.
    108.         /// </summary>
    109.         /// <param name="other">A reference to compare with.</param>
    110.         /// <returns>True if the value stored in this reference is equal to the value stored in another reference.</returns>
    111.         public bool Equals(VUnsafeRef<T> other)
    112.         {
    113.             return ptr == other.ptr;
    114.         }
    115.  
    116.         /// <summary>
    117.         /// Returns true if the value stored in this reference is equal to an object.
    118.         /// </summary>
    119.         /// <remarks>Can only be equal if the object is itself a VUnsafeRef.</remarks>
    120.         /// <param name="obj">An object to compare with.</param>
    121.         /// <returns>True if the value stored in this reference is equal to the object.</returns>
    122.         public override bool Equals(object obj)
    123.         {
    124.             if (ReferenceEquals(null, obj))
    125.             {
    126.                 return false;
    127.             }
    128.             return obj is VUnsafeRef<T> && Equals((VUnsafeRef<T>)obj);
    129.         }
    130.  
    131.         /// <summary>
    132.         /// Returns the hash code of this reference.
    133.         /// </summary>
    134.         /// <returns>The hash code of this reference.</returns>
    135.         public override int GetHashCode()
    136.         {
    137.             return Value.GetHashCode();
    138.         }
    139.  
    140.  
    141.         /// <summary>
    142.         /// Returns true if the values stored in two references are equal.
    143.         /// </summary>
    144.         /// <param name="left">A reference.</param>
    145.         /// <param name="right">Another reference.</param>
    146.         /// <returns>True if the two values are equal.</returns>
    147.         public static bool operator ==(VUnsafeRef<T> left, VUnsafeRef<T> right)
    148.         {
    149.             return left.Equals(right);
    150.         }
    151.  
    152.         /// <summary>
    153.         /// Returns true if the values stored in two references are unequal.
    154.         /// </summary>
    155.         /// <param name="left">A reference.</param>
    156.         /// <param name="right">Another reference.</param>
    157.         /// <returns>True if the two values are unequal.</returns>
    158.         public static bool operator !=(VUnsafeRef<T> left, VUnsafeRef<T> right)
    159.         {
    160.             return !left.Equals(right);
    161.         }
    162.  
    163.         public static implicit operator T(VUnsafeRef<T> reference) => reference.Value;
    164.  
    165.         /// <summary>
    166.         /// Copies the value of a reference to another reference.
    167.         /// </summary>
    168.         /// <param name="dst">The destination reference.</param>
    169.         /// <param name="src">The source reference.</param>
    170.         public static void Copy(VUnsafeRef<T> dst, VUnsafeRef<T> src)
    171.         {
    172.             if (!dst.IsCreated || !src.IsCreated)
    173.                 return;
    174.             UnsafeUtility.MemCpy(dst.ptr, src.ptr, UnsafeUtility.SizeOf<T>());
    175.         }
    176.     }
    177. }
    Value Stopwatch
    Code (CSharp):
    1. using System;
    2. using System.Diagnostics;
    3.  
    4. namespace VLib
    5. {
    6.     public struct ValueStopwatch
    7.     {
    8.         static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double) Stopwatch.Frequency;
    9.  
    10.         readonly long startTimestamp;
    11.  
    12.         public static ValueStopwatch StartNew() => new ValueStopwatch(Stopwatch.GetTimestamp());
    13.  
    14.         ValueStopwatch(long startTimestamp)
    15.         {
    16.             this.startTimestamp = startTimestamp;
    17.         }
    18.  
    19.         public TimeSpan Elapsed => TimeSpan.FromTicks(this.ElapsedTicks);
    20.  
    21.         public bool IsInvalid => startTimestamp == 0;
    22.  
    23.         public long ElapsedTicks
    24.         {
    25.             get
    26.             {
    27.                 if (startTimestamp == 0)
    28.                 {
    29.                     throw new InvalidOperationException("Detected invalid initialization(use 'default'), only to create from StartNew().");
    30.                 }
    31.  
    32.                 var delta = Stopwatch.GetTimestamp() - startTimestamp;
    33.                 return (long) (delta * TimestampToTicks);
    34.             }
    35.         }
    36.  
    37.         public float ElapsedMillisecondsF => ElapsedTicks / (float) TimeSpan.TicksPerMillisecond;
    38.        
    39.         public long ElapsedMilliseconds => ElapsedTicks / TimeSpan.TicksPerMillisecond;
    40.        
    41.         public float ElapsedSecondsF => ElapsedTicks / (float) TimeSpan.TicksPerSecond;
    42.        
    43.         public long ElapsedSeconds => ElapsedTicks / TimeSpan.TicksPerSecond;
    44.        
    45.         public float ElapsedMinutesF => ElapsedTicks / (float) TimeSpan.TicksPerMinute;
    46.        
    47.         public long ElapsedMinutes => ElapsedTicks / TimeSpan.TicksPerMinute;
    48.        
    49.         public float ElapsedHoursF => ElapsedTicks / (float) TimeSpan.TicksPerHour;
    50.        
    51.         public long ElapsedHours => ElapsedTicks / TimeSpan.TicksPerHour;
    52.     }
    53. }
     
    Last edited: Aug 11, 2023
    Kobix and Laicasaane like this.
  2. MiroLagom

    MiroLagom

    Unity Technologies

    Joined:
    Apr 28, 2023
    Posts:
    10
    Hi Virtus!

    It's awesome that you took the time to write and share you and your team's approach to thread-safety together with Burst. It may indeed help someone trying to do the same thing in the future!

    I skimmed through your code and it looks well-thought through, I didn't spot any obvious pitfalls as I was looking (but I of course can be missing something, possible race-conditions/deadlocks can be very tricky to spot). It sounds like your solution works very well for your project. If it works it works! If you do however find odd behavior stemming from Burst/Unity and bugs, please do report it so we may look at them.

    Thanks for sharing and all the best!
     
    Last edited: Aug 16, 2023
    Kobix, VirtusH and (deleted member) like this.