GithubHelp home page GithubHelp logo

timcassell / protopromise Goto Github PK

View Code? Open in Web Editor NEW
131.0 4.0 13.0 8.53 MB

Robust and efficient library for management of asynchronous operations in C#/.Net.

License: MIT License

C# 99.60% PowerShell 0.40%
async await promises csharp dotnet task coroutine concurrency unity promise

protopromise's People

Contributors

mattyleecifer avatar timcassell avatar timperfect avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

protopromise's Issues

`ParallelFor`, `ParallelForEach`, and Synchronous `Wait/GetResult` APIs

This was mentioned at the bottom of #12. Promises currently can only be awaited asynchronously to prevent deadlocks. However, there may be times where parallel workloads needs to be waited on to complete in a synchronous method. A PromiseTask class can be introduced to support this type of work.

public int Func()
{
    PromiseTask<int> task = PromiseTask.Run(() =>
    {
        // Do work on background thread.
        return 42;
    });
    // Do work on current thread.
    int fgResult = 21;
    int bgResult = task.GetResult(); // Wait for background work to complete and get its result synchronously.
    return fgResult + bgResult;
}

Built-in Tasks already provide this functionality via Task.Wait() and Task<T>.Result, but it's easy to accidentally cause deadlocks with that. PromiseTask represents a work item that is running on a background thread (never foreground), thus there is no concern for deadlocks. It can be waited on synchronously or asynchronously, and will have implicit casts to Promise(<T>).

PromiseTask.Run runs a single action on a background thread.
public static PromiseTask Run(Action action) runs an action returning void, and returns PromiseTask.
public static PromiseTask<T> Run<T>(Func<T> function) runs a function returning a T value, and returns PromiseTask<T>.
Unlike Promise.Then callbacks, no special considerations should be made to adopt the state of a returned PromiseTask or Promise (as that would mean they could be waiting for an action to complete on the current or foreground thread and cause a deadlock)

Besides that, PromisTask can have more APIs to do more complex things, like PromiseTask.Merge, PromiseTask.All, PromiseTask.Race, PromiseTask.First to complement the static Promise.All etc merge functions.

It can also have APIs like PromiseTask.ForEach to iterate over an enumerable and do an action on each item in parallel, and a PromiseTask.For. These would work very similar to Parallel.For and Parallel.ForEach except that they are asynchronous work items, meaning you can still do more work on the current thread while they are executing, then wait for their results later.

void PromiseTask.Wait() is an instance method to wait for it to complete synchronously, and will throw if the background action(s) did not complete successfully.
T PromiseTask<T>.GetResult() is an instance method that behaves the same as Wait(), except it returns the T value that the PromiseTask<T> produced.

To avoid having to catch exceptions, a PromiseTask<T>.GetResultContainer() may be added to get a result container that wraps the final state (Resolved, Rejected, or Canceled) and the result or exception.

It can also have a PromiseTask.Forget() call the same as Promise.Forget() if the user just wants to fire-and-forget.


Not all of these APIs need to be implemented at once. We can start with PromiseTask.Run and PromiseTask.Wait and PromiseTask<T>.GetResult and awaitable and implicit cast to Promise(<T>).

Add Promise `ParallelFor` and `ParallelForEach` APIs

Promise Promise.ParallelForEach<T>(IEnumerable<T> source, Action<T> body) iterates over an enumerable and invokes an action on each item in parallel (posted to the background context).
Promise Promise.ParallelFor(int fromInclusive, int toExclusive, Action<int> body) executes a for loop, where the index of each iteration is passed to an action in parallel.

They both return a Promise that will be resolved when all actions are completed successfully, or rejected with an AggregateException containing all exceptions if any action failed.

And of course overloads to pass capture values to the actions.

These work very similar to Parallel.For and Parallel.ForEach except that they are asynchronous work items, meaning you can still do more work on the current thread while they are executing, then wait for their results later.

These are starting simple APIs, we can expand to more advanced APIs in the future (like MaxDegreeOfParallelism, ParallelLoopState, etc).

Split from #24.

Execution context switching

Context switching in this case refers to switching execution between the main/UI thread and background threads. Executing code on a background thread frees up the UI thread to draw the application at a higher frame-rate and not freeze the application when executing an expensive computation. Multiple work items can also be executed in parallel, speeding up some calculations.


Built-in Tasks allow you to explicitly switch to background threads, then from the background thread switch back to the UI thread by utilizing the SynchronizationContext captured before switching to the background thread. In an async Task function, this is done with the simple Task.ConfigureAwait(bool) method. The problem with this method is, once you have switched to the background context, you cannot return to the UI context later in the function unless you manually capture the SynchronizationContext and use a much more complex API. You might also have no idea whether the context you're capturing is the UI context or the background context, in which case it is impossible to marshal any work to the UI thread.

For ProtoPromise, we can simplify this functionality and solve the impossible problem. Since we already have Promise.Manager.HandleCompletes() which is called at regular intervals (every frame) on the main/UI thread, we can ignore SynchronizationContext and use a simple enum:

public enum ExecutionOption
{
    Synchronous,
    UI,
    Background
}

With these options, the context is no longer captured, but rather stated explicitly. Synchronous will execute synchronously on whatever thread happens to complete the promise. UI will marshal the execution back to the UI thread via Promise.Manager.HandleCompletes(). Background will marshal the execution onto a background thread.

It can be used like so:

public async Promise Func()
{
    // Who knows what thread we're on?
    var text = await DownloadText1().ConfigureAwait(ExecutionOption.Background);
    // Do work on a background thread.
    int value = Parse(text);
    text = await DownloadText2().ConfigureAwait(ExecutionOption.UI);
    // Do work on the UI thread.
    int result = value + Parse(text);
}

If a Promise is awaited without being configured, the default option is Synchronous.


For async Promise functions, it would be useful to simply switch contexts without awaiting a Promise:

public async Promise Func()
{
    // Who knows what thread we're on?
    await Promise.SwitchToBackground();
    // Do work on a background thread.
    await Promise.SwitchToUI();
    // Do work on the UI thread.
}

ExecutionOption can also be added as an additional optional parameter to Then, ContinueWith, etc. methods, allowing the callback to be ran on the chosen context.


Tasks can start an operation in the background by calling Task.Run(callback). ProtoPromise should add similar Promise.RunInBackground(callback) functions that can be used to marshal a work item to the background, and Promise.RunInUI(callback) to marshal a work item to the UI thread.

Task.Run returns a Task which can be waited on synchronously to complete via Task.Wait() or Task.Result, blocking the thread until it's complete. This is inherently dangerous, as any task can be waited in that fashion, easily causing a deadlock. To prevent that from happening while still serving that functionality, Promise.RunInBackground should return a new type that can block until complete, while normal Promise(<T>) instances can only be awaited asynchronously. This new type could be called PromiseTask(<T>) or something similar, and it should have an implicit cast to Promise(<T>).

Promise.RunInUI should return a Promise(<T>), as there is no reason to ever block a thread waiting on the UI thread (and we definitely do not want the UI thread to deadlock).


This proposal relies on #8.

Unity Warnings (Demo Scripts exists outside the Asset folder)

It is not a 'critical' issue - but rather a bit Console Polluting.
When the package is added, there are 2 warnings popping up (spawning multiple times after code changes) about some scripts (Demo/DownloadHelperExample.cs, Demo/ProtoPromiseExample.cs) that exist outside the assets folder.

ProtoPromise_Warnings

Would it be possible to fix these minor issues?
(Maybe by adding a asmdef for the Demo folder)

I'm currently using Unity2021.3.1f1

Thanks in advance!

Add Promise `Wait()` and `GetResult()` APIs

void Promise.Wait() is an instance method to wait for it to complete synchronously, and will throw if the Promise did not complete successfully (the same way await throws).
T Promise<T>.GetResult() is an instance method that behaves the same as Wait(), except it returns the T value that the Promise<T> produced.

These APIs are advanced and should not be used unless the user knows what they are doing, as they can easily cause deadlocks. Usually they are meant to be combined with Promise.Run(...) to wait for the result from another thread synchronously.

Split from #24.

AwaitWithProgress with a single value

Currently, AwaitWithProgress takes 2 arguments, float minProgress and float maxProgress. When the promise is awaited, the async Promise's progress is set to minProgress.

async Promise Func()
{
    await DownloadThing("thing1").AwaitWithProgress(0f, 0.25f);
    await DownloadThing("thing2").AwaitWithProgress(0.25f, 0.5f);
    await DownloadThing("thing3").AwaitWithProgress(0.5f, 0.75f);
    await DownloadThing("thing4").AwaitWithProgress(0.75f, 1f);
}

We can add a new overload that takes only 1 float maxProgress, which will preserve the async Promise's current progress when it is awaited, effectively setting minProgress to that current progress. That will simplify the code to:

async Promise Func()
{
    await DownloadThing("thing1").AwaitWithProgress(0.25f);
    await DownloadThing("thing2").AwaitWithProgress(0.5f);
    await DownloadThing("thing3").AwaitWithProgress(0.75f);
    await DownloadThing("thing4").AwaitWithProgress(1f);
}

Which results in less typing and clearer code for the most part. This would also make it possible to have progress never go backwards in the event of retries, if the design calls for it:

async Promise Func()
{
Retry:
    try
    {
        await DownloadThing("thing1").AwaitWithProgress(1f);
    }
    catch
    {
        goto Retry;
    }
}

In this case, if the progress incremented to 0.5, then rejected for some reason, the retry would then lerp the progress from 0.5 to 1, instead of 0 to 1.

ValueTask interoperability

.Net Core 2.1 and .Net Standard 2.1 added IValueTaskSource<TResult> for custom types to back ValueTask<T> (and same for non-generic).

We can make our internal PromiseRef classes implement IValueTaskSource<TResult> to be able to convert a Promise to a ValueTask without allocation.

We can add Promise.AsValueTask() as well as an implicit cast. And we can add a ValueTask.ToPromise() extension method the same way we have a Task.ToPromise() extension method.

