Search Unity

Question How to implement locking without try/finally support of Burst

Discussion in 'Burst' started by FaithlessOne, May 21, 2023.

  1. FaithlessOne

    FaithlessOne

    Joined:
    Jun 19, 2017
    Posts:
    315
    Unfortunately Burst does not support the try/finally C# statements correctly. Also the using statement with IDisposable does not work like in plain C#. Very confusing that both are stated as supported in the official documentation of Burst.

    But how can locks for multithreading scenarios be implemented then when the lock cannot be released safely without both these techniques? Here is a code sample how this can be done in plain C#:

    Code (CSharp):
    1. private int _lock;
    2.  
    3. [BurstCompile]
    4. private void AnotherMethod()
    5. {
    6.   throw new Exception("Unexpected exception");
    7. }
    8.  
    9. [BurstCompile]
    10. private void TestTryFinally()
    11. {
    12.   LockUtility.AcquireLock(ref this._lock);
    13.  
    14.   try
    15.   {
    16.     this.AnotherMethod();
    17.   }
    18.   finally
    19.   {
    20.     LockUtility.ReleaseLock(ref this._lock);
    21.   }
    22. }
    23.  
    But when using Burst compilation ReleaseLock is never called then, because the thrown exception immediately exits the TestTryFinally method. Are there any workarounds or alternative solutions?
     
  2. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,899
    The obvious solution would be to write burst-compiled Jobs for multithreading, or if you really really have to use standard C# threading then don‘t use Burst.

    The docs say something along the lines of „Burst has very limited exception support“ and what exists is mainly there to aid debugging as far as I understood.

    What is your use case for this?
     
  3. FaithlessOne

    FaithlessOne

    Joined:
    Jun 19, 2017
    Posts:
    315
    The use case I consider using Burst here is for AI processing. Because AI is unrelated to rendering it is a good option to do the processing in a separate C# thread or Burst Job. So my idea is hijacking one of the Job Workers permanently to run all AI code like pathfinding, raycastings, etc. But this would mean starting the job only once and forever. Thereafter some thread-safe synchronizations had to be done to transfer data between the ECS world and the AI job. But it seems that this is not the use case jobs are made for, but still wanted to figure out if it is reasonable to do. At least the speed bonus of Burst would be very beneficial.

    But some proper locking would then be required. Burst fully supports the Interlocked calls which is quite nice and should be preferred to use. But some advanced scenarios would be very difficult without using such critical sections as stated in my initial post.
     
  4. MarcoPersson

    MarcoPersson

    Unity Technologies

    Joined:
    Jul 21, 2021
    Posts:
    53
    Hi @FaithlessOne, as our documentation states in the Supported C# features in HPC# section about try/finally
    Actually supporting finally statements when an exception is thrown would require more general exception support (specifically stack-unwinding) which is outside of the scope of Burst.

    My recommendation would be to either simply avoid exceptions completely in the critical section (if possible), or to do your locking in a managed wrapper outside of Burst.
     
  5. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,899
    Are you interfacing with a 3rd party AI framework?

    If so, is that framework guaranteed to be thread-safe?

    If not, I'd say it'll be better to implement the AI logic entirely using Burst/Jobs/Entities.
     
  6. FaithlessOne

    FaithlessOne

    Joined:
    Jun 19, 2017
    Posts:
    315
    I understood. At least good to know that nothing planned on this topic.

    Avoiding thrown exceptions could at least be done for the 1st party code, but for other called code it cannot be ensured.
    Mixing managed and burst code would be an option, but I guess this will cost some performance in comparison to stay bursted all the time. I don't think this loss is any important in my game, but the faster the AI performs the more complex it can be at the end. But tradeoffs have to be made.

    I have the full freedom, because implementing the AI stuff myself. Yes, using the ECS way would be creating a new system that starts Burst jobs in its OnUpdate. This would be the safest way to avoid any synchronization issues or other pit falls. But on the other side having a fully dedicated bursted AI job, then you have no job scheduling and potential memory copy overhead each update. But other drawback like losing the processing power when AI is idling and handling synchronization on your own etc.
     
  7. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,899
    I think you are overestimating the cost of this vs the headaches a custom (and unsupported) locking mechanism will introduce.

    With DOTS available to you you'll end up "fighting the system" if you don't subscribe to its job system all the way. Unless you have a strong, really strong pro-performance argument going for it I would not even consider working around this system an option. If anything out of the ordinary happens down the road, you'll be on your own, no support given, no other users doing something even remotely similar.

    I kinda get the notion that you are trying to get married with a deceptively simple idea - which is having a dedicated thread that always runs in the background doing one single thing all by itself. Maybe you still remember the days of early multithreaded game engines ~20 years ago, with a DEDICATED thread for physics (how cool is that?). These days: meh. We raveled over benchmarks that saw % fps boosts beyond 50% for a second core, and if you got to 75% boost with four cores that was a dream-come-true game (to show off benchmark results)!

    The idea sounds fine on paper or from a historical perspective, but the reality of technology is probably a different one and the (modern) CPU may just not care nearly enough about all these blittable, bursted, vectorized memcopies than you think it might. ;)
     
    FaithlessOne likes this.
  8. FaithlessOne

    FaithlessOne

    Joined:
    Jun 19, 2017
    Posts:
    315
    Yes, you are right. I stick to the intended way and starting jobs from an ECS system. That would be safe and saves the headaches. Then the OnUpdate would be the point of synchronization. I still would use an IJob instead of IJobEntity to avoid direct IComponentData dependencies and enable that the AI job can run over multiple frames without blocking rendering, physics, etc.