Throw original exception from await

Something to investigate before releasing v2.0 is throwing the original exception when awaiting promises, instead of throwing a wrapper exception. This is how Tasks behave, and it would make it easier to filter exception types with try/catch clauses in async functions. We can use ExceptionDispatchInfo.Capture(Exception) and ExceptionDispatchInfo.Throw() to preserve the causality trace information that the wrapper exception is currently doing.

For non-exception rejections, the wrapper exception can still be thrown (because C# doesn't support throwing and catching non-exception objects).

Thread Safety

How it works now

ProtoPromise v1 only supports usage on the main thread, and even has an extra check in there to make sure it is only accessed from a single thread in DEBUG mode (it sends a warning to the warning handler if it exists, otherwise it throws). This is undocumented.

The way Promises work are when a promise is resolved, rejected, or canceled, the callbacks that were subscribed to it get added to the event queue instead of executed synchronously. The next time Promise.Manager.HandleCompletes(AndProgress) is called (which happens automatically with the Unity package) is when the callbacks are dequeued and executed.

This behavior follows the Promises/A+ specification 2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code. It also means that callbacks are executed iteratively instead of recursively so that the library will never cause a stack overflow (this is why the benchmarks always work with high values of N while other libraries fail). This also helps with usability, allowing programmers to add as many callbacks as they want to a single Promise object within a single method. Uncaught rejections and object pooling kicks in later in the execution pipeline when the event queue is handled.

The problem

Unfortunately (or rather, fortunately), unlike javascript, C# is not relegated to a single thread (depending on the target application). Advanced C# programmers make great use of threads to speed up their applications. Simply adding "thread safety" to an individual Promise object introduces problems when you want to add multiple callbacks to it. While you're adding callbacks in a background thread, the main thread could be executing the event queue and trying to put that Promise object back in the object pool, thus invalidating any further actions to that Promise object, or throwing unhandled rejections before the background thread has a chance to add a rejection handler.

Of course, if we just get rid of object pooling entirely and remove the event queue to just execute callbacks synchronously, this would be a non-issue (like built-in Tasks), but one of the goals of this library was to reduce GC pressure, so I don't think that's the way to go (otherwise, why not just use built-in Tasks?), plus you can see from the benchmarks that object pooling greatly increases the library's speed (as minuscule of a difference as that makes in a real application, every bit matters). Also, uncaught rejections would need to be caught in the Promise's finalizer rather than during the event handling, further increasing GC pressure.

RSG Promises handle the uncaught rejections by forcing the programmer to call Done() on the last promise in the chain. UniTask has a similar Forget() that you can call to try to have the underlying object added back to the object pool. ProtoPromise currently does it opposite, where it automatically handles uncaught rejections and repooling in the event handler, but you can also prevent that from happening by calling Retain() if you need to use the Promise object for a longer lifetime (not recommended because it can be difficult to keep track of retained objects to release them later).

If we have a Promise.Run(Action) that executes a delegate on a background thread (to replace Task.Run), we could set up a thread-local event queue, but that doesn't solve the issue of 2 separate threads using a single Promise object, and actually increases the contention. Also, a lot of C# applications are already using threads by other means (new Thread()), and those threads should be able to interact with Promises without issues.

Possible solution

UniTask by default only lets you await a task once. You can call Preserve() to allow you to await it as much as you want, along with Forget() to allow it to repool again. What if we copy that behavior?

  • Execute callbacks synchronously when a promise is settled instead of placing on event queue (this will take some extra work to execute iteratively). Only Progress callbacks will be executed on the main thread through the existing mechanism.
  • Promises can only be awaited once by default (Then, Catch, ContinueWith, await)

Now, to be able to await multiple times/add as many callbacks as we want, we need a similar Preserve() method that will need to be called before we add any callbacks. We already have Retain() so let's just keep that. We also need a Forget() like UniTask to handle uncaught rejections and repooling. We already have Release(), but if we always use Release() as the promise chain terminator, that makes it impossible for multiple threads to retain a single Promise object. So let's use Release() as the counter to Retain(), and add a new Forget() as the chain terminator (RSG uses Done, but I think Forget is more descriptive).

promise
    .Then(() => {})
    .Then(() => {})
    .Forget();

What if we want to add a callback to a promise, and then return that promise?

promise.Retain();
promise.Then(() => {}).Forget();
promise.Release();
return promise;

Retain allows as many awaits as you want until Release is called. After that, it reverts to its initial state of only allowing a single await, or Forget.

What if you call Forget() on a retained Promise object? Should it still allow callbacks to be added until Release() is called? Or should it throw an exception saying that Release must be called before Forget? I'm leaning towards the former to allow users to cache Promise objects (even though it's not recommended) and to allow a thread to keep using a promise after another thread forgot it.
If we allow callbacks to still be added after it's forgotten and until it's released, Forget should also be allowed to be called any number of times. If a promise is already forgotten while retained, when it is fully released, the last Release() call acts the same as a normal Forget() call (it handles uncaught rejections and repools the object).

Further changes needed

With callbacks being executed synchronously upon Promise resolution, deferreds must be changed to invalidate themselves when they are resolved/rejected/canceled. This makes the State property completely useless. The only thing that will matter then is IsPending which is equivalent to IsValid.

With these hefty changes, Promises might also benefit from being changed to struct instead of class, similar to how CancelationTokens work (and even Deferreds themselves). This would allow better validity checks and let us enable object pooling in DEBUG mode (currently Promises are not pooled in DEBUG mode to allow for validity checks, which are completely absent from RELEASE builds). Pooling could also be simplified to on or off, completely removing the internal option (all pooled objects would be internal with this change). Furthermore, changing Promises to structs would increase the efficiency of already-completed Promises since it would only need to live on the stack and not have to go to the heap to find/create a Promise object.

[Edit] The downside of changing promises to structs is we lose the Promise<T> : Promise inheritance, and all the benefits that brings with it.

ProtoPromise v2?

Such big changes to support threads surely require a major version change. It might even need a separate fork of its own, what do you think?

Race with win index

When racing promises, it can sometimes be useful to know which promise won. Currently, Promise.Race and Promise.First return a promise of the same type as the inputs, and there is no easy way to know which promise won.

I propose adding Promise.RaceWithIndex and Promise.FirstWithIndex that will return Promise<int> for void promises, or Promise<(int winIndex, T result)> for result promises. The resulting index will be that of the promise that won the race.

var (winIndex, result) = await Promise<T>.RaceWithIndex(promise1, promise2);

Add `AsyncLock`

Adding an AsyncLock type akin to AsyncEx would be useful for locking in async code, and we can do it more efficiently (AsyncEx wraps Tasks which are heavier than our Promises).

Test failure

https://github.com/timcassell/ProtoPromise/actions/runs/3117637137/jobs/5056811984

NewAndRunTests.PromiseRunIsInvokedAndCompletedAndProgressReportedProperly_adopt_capture_T(Background,Foreground,Resolve,False,False)
 at ProtoPromiseTests.ProgressHelper.GetCurrentProgressEqualsExpected (System.Single expectedProgress, System.Boolean waitForInvoke, System.Boolean executeForeground, System.TimeSpan timeout) [0x0001a] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/ProgressHelper.cs:121 
  at ProtoPromiseTests.ProgressHelper.ReportProgressAndAssertResult (Proto.Promises.Promise+DeferredBase deferred, System.Single reportValue, System.Single expectedProgress, System.Boolean waitForInvoke, System.Boolean executeForeground, System.TimeSpan timeout) [0x00024] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/ProgressHelper.cs:163 
  at ProtoPromiseTests.APIs.NewAndRunTests.PromiseRunIsInvokedAndCompletedAndProgressReportedProperly_adopt_capture_T (ProtoPromiseTests.SynchronizationType synchronizationType, ProtoPromiseTests.SynchronizationType invokeContext, ProtoPromiseTests.CompleteType completeType, System.Boolean throwInAction, System.Boolean isAlreadyComplete) [0x0016d] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Tests/APIs/NewAndRunTests.cs:1180 
  at (wrapper managed-to-native) System.Reflection.MonoMethod.InternalInvoke(System.Reflection.MonoMethod,object,object[],System.Exception&)
  at System.Reflection.MonoMethod.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00032] in <c8d0d7b9135640958bff528a1e374758>:0 
--TimeoutException
  at ProtoPromiseTests.ProgressHelper.MaybeWaitForInvoke (System.Boolean waitForInvoke, System.Boolean executeForeground, System.TimeSpan waitTimeout) [0x00085] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/ProgressHelper.cs:87 
  at ProtoPromiseTests.ProgressHelper.GetCurrentProgress (System.Boolean waitForInvoke, System.Boolean executeForeground, System.TimeSpan timeout) [0x00014] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/ProgressHelper.cs:151 
  at ProtoPromiseTests.ProgressHelper.GetCurrentProgressEqualsExpected (System.Single expectedProgress, System.Boolean waitForInvoke, System.Boolean executeForeground, System.TimeSpan timeout) [0x00002] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/ProgressHelper.cs:116 
--TearDown
  at Proto.Promises.Internal.AssertAllObjectsReleased () [0x000d5] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromise/Core/InternalShared/PoolInternal.cs:213 
  at ProtoPromiseTests.TestHelper.Cleanup () [0x0004b] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/TestHelper.cs:146 
  at ProtoPromiseTests.APIs.NewAndRunTests.Teardown () [0x00001] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Tests/APIs/NewAndRunTests.cs:26 
  at (wrapper managed-to-native) System.Reflection.MonoMethod.InternalInvoke(System.Reflection.MonoMethod,object,object[],System.Exception&)
  at System.Reflection.MonoMethod.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00032] in <c8d0d7b9135640958bff528a1e374758>:0 

Add `CancelationToken.ToCancellationToken()`

I know the title sounds weird. But here is the API:

public System.Threading.CancellationToken ToCancellationToken()

The motivation for this is that the built-in .Net APIs are increasingly relying on CancellationToken, which are a separate type from this library's CancelationToken. Other third-party libraries also rely on it. In order to interop with those APIs while still using this library, there needs to be a simple way to convert a Proto.Promises.CancelationToken to a System.Threading.CancellationToken.

You might ask, "why not just ditch Proto.Promises.CancelationToken and switch over fully to System.Threading.CancellationToken?".

The answer to this is that System.Threading.CancellationTokenSources cannot be re-used. Proto.Promises.CancelationSource can be re-used to reduce GC pressure.

.Net 6.0 is also adding System.Threading.CancellationTokenSource.TryReset() as a new API that can be used to attempt to reduce allocations, and that can be used internally by the Proto.Promises.CancelationToken. Proto.Promises.CancelationToken.ToCancellationToken() will not be able to guarantee allocation-free the way the rest of the library does with pooling enabled, but cancelations are the exception, not the rule, so it will usually not allocate.


Unfortunately, a ToCancelationToken() extension cannot be added to System.Threading.CancellationToken, because there is no way to hook into the internal Dispose of the source in order to dispose of our source. Because of that, I may look into adding System.Threading.CancellationToken consumption in the public APIs as overloads. But that would be a large undertaking to support both kinds of tokens.

[Edit] It would be possible if an API is exposed to be able to hook into that (like this discussion), but I don't see that getting off the ground.

[Edit2] Actually, it looks like it is possible to implement a proper System.Threading.CancellationToken.ToCancelationToken() extension: dotnet/runtime#60843 (comment), though it will be more expensive than converting the other way (more allocations that cannot be amortized away).

Concurrent test failure

https://github.com/timcassell/ProtoPromise/runs/7703914830?check_suite_focus=true

ProtoPromiseTests.Threading.PromiseConcurrencyTests.PromiseProgressMayBeSubscribedWhilePromiseIsCompletedAndProgressIsReportedConcurrently_Pending_T(Parallel,Parallel,Parallel,CancelFromToken,Interface,Foreground)
--IndexOutOfRangeException
   at Proto.Promises.Internal.ValueList`1.get_Item(Int32 index) in D:\a\ProtoPromise\ProtoPromise\ProtoPromise_Unity\Assets\Plugins\ProtoPromise\Core\InternalShared\ValueCollectionsInternal.cs:line 438
   at Proto.Promises.Internal.PromiseRefBase.PromiseMultiAwait`1.InvokeProgressFromContext() in D:\a\ProtoPromise\ProtoPromise\ProtoPromise_Unity\Assets\Plugins\ProtoPromise\Core\Promises\Internal\ProgressInternal.cs:line 814
   at Proto.Promises.Internal.StackUnwindHelper.InvokeProgressors() in D:\a\ProtoPromise\ProtoPromise\ProtoPromise_Unity\Assets\Plugins\ProtoPromise\Core\Promises\Internal\ProgressInternal.cs:line 99
   at Proto.Promises.Internal.PromiseRefBase.DeferredPromiseBase`1.ReportProgressAlreadyIncremented(Fixed32 progress, HandleablePromiseBase progressListener) in D:\a\ProtoPromise\ProtoPromise\ProtoPromise_Unity\Assets\Plugins\ProtoPromise\Core\Promises\Internal\ProgressInternal.cs:line 882
   at Proto.Promises.Internal.PromiseRefBase.DeferredPromiseBase`1.TryReportProgress(Int32 deferredId, Single progress) in D:\a\ProtoPromise\ProtoPromise\ProtoPromise_Unity\Assets\Plugins\ProtoPromise\Core\Promises\Internal\ProgressInternal.cs:line 859
   at Proto.Promises.Internal.DeferredPromiseHelper.TryReportProgress(IDeferredPromise _this, Int32 deferredId, Single progress) in D:\a\ProtoPromise\ProtoPromise\ProtoPromise_Unity\Assets\Plugins\ProtoPromise\Core\Promises\Internal\DeferredInternal.cs:line 48
   at Proto.Promises.Promise`1.Deferred.TryReportProgress(Single progress) in D:\a\ProtoPromise\ProtoPromise\ProtoPromise_Unity\Assets\Plugins\ProtoPromise\Core\Promises\Deferred.cs:line 821
   at ProtoPromiseTests.Threading.PromiseConcurrencyTests.<>c__DisplayClass14_0.<PromiseProgressMayBeSubscribedWhilePromiseIsCompletedAndProgressIsReportedConcurrently_Pending_T>b__3() in D:\a\ProtoPromise\ProtoPromise\ProtoPromise_Unity\Assets\Plugins\ProtoPromiseTests\Tests\Threading\PromiseConcurrencyTests.cs:line 485
   at ProtoPromiseTests.Threading.ThreadHelper.ThreadRunner.Execute() in D:\a\ProtoPromise\ProtoPromise\ProtoPromise_Unity\Assets\Plugins\ProtoPromiseTests\Helpers\ThreadHelper.cs:line 71

Bug: allocation when awaiting resolved promise

Benchmarks are showing an allocation that should not happen:

|                  Type |          Method |   N | BaseIsPending |      Mean | Code Size | Allocated | Survived |
|---------------------- |---------------- |---- |-------------- |----------:|----------:|----------:|---------:|
|          AwaitPending | ProtoPromise_V2 | 100 |             ? | 382.57 ฮผs |  13,141 B |         - |    416 B |
|          AsyncPending | ProtoPromise_V2 | 100 |             ? |  86.27 ฮผs |   7,240 B |         - |    728 B |
|         AsyncResolved | ProtoPromise_V2 | 100 |             ? |  16.82 ฮผs |   2,172 B |         - |        - |
|         AwaitResolved | ProtoPromise_V2 | 100 |             ? |  11.03 ฮผs |   1,906 B |      72 B |        - |
|   ContinueWithPending | ProtoPromise_V2 | 100 |         False | 412.14 ฮผs |  17,455 B |         - | 17,264 B |
| ContinueWithFromValue | ProtoPromise_V2 | 100 |         False |  25.16 ฮผs |   8,005 B |         - |        - |
|  ContinueWithResolved | ProtoPromise_V2 | 100 |         False |  24.67 ฮผs |   8,195 B |         - |        - |
|   ContinueWithPending | ProtoPromise_V2 | 100 |          True | 403.14 ฮผs |  14,912 B |         - | 17,216 B |
| ContinueWithFromValue | ProtoPromise_V2 | 100 |          True | 112.18 ฮผs |   9,504 B |         - | 17,144 B |
|  ContinueWithResolved | ProtoPromise_V2 | 100 |          True | 126.77 ฮผs |   9,521 B |         - | 17,144 B |

Originally posted by @timcassell in #26 (comment)

NullReferenceException

Got a null ref in a CI run. https://github.com/timcassell/ProtoPromise/actions/runs/3458362187/jobs/5772714156

The stacktrace is useless since it was ran in Release, but putting the log here since it will be lost when Github wipes the logs.

Begin time: 00:30:45.5324112, test: ProtoPromiseTests.Threading.MergeConcurrencyTests.DeferredsMayBeCompletedWhileTheirPromisesArePassedToMergeConcurrently_T2void(Parallel_WithProgress,CancelFromToken,Resolve,Resolve)
Begin time: 00:30:46.3836158, test: ProtoPromiseTests.Threading.MergeConcurrencyTests.DeferredsMayBeCompletedWhileTheirPromisesArePassedToMergeConcurrently_T2void(InSetup_ProgressParallel,Resolve,Resolve,Resolve)
  Failed DeferredsMayBeCompletedWhileTheirPromisesArePassedToMergeConcurrently_T2void(Parallel_WithProgress,CancelFromToken,Resolve,Resolve) [851 ms]
  Error Message:
   System.Exception : No message provided
  ----> System.NullReferenceException : Object reference not set to an instance of an object.
TearDown : NUnit.Framework.AssertionException :   Expected: "Fail"
  But was:  <Proto.Promises.UnreleasedObjectException: CancelationSource's resources were garbage collected without being disposed.>

  Stack Trace:
     at ProtoPromiseTests.Threading.ThreadHelper.ExecutePendingParallelActions(TimeSpan timeoutPerAction) in /home/runner/work/ProtoPromise/ProtoPromise/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/ThreadHelper.cs:line 196
   at ProtoPromiseTests.Threading.ThreadHelper.ExecuteParallelActionsWithOffsets(Boolean expandToProcessorCount, Action setup, Action teardown, Action[] actions) in /home/runner/work/ProtoPromise/ProtoPromise/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/ThreadHelper.cs:line 283
   at ProtoPromiseTests.Threading.MergeConcurrencyTests.DeferredsMayBeCompletedWhileTheirPromisesArePassedToMergeConcurrently_T2void(CombineType combineType, CompleteType completeType0, CompleteType completeType1, CompleteType completeTypeVoid) in /home/runner/work/ProtoPromise/ProtoPromise/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Tests/Threading/MergeConcurrencyTests.cs:line 180
--NullReferenceException
   at ProtoPromiseTests.Threading.ParallelCombineTestHelper.ParallelCombineTestHelperT`1.<MaybeAddParallelAction>b__3_1() in /home/runner/work/ProtoPromise/ProtoPromise/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/ThreadTestHelper.cs:line 138
   at ProtoPromiseTests.Threading.ThreadHelper.ThreadRunner.Execute() in /home/runner/work/ProtoPromise/ProtoPromise/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/ThreadHelper.cs:line 89
--TearDown
   at ProtoPromiseTests.TestHelper.Cleanup() in /home/runner/work/ProtoPromise/ProtoPromise/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/TestHelper.cs:line 166
   at ProtoPromiseTests.Threading.MergeConcurrencyTests.Teardown() in /home/runner/work/ProtoPromise/ProtoPromise/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Tests/Threading/MergeConcurrencyTests.cs:line 36
  Failed DeferredsMayBeCompletedWhileTheirPromisesArePassedToMergeConcurrently_T2void(InSetup_ProgressParallel,Resolve,Resolve,Resolve) [8 s]
  Error Message:
   System.TimeoutException : 7 Action(s) timed out after 00:00:07, there may be a deadlock.
TearDown : System.TimeoutException : WaitForAllThreadsToComplete timed out after 00:00:02, _runningActionCount: 2
  Stack Trace:
     at ProtoPromiseTests.Threading.ThreadHelper.ExecutePendingParallelActions(TimeSpan timeoutPerAction) in /home/runner/work/ProtoPromise/ProtoPromise/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/ThreadHelper.cs:line 196
   at ProtoPromiseTests.Threading.ThreadHelper.ExecuteParallelActionsWithOffsets(Boolean expandToProcessorCount, Action setup, Action teardown, Action[] actions) in /home/runner/work/ProtoPromise/ProtoPromise/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/ThreadHelper.cs:line 283
   at ProtoPromiseTests.Threading.MergeConcurrencyTests.DeferredsMayBeCompletedWhileTheirPromisesArePassedToMergeConcurrently_T2void(CombineType combineType, CompleteType completeType0, CompleteType completeType1, CompleteType completeTypeVoid) in /home/runner/work/ProtoPromise/ProtoPromise/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Tests/Threading/MergeConcurrencyTests.cs:line 180
--TearDown
   at ProtoPromiseTests.BackgroundSynchronizationContext.WaitForAllThreadsToComplete() in /home/runner/work/ProtoPromise/ProtoPromise/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/BackgroundSynchronizationContext.cs:line 94
   at ProtoPromiseTests.TestHelper.WaitForAllThreadsToCompleteAndGcCollect() in /home/runner/work/ProtoPromise/ProtoPromise/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/TestHelper.cs:line 173
   at ProtoPromiseTests.TestHelper.Cleanup() in /home/runner/work/ProtoPromise/ProtoPromise/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/TestHelper.cs:line 166
   at ProtoPromiseTests.Threading.MergeConcurrencyTests.Teardown() in /home/runner/work/ProtoPromise/ProtoPromise/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Tests/Threading/MergeConcurrencyTests.cs:line 36

Improve performance of progress reporting

Currently, progress is subscribed in O(1) time and space, optimized in #63. The optimizations there also improved the baseline performance of promises when progress isn't used, but they added the negative effect of increasing the time of resolving a .Then chain of promises to O(n^2) time, even if progress is never subscribed to (because progress reports iterate over the entire promise tree). Ideally, resolving a promise chain should complete in O(n) time.

I believe the time can be improved by going back to the previous way of subscribing progress, where it iterates backwards up the promise chain and subscribes the listener to each promise, and progress reports only report directly to the listener instead of iterating over the entire promise tree. This increases the subscribe time back to O(n), but it also decreases progress report time from O(n) to O(1), well worth the trade-off for a 1-time cost vs potentially multiple progress reports, and more importantly, .Then promise chains without subscribed progress will be more efficient.

The O(1) space can be preserved by just using the same object to subscribe to each promise in the chain (and incrementing a retain counter for each). Calling promise.Progress twice in a row won't iterate the entire chain, it will just stop at the previous progress object, so multiple objects won't need to be added to each promise object.
Crucially, to prevent increasing the memory footprint and execution time of promises ignoring progress, some tricks must be employed to re-use the existing fields.

  • Obviously, to iterate over the chain we must have a PromiseRefBase _previous field, but this is currently only used in DEBUG mode. Since we don't want to add a new field in RELEASE mode, we can combine it with the _rejectContainer field, changing its type to object and using it the same way we used to (if the previous has not yet completed, it will contain the previous, otherwise it will contain the reject container or null).
  • To subscribe the progress object to each promise, one might think we need a field to store it. Luckily, the progress object already inherits from the same HandleablePromiseBase type as all the other promise objects (done in #8), so instead of adding a new field, we can re-use the HandleablePromiseBase _next and just swap the existing waiter with the progress object, and store that waiter in the progress object so that the progress object will act as a pass-through. This should make it so the promise resolution logic remains the same and efficient (no separate HandleProgressListener() call like we used to do). Even though it will have to store multiple waiters, lucky for us is that the storage of the waiters won't cost any extra memory, since they are already linked via their _next fields, so it can store them in the form of a linked-stack (only using a single reference field). [Edit] I realized if we're overwriting the _next field with the progress object, we can't link all waiters with the same field. So I will need to either store them in a separate collection, or use a different field to link them (possibly re-use _previousOrRejectContainer?).

The wrinkles are with promise.Preserve and Promise.Merge/All/Race/First, since they introduce divergence and convergence in the promise tree. In both cases we should be able to treat the PromiseMultiAwait and MultiHandleablePromiseBase/ PromisePassThrough objects as special variants of PromiseProgress, but I'm not 100% sure of this until I start the implementation.

These changes will bring back the complexity that was removed in #63, but it should be worth it to get those performance gains, especially considering listening to progress is the exception rather than the norm. These changes should ideally not adversely affect the execution time or memory consumption of promises without progress, but special care will need to be taken for thread-safety (subscribing progress was the most difficult thread-safety issue for v2.0.0, and it will be again with these changes). I need to make sure the thread-safety won't slow anything down (I can't be sure until I actually implement the changes and benchmark it, and I'm unsure of how complex the thread-safety will be until I start digging into it; hopefully less than it was before).


Regarding listening to progress on async Promise promises, currently they don't have the same performance pitfall as .Then chains if .AwaitWithProgress is not used. But if it is used, then the same performance pitfall exists even if .Progress is never called. A similar strategy should be able to be used for that as whatever implementation I decide to do for promise.Then(() => otherPromise), but I will need to investigate this more after I get started on the implementation.

Separate Nuget package for Unity helpers

RestClient for Unity is interested in adopting ProtoPromise proyecto26/RestClient#216. They have a Nuget package with UnityEngine dependencies. The ProtoPromise Nuget package does not have any UnityEngine dependencies so that it can work on any .Net platform.

We do have some UnityEngine depencies in the UnityPackage for helpers like converting coroutines to Promises and executing the PromiseSynchronizationContext for foreground scheduling. Obviously, I would recommend just using the UnityPackage for source code, but we should create a separate Nuget package that contains those Unity helpers with the UnityEngine.dll dependency so that RestClient and other Unity-based libraries who wish to use ProtoPromise and host their package on Nuget can add a dependency to that helper package.

StackTraceHiddenAttribute

.Net 6 added StackTraceHiddenAttribute which we can use to more easily omit internal methods from causality traces. It will also help to eliminate some methods that are impossible to remove from exception stacktraces prior to .Net 6 (delegate wrappers for Promise.Then).

Improve readability of readme

In #189, I was made aware of a readability issue in the readme where a new user missed some important information because it was placed after the Advanced section. Readability can be improved by placing the Advanced section last in the readme.

PromiseBehaviour warning when not reloading domain

Hi,

When I enable Settings > Editor > Enter Play Mode, I get this msg:
There can only be one instance of PromiseBehaviour. Destroying new instance

I suspect this is expected behavior given the nature of the setting but worth raising for those who are OCD about warnings.

Optimize async method builder in Unity 2021.2

https://issuetracker.unity3d.com/issues/il2cpp-incorrect-results-when-calling-a-method-from-outside-class-in-a-struct

This issue which forced us to use a less efficient async method builder in IL2CPP builds was fixed in Unity 2020.3.20f1 and 2021.1.24f1. We can #if UNITY_2021_2_OR_NEWER to enable the optimized builder for IL2CPP builds that other runtimes enjoy (it would take too many ifdefs to also support 2020, because unity doesn't include OR_NEWER symbols for patch releases).

Originally posted by @timcassell in #26 (comment)

`WaitAsync` forceAsync flag

Currently, for optimization purposes, if it is currently executing on the same context that was provided to WaitAsync or Run or Progress, the continuation will be invoked synchronously. This may not be desired in some instances if the user wants to force an action to run on a separate thread for increased parallelization. For that we can add an optional forceAsync flag to WaitAsync, Run, and Progress functions that, if true, will always schedule the continuation on the desired context. The default will be false.

`CancellationToken.ToCancelationToken()` broken after `CancellationToken.TryReset()`

.Net 6 introduced CancellationTokenSource.TryReset() which unregisters all callbacks if it hasn't already been canceled. CancellationToken.ToCancelationToken() caches an internal source so that it doesn't have to allocate a new object if ToCancelationToken() is called on the same token more than once, but since the System.Threading.CancellationToken API does not support a way to detect unregistration, the cached object can't re-register itself, making the returned token unable to be canceled when the original token is canceled.

Promises wrapper for Unity's AssetBundle.LoadAssetAsync()?

Hi,

What is the proper Promises invocation to wrap Unity's AssetBundle.LoadAssetAsync()?

When using UnityAsync, I was just able to call:

T obj = await _assetBundle.LoadAssetAsync<T>(nameID);

But using Promises I'm unclear if there's even a convenience wrapper or what it looks like under Promises in this case.

Prefix `bool`-returning methods with `Try`

Functions like AsyncMonitor.Wait and Promise.Wait return bool, while common practice is for boolean-returning methods to be prefixed with Try. This was done to closely mimic the existing BCL methods, but I think it makes more sense to update to more sensible method names.

The existing methods don't need to be removed, they can just be deprecated, with suggestions to use the new Try methods.

Reporting progress from an `async Promise` function

Currently, it is impossible to report progress from an async Promise function. The conventional way of doing so is to pass an IProgress<T> argument into the function.

My proposal is to allow reporting progress to the async Promise via a Promise.ConfigureAwait method.

public async Promise Func()
{
    await DownloadLargeThing().ConfigureAwait(0f, 0.5f);
    await DownloadSmallThing1().ConfigureAwait(0.5f, 0.75f);
    await DownloadSmallThing2().ConfigureAwait(0.75f, 1f);
}

This allows the programmer to manually normalize the progress that is reported. The ConfigureAwait takes 2 arguments: minProgress and maxProgress. When the configured awaitable is awaited, the progress is immediately set to minProgress and goes up to maxProgress as the promise reports its own progress from 0 to 1. This means the progress can go backwards if the programmer so chooses (this can actually be useful for repeating async actions like error retries).

Potential performance improvements

Currently in master, benchmark results are showing speedups when dealing with already resolved promises and slowdowns when dealing with pending promises.

[Edit] Updated table from #27 and adjusting the AsyncPending benchmark to use a simpler reusable awaiter.

Type Method N BaseIsPending Mean Code Size
AsyncPending ProtoPromise_V1 100 ? 73.930 ฮผs 4,702 B
AsyncPending ProtoPromise_V2 100 ? 107.352 ฮผs 7,553 B
AsyncResolved ProtoPromise_V1 100 ? 39.307 ฮผs 2,982 B
AsyncResolved ProtoPromise_V2 100 ? 18.702 ฮผs 2,215 B
AwaitPending ProtoPromise_V1 100 ? 278.061 ฮผs 9,178 B
AwaitPending ProtoPromise_V2 100 ? 424.685 ฮผs 16,647 B
AwaitResolved ProtoPromise_V1 100 ? 3.931 ฮผs 2,025 B
AwaitResolved ProtoPromise_V2 100 ? 13.674 ฮผs 2,845 B
ContinueWithPending ProtoPromise_V1 100 False 328.419 ฮผs 9,248 B
ContinueWithPending ProtoPromise_V2 100 False 487.998 ฮผs 18,336 B
ContinueWithPending ProtoPromise_V1 100 True 336.582 ฮผs 9,248 B
ContinueWithPending ProtoPromise_V2 100 True 484.569 ฮผs 14,874 B
ContinueWithFromValue ProtoPromise_V1 100 False 77.698 ฮผs 4,059 B
ContinueWithFromValue ProtoPromise_V2 100 False 24.998 ฮผs 7,238 B
ContinueWithFromValue ProtoPromise_V1 100 True 73.843 ฮผs 4,391 B
ContinueWithFromValue ProtoPromise_V2 100 True 128.037 ฮผs 9,317 B
ContinueWithResolved ProtoPromise_V1 100 False 106.440 ฮผs 4,017 B
ContinueWithResolved ProtoPromise_V2 100 False 32.014 ฮผs 9,193 B
ContinueWithResolved ProtoPromise_V1 100 True 100.274 ฮผs 4,522 B
ContinueWithResolved ProtoPromise_V2 100 True 137.503 ฮผs 8,947 B

This is expected due to changing promises to structs and adding thread safety. But there are areas where we can improve the performance for pending promises.

  • Interface method calls are twice as expensive as class virtual method calls. Some internal interfaces can be changed to abstract classes, such as ITreeHandleable, IValueContainer, and IMultiTreeHandleable.
  • Multiple virtual calls are made that can be reduced to a single virtual call [Edit] I tried these in #38 and it wound up being a wash with larger code, so I opted not to merge it.
    • When hooking up a callback (Promise.Then), MarkAwaited is a virtual call, then the new PromiseRef is created, then HookupNewPromise is a second virtual call. This was done for validation purposes, but it can be reduced to a single virtual call, with careful consideration for the PromiseRef created when the call is invalid. Also note that we cannot use a generic creator passed into the virtual function, because AOT compilers have trouble with generic virtual functions and structs (and using a non-struct would still be a second virtual call and consume heap space).
    • When ITreeHandleable.Handle is invoked, PromiseRef calls a separate virtual Execute function. This is done so that all calls to Execute are wrapped in the try/catch, but that logic can be shifted to pass the IDelegate structs to a generic function with the try/catch in it instead, similar to the CallbackHelper for synchronous invokes.
  • When promises are completed, ValueContainers are created with 0 retains and retain is called when it is passed to ResolveInternal or RejectOrCancelInternal. This can be changed to always create with 1 retain and don't call retain in those methods.
  • PromiseRef.MaybeDispose() will always call Dispose() virtually. Most of the time MaybeDispose() is called from the most derived class, so that can be changed to a direct call instead of virtual. [Edit] No longer true since #55
  • async/await can be improved by hooking up the AsyncPromiseRef directly to the awaited PromiseRef instead of creating an AwaiterRef, by doing a special type check on the awaiter for PromiseAwaiter in the PromiseMethodBuilder. This optimization is trickier than the others and will need to make sure it doesn't box (very similar to #10).
  • It is possible to change object pooling to use an Interlocked method instead of Spinlock, but to be efficient it would require using C#7's ref local and ref return features. This can be done while still supporting older language versions, but it will get ugly with the #ifdefs.
  • Call IPromiseWaiter.AwaitOnCompletedInternal directly in .Net Core instead of going through AwaitOverrider, since the JIT optimizes away the box instructions (but not in Unity).
  • Unsafe.As<T>(object) in .Net 5+

Unfortunately, I don't think there is much that can be done for awaiting a resolved promise, as v1 was branchless (the promise was its own awaiter and didn't have to check itself for null). V2 is more complex with the struct wrapping a reference and using a separate PromiseAwaiter struct for safety. [Edit] Await promise was improved in #39.

`PromiseYielder.WaitOneFrame()` waits an extra frame.

This test fails. It appears to be due to the optimization I added that continues to use the already-running coroutine rather than starting a new coroutine. I'm not sure why it happens, but the first call waits 1 frame, and successive (recursive) calls wait 2 frames.

[UnityTest]
public IEnumerator PromiseYielderWaitOneFrame_WaitsOneFrameMultiple(
    [Values] RunnerType runnerType)
{
    int currentFrame = Time.frameCount;

    var promise = PromiseYielder.WaitOneFrame(GetRunner(runnerType))
        .Then(() =>
        {
            Assert.AreEqual(currentFrame + 1, Time.frameCount);
            return PromiseYielder.WaitOneFrame(GetRunner(runnerType));
        })
        .Then(() =>
        {
            Assert.AreEqual(currentFrame + 2, Time.frameCount);
            return PromiseYielder.WaitOneFrame(GetRunner(runnerType));
        })
        .Then(() =>
        {
            Assert.AreEqual(currentFrame + 3, Time.frameCount);
            return PromiseYielder.WaitOneFrame(GetRunner(runnerType));
        })
        .Then(() =>
        {
            Assert.AreEqual(currentFrame + 4, Time.frameCount);
        });
    using (var yieldInstruction = promise.ToYieldInstruction())
    {
        yield return yieldInstruction;
        if (yieldInstruction.State == Promise.State.Rejected)
            yieldInstruction.GetResult();
    }
}

Unfortunately, it seems the only way to fix it is to remove the optimization and always start a new coroutine. At least I am working on more optimized Unity awaits (#196) to offset the performance regression.

Add async void method builder override

C# 10 added async method builder overrides. We can add a new method builder to be able to override async void functions to use the Promise system instead of the Task system.

[AsyncMethodBuilder(typeof(PromiseVoidMethodBuilder))]
public async void FuncAsync()
{
    await ...
}

`TryWait` with an already canceled token does not release the lock.

If the token is already canceled, the lock should be released, then re-enter the lock queue, in case another part of code is already waiting to acquire the lock, in which case the wait should receive the signal and return true. The behavior should match Monitor.Wait(obj, 0); This test fails:

[Test]
public void AsyncConditionVariable_TryWaitAsync_AlreadyCanceled_AnotherLockWaiting_ReturnsTrue()
{
    var mutex = new AsyncLock();
    Promise.Run(async () =>
    {
        Promise notifyPromise;
        using (var key = await mutex.LockAsync())
        {
            notifyPromise = Promise.Run(async () =>
            {
                using (var key2 = await mutex.LockAsync())
                {
                    AsyncMonitor.Pulse(key2);
                }
            }, SynchronizationOption.Synchronous);

            var success = await AsyncMonitor.TryWaitAsync(key, CancelationToken.Canceled());
            Assert.True(success);
        }
        return notifyPromise;
    }, SynchronizationOption.Synchronous)
        .WaitWithTimeoutWhileExecutingForegroundContext(TimeSpan.FromSeconds(1));
}

Support `AsyncLocal<T>`

.Net Core hugely optimized ExecutionContext so capturing and executing a callback through it are now allocation-free and have almost no overhead if it is not written to. We can take advantage of this optimization to add AsyncLocal<T> support without hampering performance when it is not used.

A linked `CancelationSource` is not canceled from `CancellationTokenSource`

This test fails (.Net 6+):

[Test]
public void LinkedCancelationSourceFromToCancelationToken_RegisterCallbackIsInvoked_WhenSourceIsResetThenCanceled()
{
    var cancelationSource = new CancellationTokenSource();
    var linkedCancelationSource = CancelationSource.New(cancelationSource.Token.ToCancelationToken());
    var token = linkedCancelationSource .Token;
    int canceledCount = 0;

    token.Register(() => ++canceledCount);

    Assert.AreEqual(0, canceledCount);

    cancelationSource.TryReset();

    token.Register(() => ++canceledCount);
    Assert.AreEqual(0, canceledCount);

    cancelationSource.Cancel();
    Assert.AreEqual(1, canceledCount);

    token.Register(() => ++canceledCount);
    Assert.AreEqual(2, canceledCount);

    cancelationSource.Dispose();
    linkedCancelationSource .Dispose();
}

Add `CancelationToken.GetRetainer()` API

Currently, CancelationToken.TryRetain() and CancelationToken.Release() combination is kind of clunky. Example:

public IEnumerator FuncEnumerator(CancelationToken token)
{
    bool retained = token.TryRetain();
    try
    {
        while (!token.IsCancelationRequested)
        {
            Console.Log("Doing something");
            if (DoSomething())
            {
                yield break;
            }
            yield return null;
        }
        Console.Log("token was canceled");
    }
    finally
    {
        if (retained)
        {
            token.Release();
        }
    }
}

If we add a GetRetention() API, it could simplify a lot of that boilerplate code to a using statement:

public IEnumerator FuncEnumerator(CancelationToken token)
{
    using (token.GetRetention())
    {
        while (!token.IsCancelationRequested)
        {
            Console.Log("Doing something");
            if (DoSomething())
            {
                yield break;
            }
            yield return null;
        }
        Console.Log("token was canceled");
    }
}

GetRetention() would return a simple struct that implements IDisposable to wrap the exact same behavior.

Add `PromiseEnumerable`

C# 8 added IAsyncEnumerable to await and yield in the same function.

static async IAsyncEnumerable<int> RangeAsync(int start, int count)
{
    for (int i = 0; i < count; i++)
    {
        await Task.Delay(i);
        yield return start + i;
    }
}

And then it's consumed like this in an async function:

await foreach (int i in RangeAsync(0, 100))
{
    // ...
}

There is a downside to this, though: it causes an allocation every time the function is called, even if it completes synchronously, and another allocation when it is iterated. We can solve that issue by implementing our own custom PromiseEnumerable type that implements IAsyncEnumerable. It will be able to be consumed by the same await foreach syntax.
Unfortunately, C# has not added a custom AsyncEnumerableMethodBuilder like it did with AsyncMethodBuilder, so we can't do async PromiseEnumerable<int> like we can do async Promise<int> (dotnet/csharplang#3629). So, instead, we'll have to create a static factory API to run the async function and yield:

static PromiseEnumerable<int> RangeAsync(int start, int count)
{
    return PromiseEnumerable<int>.Create(async (writer, cancelationToken) =>
    {
        for (int i = 0; i < count && !cancelationToken.IsCancelationRequested; i++)
        {
            await Task.Yield();
            await writer.YieldAsync(start + 1); // instead of yield return start + i;
        }
    });
}

Along with overloads to accept capture values and CancelationToken.

This should be able to be allocation-free if it actually runs to completion synchronously, or we can use object pooling to prevent allocating every time (same as we do with regular promises).

IndexOutOfRangeException on iOS?

The same code that runs without messages or issues on macOS desktop gets a reproducible exception when running under iOS:

IndexOutOfRangeException: Index was outside the bounds of the array.
  at Proto.Promises.InternalHelper+InstructionProcessorGroup+InstructionProcessor`1[TYieldInstruction].WaitFor (TYieldInstruction& instruction) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Runtime.CompilerServices.AsyncVoidMethodBuilder.AwaitUnsafeOnCompleted[TAwaiter,TStateMachine] (TAwaiter& awaiter, TStateMachine& stateMachine) [0x00000] in <00000000000000000000000000000000>:0 
  at Arcspace.AssetService.loadTextureUsingWebRequest[T] (Arcspace.AssetReq`1[T] texReq) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Runtime.CompilerServices.AsyncVoidMethodBuilder.Start[TStateMachine] (TStateMachine& stateMachine) [0x00000] in <00000000000000000000000000000000>:0 
  at Arcspace.AssetService.loadTextureUsingWebRequest[T] (Arcspace.AssetReq`1[T] texReq) [0x00000] in <00000000000000000000000000000000>:0 
  at Arcspace.AssetService.TryResolveAsset[T] (Arcspace.AssetReq`1[T] req) [0x00000] in <00000000000000000000000000000000>:0 
  at Arcspace.CellScope.TryResolveAsset[T] (Arcspace.AssetReq`1[T] req) [0x00000] in <00000000000000000000000000000000>:0 

I'm using v2.5.3 cloned from main a few days ago and have no reason this issue is new. Willing to test earlier versions for regression on request.

Perhaps there is an exception happening from my side that is propagating to Proto.Promises?

For reference, here is loadTextureUsingWebRequest():

        
        static async void loadTextureUsingWebRequest<T>(AssetReq<T> texReq) where T : UnityEngine.Object {
            var url = texReq.AssetRef.URL;
            using (var req = UnityWebRequestTexture.GetTexture(url, false)) {
                await PromiseYielder.WaitForAsyncOperation(req.SendWebRequest());
                
                if (req.result == UnityWebRequest.Result.Success) {
                    texReq.Resolve(((DownloadHandlerTexture) req.downloadHandler).texture);
                } else {
                    texReq.Fail(req.error);
                }
            }
        }

Improvements with breaking changes

Since v2.0 already has a sizable list of breaking changes, it makes sense to add some more in there that improve the library for v2.0, rather than leaving them as-is or changing them later (requiring another major version change).

PromiseYielder's public static Promise<TYieldInstruction> WaitFor<TYieldInstruction>(TYieldInstruction yieldInstruction) can be changed to public static Promise WaitFor(object yieldInstruction, MonoBehaviour runner = null). Because capture values were added a while ago, there is no need to yield the same instruction in the Promises's result, making it more efficient. Async functions also implicitly capture all variables across await boundaries. runner is an extra optional argument that the caller can provide to run the coroutine on, otherwise it defaults to the static runner.

Undocumented features (in the README) that can be removed or made internal:

  • Everything in Proto.Utils namespace. Most of the namespace's members are unsafe for general use, and none of them thematically belong in this library. Some of the types have already started to change for #8.

  • Promise(<T>).ResultType. Since Promise and Promise<T> are now structs with no inheritance hierarchy, and Promise cannot be casted to Promise<T>, ResultType no longer has any value.

CatchCancelation's behavior can be changed to allow continuations (much like the regular Catch) by returning a value or Promise. ObserveCancelation can be added to keep the existing CatchCancelation behavior. This is a more logical and less confusing naming scheme.

  • PromiseYielder
  • Proto.Utils
  • ResultType
  • CatchCancelation
  • Cancelation Reason

Implement `IDisposable` and `IAsyncDisposable` on `CancelationRegistration`

Dispose unregisters the callback if it can, or if it can't, it will wait until the callback has completed invoking, unless it's called from the callback itself (to prevent deadlocks).
DisposeAsync does the same thing, but will wait asynchronously.

This will bring it to feature parity with System.Threading.CancellationTokenRegistration and simplify implementing cancelations, worrying less about race conditions, in some cases.

Cancel async Promise function while waiting for promise

Currently, this is the way to cancel an async Promise function if the awaited promise does not have a mechanism for being canceled via the provided token (UncancelablePromise() doesn't accept a cancelation token).

public async Promise Func(CancelationToken token)
{
    var val = await UncancelablePromise();
    token.ThrowIfCancelationRequested();
    // do work
}

This works most of the time, but is unreliable if the CancelationSource is disposed before the awaited promise completes. That problem can be solved by retaining the token, but that adds a bunch of extra boilerplate code:

public async Promise Func(CancelationToken token)
{
    token.Retain();
    try
    {
        var val = await UncancelablePromise();
        token.ThrowIfCancelationRequested();
        // do work
    }
    finally
    {
        token.Release();
    }
}

Another issue with this approach is the cancelation will not propagate when the token is canceled, but rather it will wait until the awaited promise completes. This can also be solved via another method, but is unintuitive:

public async Promise Func(CancelationToken token)
{
    var val = await UncancelablePromise().Then(v => v, token); // Immediately cancel when the token is canceled.
    // do work
}

This approach mixes async/await style with Then style, which is undesirable and potentially confusing to anyone looking at the code. It also is less efficient than it could be, because it's adding a new callback simply to act as a pass-through.

My proposal is to add a more intuitive method of canceling the awaited promise via a ConfigureAwait method on the promise:

public async Promise Func(CancelationToken token)
{
    var val = await UncancelablePromise().ConfigureAwait(token); // Immediately cancel when the token is canceled.
    // do work
}

Checking `CancelationToken.IsCancelationRequested` returns wrong value

In .Net 6+, when a System.Threading.CancellationTokenSource is reset, then cancelled, the converted Proto.Promises.CancelationToken is not canceled when checking its cancelation status via IsCancelationRequested. This test fails:

[Test]
public void ToCancelationTokenIsCanceledWhenSourceIsResetThenCanceled()
{
    var cancelationSource = new CancellationTokenSource();
    var token = cancelationSource.Token.ToCancelationToken();

    Assert.IsFalse(token.IsCancelationRequested);

    cancelationSource.TryReset();
    Assert.IsFalse(token.IsCancelationRequested);

    cancelationSource.Cancel();
    Assert.IsTrue(token.IsCancelationRequested);

    cancelationSource.Dispose();
}

Issue with WaitForResult

Hi Tim,

I've got an issue when using the WaitForResult when using it with nested promises.
I'm still investigating the issue, but I've got a promise that does multiple Http calls (internally I'm using deferred promises to get the result and state of these calls), for some reason (with the last update of your package) after the first promise gets resolved everything stops working (block the entire process). So the 'then' call of the associated first deferred promise doesn't fire anymore - stalling the process.

Again, I switched to using await for now (skipping the WaitForResult) which seems to resolve the issue. So somehow the 'WaitForResult' is bugged. And it must be related to having multiple promises during that call because I don't have this issue with a single-level promise. I know the explanation is a bit fuzzy, but just wanted to let you know maybe you are aware of it.

Kind Regards

Add additional async coordination primitives

AsyncLock (#159) is being added in v2.4.0. We can add additional async coordination primitives to sit alongside it, such as AsyncAutoResetEvent, AsyncManualResetEvent, AsyncSemaphore, and AsyncReaderWriterLock.

  • AsyncReaderWriterLock
  • AsyncManualResetEvent
  • AsyncAutoResetEvent
  • AsyncSemaphore
  • AsyncCountdownEvent
  • AsyncConditionVariable

Test failures in IL2CPP

I ran tests on my local machine with the IL2CPP backend and got some test failures:

PromiseWait_DoesNotReturnUntilOperationIsComplete(False,True) (0.568s)
---
Proto.Promises.InvalidOperationException : PromiseAwaiter.GetResult() is only valid when the promise is completed.
---
at Proto.Promises.Internal+PromiseRefBase.GetExceptionDispatchInfo (Proto.Promises.Promise+State state, System.Int16 promiseId) [0x00000] in <00000000000000000000000000000000>:0 
  at Proto.Promises.Promise.SwitchToForeground (System.Boolean forceAsync) [0x00000] in <00000000000000000000000000000000>:0 
  at ProtoPromiseTests.APIs.MiscellaneousTests.PromiseWait_DoesNotReturnUntilOperationIsComplete (System.Boolean alreadyComplete, System.Boolean withTimeout) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Reflection.MonoMethod.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Reflection.MethodBase.Invoke (System.Object obj, System.Object[] parameters) [0x00000] in <00000000000000000000000000000000>:0 
  at NUnit.Framework.Internal.Reflect.InvokeMethod (System.Reflection.MethodInfo method, System.Object fixture, System.Object[] args) [0x00000] in <00000000000000000000000000000000>:0 
  at NUnit.Framework.Internal.Commands.TestMethodCommand.Execute (NUnit.Framework.Internal.ITestExecutionContext context) [0x00000] in <00000000000000000000000000000000>:0 
  at NUnit.Framework.Internal.Commands.TestActionCommand.Execute (NUnit.Framework.Internal.ITestExecutionContext context) [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.UnityLogCheckDelegatingCommand.Execute (NUnit.Framework.Internal.ITestExecutionContext context) [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.UnityLogCheckDelegatingCommand+<ExecuteEnumerable>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.DefaultTestWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestTools.TestRunner.PlaymodeTestsController+<TestRunnerCorotine>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) [0x00000] in <00000000000000000000000000000000>:0
PromiseWait_DoesNotReturnUntilOperationIsComplete_AndThrowsCorrectException(Reject,False,False) (0.619s)
---
TearDown : NUnit.Framework.AssertionException :   Expected: <Proto.Promises.InvalidOperationException: Test
  at ProtoPromiseTests.APIs.MiscellaneousTests+<>c__DisplayClass30_0.<PromiseWait_DoesNotReturnUntilOperationIsComplete_AndThrowsCorrectException>b__0 () [0x00000] in <00000000000000000000000000000000>:0 
  at System.Func`1[TResult].Invoke () [0x00000] in <00000000000000000000000000000000>:0 
  at Proto.Promises.Internal+DeferredPromiseHelper.TryReportProgress (Proto.Promises.Internal+IDeferredPromise _this, System.Int32 deferredId, System.Single progress) [0x00000] in <00000000000000000000000000000000>:0 
  at Proto.Promises.Internal+PromiseRefBase+PromiseResolvePromise`2[TResult,TResolver].Execute (Proto.Promises.Internal+PromiseRefBase handler, System.Boolean& invokingRejected, System.Boolean& handlerDisposedAfterCallback) [0x00000] in <00000000000000000000000000000000>:0 
  at Proto.Promises.Internal+PromiseRefBase+PromiseSingleAwait`1[TResult].Handle (Proto.Promises.Internal+PromiseRefBase handler) [0x00000] in <00000000000000000000000000000000>:0 
  at Proto.Promises.Internal+PromiseRefBase+PromiseConfigured`1[TResult].HandleFromContext () [0x00000] in <00000000000000000000000000000000>:0 
  at Proto.Promises.Internal.HandleFromContext (System.Object state) [0x00000] in <00000000000000000000000000000000>:0 
  at ProtoPromiseTests.BackgroundSynchronizationContext+ThreadRunner.ThreadAction () [0x00000] in <00000000000000000000000000000000>:0 
  at NUnit.Framework.TestDelegate.Invoke () [0x00000] in <00000000000000000000000000000000>:0 
  at System.Threading.ExecutionContext.RunInternal (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Threading.ExecutionContext.Run (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state) [0x00000] in <00000000000000000000000000000000>:0 
  at NUnit.Framework.TestDelegate.Invoke () [0x00000] in <00000000000000000000000000000000>:0 
--- End of stack trace from previous location where exception was thrown ---
  at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () [0x00000] in <00000000000000000000000000000000>:0 
  at ProtoPromiseTests.APIs.MiscellaneousTests.PromiseWait_DoesNotReturnUntilOperationIsComplete_AndThrowsCorrectException (ProtoPromiseTests.CompleteType throwType, System.Boolean alreadyComplete, System.Boolean withTimeout) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Reflection.MonoMethod.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Reflection.MethodBase.Invoke (System.Object obj, System.Object[] parameters) [0x00000] in <00000000000000000000000000000000>:0 
  at NUnit.Framework.Internal.Reflect.InvokeMethod (System.Reflection.MethodInfo method, System.Object fixture, System.Object[] args) [0x00000] in <00000000000000000000000000000000>:0 
  at NUnit.Framework.Internal.Commands.TestMethodCommand.Execute (NUnit.Framework.Internal.ITestExecutionContext context) [0x00000] in <00000000000000000000000000000000>:0 
  at NUnit.Framework.Internal.Commands.TestActionCommand.Execute (NUnit.Framework.Internal.ITestExecutionContext context) [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.UnityLogCheckDelegatingCommand.Execute (NUnit.Framework.Internal.ITestExecutionContext context) [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.UnityLogCheckDelegatingCommand+<ExecuteEnumerable>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.DefaultTestWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestTools.TestRunner.PlaymodeTestsController+<TestRunnerCorotine>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) [0x00000] in <00000000000000000000000000000000>:0 >
  But was:  <Proto.Promises.InvalidOperationException: Test
  at ProtoPromiseTests.APIs.MiscellaneousTests+<>c__DisplayClass30_0.<PromiseWait_DoesNotReturnUntilOperationIsComplete_AndThrowsCorrectException>b__0 () [0x00000] in <00000000000000000000000000000000>:0 
  at System.Func`1[TResult].Invoke () [0x00000] in <00000000000000000000000000000000>:0 
  at Proto.Promises.Internal+DeferredPromiseHelper.TryReportProgress (Proto.Promises.Internal+IDeferredPromise _this, System.Int32 deferredId, System.Single progress) [0x00000] in <00000000000000000000000000000000>:0 
  at Proto.Promises.Internal+PromiseRefBase+PromiseResolvePromise`2[TResult,TResolver].Execute (Proto.Promises.Internal+PromiseRefBase handler, System.Boolean& invokingRejected, System.Boolean& handlerDisposedAfterCallback) [0x00000] in <00000000000000000000000000000000>:0 
  at Proto.Promises.Internal+PromiseRefBase+PromiseSingleAwait`1[TResult].Handle (Proto.Promises.Internal+PromiseRefBase handler) [0x00000] in <00000000000000000000000000000000>:0 
  at Proto.Promises.Internal+PromiseRefBase+PromiseConfigured`1[TResult].HandleFromContext () [0x00000] in <00000000000000000000000000000000>:0 
  at Proto.Promises.Internal.HandleFromContext (System.Object state) [0x00000] in <00000000000000000000000000000000>:0 
  at ProtoPromiseTests.BackgroundSynchronizationContext+ThreadRunner.ThreadAction () [0x00000] in <00000000000000000000000000000000>:0 
  at NUnit.Framework.TestDelegate.Invoke () [0x00000] in <00000000000000000000000000000000>:0 
  at System.Threading.ExecutionContext.RunInternal (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Threading.ExecutionContext.Run (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state) [0x00000] in <00000000000000000000000000000000>:0 
  at NUnit.Framework.TestDelegate.Invoke () [0x00000] in <00000000000000000000000000000000>:0 >
---
--TearDown
  at NUnit.Framework.Assert.That[TActual] (TActual actual, NUnit.Framework.Constraints.IResolveConstraint expression, System.String message, System.Object[] args) [0x00000] in <00000000000000000000000000000000>:0 
  at NUnit.Framework.Assert.AreEqual (System.Object expected, System.Object actual) [0x00000] in <00000000000000000000000000000000>:0 
  at ProtoPromiseTests.TestHelper+<>c.<Setup>b__6_0 (Proto.Promises.UnhandledException e) [0x00000] in <00000000000000000000000000000000>:0 
  at Proto.Promises.Internal+PromiseRefBase.Finalize () [0x00000] in <00000000000000000000000000000000>:0
PromiseWaitForResult_DoesNotReturnUntilOperationIsComplete_AndReturnsWithCorrectResult(False,True) (0.566s)
---
Proto.Promises.InvalidOperationException : PromiseAwaiter.GetResult() is only valid when the promise is completed.
---
at Proto.Promises.Internal+PromiseRefBase.GetExceptionDispatchInfo (Proto.Promises.Promise+State state, System.Int16 promiseId) [0x00000] in <00000000000000000000000000000000>:0 
  at Proto.Promises.Promise`1[T].Resolved (T value) [0x00000] in <00000000000000000000000000000000>:0 
  at ProtoPromiseTests.APIs.MiscellaneousTests.Wait[T] (Proto.Promises.Promise`1[T] promise, T& result, System.Boolean withTimeout) [0x00000] in <00000000000000000000000000000000>:0 
  at ProtoPromiseTests.APIs.MiscellaneousTests.PromiseWaitForResult_DoesNotReturnUntilOperationIsComplete_AndReturnsWithCorrectResult (System.Boolean alreadyComplete, System.Boolean withTimeout) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Reflection.MonoMethod.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Reflection.MethodBase.Invoke (System.Object obj, System.Object[] parameters) [0x00000] in <00000000000000000000000000000000>:0 
  at NUnit.Framework.Internal.Reflect.InvokeMethod (System.Reflection.MethodInfo method, System.Object fixture, System.Object[] args) [0x00000] in <00000000000000000000000000000000>:0 
  at NUnit.Framework.Internal.Commands.TestMethodCommand.Execute (NUnit.Framework.Internal.ITestExecutionContext context) [0x00000] in <00000000000000000000000000000000>:0 
  at NUnit.Framework.Internal.Commands.TestActionCommand.Execute (NUnit.Framework.Internal.ITestExecutionContext context) [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.UnityLogCheckDelegatingCommand.Execute (NUnit.Framework.Internal.ITestExecutionContext context) [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.UnityLogCheckDelegatingCommand+<ExecuteEnumerable>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.DefaultTestWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestTools.TestRunner.PlaymodeTestsController+<TestRunnerCorotine>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) [0x00000] in <00000000000000000000000000000000>:0
PromiseWaitForResult_DoesNotReturnUntilOperationIsComplete_AndThrowsCorrectException(Reject,False,Fa (0.574s)
---
TearDown : NUnit.Framework.AssertionException :   Expected: <Proto.Promises.InvalidOperationException: Test
  at ProtoPromiseTests.APIs.MiscellaneousTests+<>c__DisplayClass33_0.<PromiseWaitForResult_DoesNotReturnUntilOperationIsComplete_AndThrowsCorrectException>b__0 () [0x00000] in <00000000000000000000000000000000>:0 
  at System.Func`1[TResult].Invoke () [0x00000] in <00000000000000000000000000000000>:0 
  at Proto.Promises.Internal+PromiseRefBase+PromiseResolvePromise`2[TResult,TResolver].Execute (Proto.Promises.Internal+PromiseRefBase handler, System.Boolean& invokingRejected, System.Boolean& handlerDisposedAfterCallback) [0x00000] in <00000000000000000000000000000000>:0 
  at Proto.Promises.Internal+PromiseRefBase+PromiseSingleAwait`1[TResult].Handle (Proto.Promises.Internal+PromiseRefBase handler) [0x00000] in <00000000000000000000000000000000>:0 
  at Proto.Promises.Internal+PromiseRefBase+PromiseConfigured`1[TResult].HandleFromContext () [0x00000] in <00000000000000000000000000000000>:0 
  at Proto.Promises.Internal.HandleFromContext (System.Object state) [0x00000] in <00000000000000000000000000000000>:0 
  at ProtoPromiseTests.BackgroundSynchronizationContext+ThreadRunner.ThreadAction () [0x00000] in <00000000000000000000000000000000>:0 
  at NUnit.Framework.TestDelegate.Invoke () [0x00000] in <00000000000000000000000000000000>:0 
  at System.Threading.ExecutionContext.RunInternal (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Threading.ExecutionContext.Run (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state) [0x00000] in <00000000000000000000000000000000>:0 
  at NUnit.Framework.TestDelegate.Invoke () [0x00000] in <00000000000000000000000000000000>:0 
--- End of stack trace from previous location where exception was thrown ---
  at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () [0x00000] in <00000000000000000000000000000000>:0 
  at Proto.Promises.Promise`1[T].Resolved (T value) [0x00000] in <00000000000000000000000000000000>:0 
  at ProtoPromiseTests.APIs.MiscellaneousTests.Wait[T] (Proto.Promises.Promise`1[T] promise, T& result, System.Boolean withTimeout) [0x00000] in <00000000000000000000000000000000>:0 
  at ProtoPromiseTests.APIs.MiscellaneousTests.PromiseWaitForResult_DoesNotReturnUntilOperationIsComplete_AndThrowsCorrectException (ProtoPromiseTests.CompleteType throwType, System.Boolean alreadyComplete, System.Boolean withTimeout) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Reflection.MonoMethod.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Reflection.MethodBase.Invoke (System.Object obj, System.Object[] parameters) [0x00000] in <00000000000000000000000000000000>:0 
  at NUnit.Framework.Internal.Reflect.InvokeMethod (System.Reflection.MethodInfo method, System.Object fixture, System.Object[] args) [0x00000] in <00000000000000000000000000000000>:0 
  at NUnit.Framework.Internal.Commands.TestMethodCommand.Execute (NUnit.Framework.Internal.ITestExecutionContext context) [0x00000] in <00000000000000000000000000000000>:0 
  at NUnit.Framework.Internal.Commands.TestActionCommand.Execute (NUnit.Framework.Internal.ITestExecutionContext context) [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.UnityLogCheckDelegatingCommand.Execute (NUnit.Framework.Internal.ITestExecutionContext context) [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.UnityLogCheckDelegatingCommand+<ExecuteEnumerable>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.DefaultTestWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.TestTools.TestRunner.PlaymodeTestsController+<TestRunnerCorotine>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) [0x00000] in <00000000000000000000000000000000>:0 >
  But was:  <Proto.Promises.InvalidOperationException: Test
  at ProtoPromiseTests.APIs.MiscellaneousTests+<>c__DisplayClass33_0.<PromiseWaitForResult_DoesNotReturnUntilOperationIsComplete_AndThrowsCorrectException>b__0 () [0x00000] in <00000000000000000000000000000000>:0 
  at System.Func`1[TResult].Invoke () [0x00000] in <00000000000000000000000000000000>:0 
  at Proto.Promises.Internal+PromiseRefBase+PromiseResolvePromise`2[TResult,TResolver].Execute (Proto.Promises.Internal+PromiseRefBase handler, System.Boolean& invokingRejected, System.Boolean& handlerDisposedAfterCallback) [0x00000] in <00000000000000000000000000000000>:0 
  at Proto.Promises.Internal+PromiseRefBase+PromiseSingleAwait`1[TResult].Handle (Proto.Promises.Internal+PromiseRefBase handler) [0x00000] in <00000000000000000000000000000000>:0 
  at Proto.Promises.Internal+PromiseRefBase+PromiseConfigured`1[TResult].HandleFromContext () [0x00000] in <00000000000000000000000000000000>:0 
  at Proto.Promises.Internal.HandleFromContext (System.Object state) [0x00000] in <00000000000000000000000000000000>:0 
  at ProtoPromiseTests.BackgroundSynchronizationContext+ThreadRunner.ThreadAction () [0x00000] in <00000000000000000000000000000000>:0 
  at NUnit.Framework.TestDelegate.Invoke () [0x00000] in <00000000000000000000000000000000>:0 
  at System.Threading.ExecutionContext.RunInternal (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Threading.ExecutionContext.Run (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state) [0x00000] in <00000000000000000000000000000000>:0 
  at NUnit.Framework.TestDelegate.Invoke () [0x00000] in <00000000000000000000000000000000>:0 >
---
--TearDown
  at NUnit.Framework.Assert.That[TActual] (TActual actual, NUnit.Framework.Constraints.IResolveConstraint expression, System.String message, System.Object[] args) [0x00000] in <00000000000000000000000000000000>:0 
  at NUnit.Framework.Assert.AreEqual (System.Object expected, System.Object actual) [0x00000] in <00000000000000000000000000000000>:0 
  at ProtoPromiseTests.TestHelper+<>c.<Setup>b__6_0 (Proto.Promises.UnhandledException e) [0x00000] in <00000000000000000000000000000000>:0 
  at Proto.Promises.Internal+PromiseRefBase.Finalize () [0x00000] in <00000000000000000000000000000000>:0

IL2CPP crash

Running IL2CPP tests locally (since it still can't be done in CI...) results in the player crashing. I'm looking into it now, but it's a slow process. I'm opening this issue to document and make sure it's fixed before releasing v2.4.0.

Concurrency failure in CI

https://github.com/timcassell/ProtoPromise/actions/runs/3522270696/jobs/5906008504

Failure in PromiseReturnedInCallbackMayBeCompletedConcurrently_T.

at ProtoPromiseTests.Threading.PromiseConcurrencyTests+<>c__DisplayClass16_0.<PromiseReturnedInCallbackMayBeCompletedConcurrently_T>b__4 () [0x0000d] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Tests/Threading/PromiseConcurrencyTests.cs:686
at (wrapper delegate-invoke) <Module>.invoke_void()
at ProtoPromiseTests.Threading.ThreadHelper.ExecuteParallelActionsWithOffsetsAndSetup (System.Action setup, System.Action`1[System.Action][] parallelActionsSetup, System.Action[] parallelActions, System.Action teardown) [0x000da] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/ThreadHelper.cs:313
at ProtoPromiseTests.Threading.PromiseConcurrencyTests.PromiseReturnedInCallbackMayBeCompletedConcurrently_T (ProtoPromiseTests.CompleteType completeType) [0x00095] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Tests/Threading/PromiseConcurrencyTests.cs:661

--TearDown

--InvalidCastException
  at (wrapper castclass) System.Object.__castclass_with_cache(object,intptr,intptr)
  at Proto.Promises.Internal.UnsafeAs[T] (System.Object o) [0x00001] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromise/Core/InternalShared/HelperFunctionsInternal.cs:227 
  at Proto.Promises.Promise`1+ResultContainer[T]..ctor (Proto.Promises.Internal+PromiseRefBase source) [0x00000] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromise/Core/Promises/ResultContainers.cs:254 
  at Proto.Promises.Internal+PromiseRefBase+DelegateContinueArgResult`2[TArg,TResult].Invoke (Proto.Promises.Internal+PromiseRefBase handler, Proto.Promises.Internal+PromiseRefBase owner) [0x00001] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromise/Core/Promises/Internal/DelegateWrappersInternal.cs:984 
  at Proto.Promises.Internal+PromiseRefBase+PromiseContinue`2[TResult,TContinuer].Execute (Proto.Promises.Internal+PromiseRefBase handler, Proto.Promises.Promise+State state, System.Boolean& invokingRejected) [0x0001c] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromise/Core/Promises/Internal/PromiseInternal.cs:1436 
  at Proto.Promises.Internal+PromiseRefBase+PromiseSingleAwait`1[TResult].Handle (Proto.Promises.Internal+PromiseRefBase handler, System.Object rejectContainer, Proto.Promises.Promise+State state) [0x0000d] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromise/Core/Promises/Internal/PromiseInternal.cs:601 

This looks like a case of out-of-order execution. The fix should be easy enough. We could add a Thread.MemoryBarrier() to prevent the re-ordering of instructions, but I think a cleaner approach is to just pass the object rejectContainer, Promise.State state directly to the new Promise<TArg>.ResultContainer instead of reading the fields from the handler. And store the rejectContainer as an object instead of IRejectContainer and cast it when it's needed instead of in the constructor. Of course, we can also add the memory barrier in just to be safe (best place is probably at the beginning of Dispose().

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.