Skip to content

Commit

Permalink
Merge pull request #489 from timcassell/develop
Browse files Browse the repository at this point in the history
Merge develop
  • Loading branch information
timcassell authored Nov 3, 2024
2 parents d9bb3a8 + f01a049 commit d716974
Show file tree
Hide file tree
Showing 162 changed files with 11,352 additions and 5,917 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/unity-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ jobs:
- unity: { major: 2019 }
buildTargetId: 1
- unity: { major: 2023 }
buildtargetid: standalone
buildTargetId: standalone

steps:
- name: Checkout
Expand Down Expand Up @@ -126,7 +126,7 @@ jobs:
projectPath: ProtoPromise_Unity
testMode: ${{ matrix.testMode.value }}
unityVersion: ${{ matrix.unity.version }}
timeout-minutes: 120
timeout-minutes: 180

- uses: dorny/test-reporter@main
if: always()
Expand Down
30 changes: 30 additions & 0 deletions Docs/Changelog/v3.2.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Change Log

## v3.2.0 - November 3, 2024

Enhancements:

- Added `Channel<T>` and related types in `Proto.Promises.Channels` namespace.
- Added `Promise(<T>).{ConfigureAwait, ConfigureContinuation}` APIs and accompanying `ContinuationOptions` type.
- Added `SynchronizationOption.CapturedContext` option.
- Added `(Ordered, Configured)AsyncEnumerable.ConfigureAwait(ContinuationOptions)` new overloads.
- Exposed `ConfiguredAsyncEnumerable<T>.{ContinuationOptions, CancelationToken}` and `ConfiguredAsyncEnumerable<T>.Enumerator.ContinuationOptions`.
- Added `ManualSynchronizationContextCore` type.
- Added `PromiseSynchronizationContext.Execute(bool exhaustive)` API.
- Added `Promise(<T>).FromException` API.
- Added option to disable context capture for async synchronization primitives.

Fixes:

- Fixed some bugs surrounding `AsyncEnumerable.Merge` enumerator disposal.
- Fixed a potential race condition with `Promise.New` API.
- Fixed some async Linq implementations that weren't configuring the awaits properly.

Deprecated:

- Deprecated `Promise(<T>).WaitAsync` APIs accepting `SynchronizationContext` and `SynchronizationOption`.

Misc:

- Changed default `Progress` invokeOption to `CapturedContext`.
- Added net8.0 build target.
7 changes: 7 additions & 0 deletions Docs/Guides/channels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Channels

A channel is an async producer/consumer data structure, similar to a blocking collection. You can use it to move data from 1 or more producers to 1 or more consumers asynchronously. Both bounded and unbounded channels are supported.

`Proto.Promises.Channels.Channel<T>` was designed very similar to `System.Threading.Channels.Channel<T>`. See the [BCL documentation](https://learn.microsoft.com/en-us/dotnet/core/extensions/channels) to see how channels may be typically used. Channels in this library work very similarly, but were also designed to not allocate. When you no longer need the channel, you can `Dispose` it to return the backing object to the pool for future re-use.

Another difference from the BCL design is, if the channeled objects need to be cleaned up, and you are working with a bounded channel, you can retrieve the dropped item and clean it up, or try to write it to the channel again. `if (channelWriteResult.TryGetDroppedItem(out var droppedItem)) { ... }`
2 changes: 1 addition & 1 deletion Docs/Guides/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ If you are in DEBUG mode, you can configure when additional stacktraces will be

`Promise.Config.UncaughtRejectionHandler` allows you to route unhandled rejections through a delegate instead of being thrown.

`Promise.Config.ForegroundContext` is the context to which foreground operations are posted, typically used to marshal work to the UI thread. This is automatically set in Unity, but in other UI frameworks it should be set at application startup (usually `Promise.Config.ForegroundContext = SynchronizationContext.Current` is enough). Note: if your application uses multiple `SynchronizationContext`s, you should instead pass the context directly to the `WaitAsync` and other APIs instead of using `SynchronizationOption.Foreground`. See [Switching Execution Context](context-switching.md).
`Promise.Config.ForegroundContext` is the context to which foreground operations are posted, typically used to marshal work to the UI thread. This is automatically set in Unity, but in other UI frameworks it should be set at application startup (usually `Promise.Config.ForegroundContext = SynchronizationContext.Current` is enough). Note: if your application uses multiple `SynchronizationContext`s, you may want to pass `ContinuationOptions.CapturedContext` to the `ConfigureAwait` and other APIs instead of `Foreground`. See [Switching Execution Context](context-switching.md).

`Promise.Config.BackgroundContext` can be set to override how background operations are executed. If this is null, `ThreadPool.QueueUserWorkItem(callback, state)` is used.

Expand Down
14 changes: 7 additions & 7 deletions Docs/Guides/context-switching.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@

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.

Promise continuations (`.Then` or `await`) normally execute synchronously, not caring what thread they are executing on. However, you can force continuations to execute either on the foreground context for UI code, or background context for expensive non-UI code. You can use the `promise.WaitAsync(SynchronizationOption)` to force the next continuation to execute on the given context (`Synchronous` (default), `Foreground`, or `Background`).
Promise continuations (`.Then` or `await`) execute synchronously by default, not caring what thread they are executing on. However, you can force continuations to execute either on the foreground context for UI code, or background context for expensive non-UI code. You can use the `promise.ConfigureAwait(ContinuationOptions)` or `promise.ConfigureContinuation(ContinuationOptions)` APIs to force the continuation behavior.

```cs
async void Func()
{
// Not sure what thread we're on here...
await DoSomethingAsync()
.WaitAsync(SynchronizationOption.Background);
.ConfigureAwait(ContinuationOptions.Background);
// Now we're executing in the background.
Console.Log("Thread is background: " + Thread.CurrentThread.IsBackground); // true
await DoSomethingElseAsync()
.WaitAsync(SynchronizationOption.Foreground);
.ConfigureAwait(ContinuationOptions.Foreground);
// Now we're executing in the foreground (UI thread).
Console.Log("Thread is background: " + Thread.CurrentThread.IsBackground); // false
}
Expand All @@ -24,14 +24,14 @@ void Func()
{
// Not sure what thread we're on here...
DoSomethingAsync()
.WaitAsync(SynchronizationOption.Background)
.ConfigureContinuation(ContinuationOptions.Background)
.Then(() =>
{
// Now we're executing in the background.
Console.Log("Thread is background: " + Thread.CurrentThread.IsBackground); // true
return DoSomethingElseAsync();
}
.WaitAsync(SynchronizationOption.Foreground)
.ConfigureContinuation(ContinuationOptions.Foreground)
.Then(() =>
{
// Now we're executing in the foreground (UI thread).
Expand All @@ -45,8 +45,8 @@ To make things a little easier, there are shortcut functions to simply hop to th

The `Foreground` option posts the continuation to `Promise.Config.ForegroundContext`. This property is set automatically in Unity, but in other UI frameworks it should be set at application startup (usually `Promise.Config.ForegroundContext = SynchronizationContext.Current` is enough).

If your application uses multiple `SynchronizationContext`s, instead of using `SynchronizationOption.Foreground`, you should pass the proper `SynchronizationContext` directly to `WaitAsync` or `Promise.SwitchToContext`.
If your application uses multiple `SynchronizationContext`s, instead of using `SynchronizationOption.Foreground`, you may want to pass the proper `SynchronizationContext` directly to `Promise.SwitchToContext`, or `ContinuationOptions.CapturedContext` to `Promise.ConfigureAwait`.

For context switching optimized for async/await, use `Promise.SwitchToForegroundAwait`, `Promise.SwitchToBackgroundAwait`, and `Promise.SwitchToContextAwait`. These functions return custom awaiters that avoid the overhead of `Promise`.

Other APIs that allow you to pass `SynchronizationOption` or `SynchronizationContext` to configure the context that the callback executes on are `Progress.New` (default `Foreground`), `Promise.New` (default `Synchronous`), and `Promise.Run` (default `Background`).
Other APIs that allow you to pass `SynchronizationOption` or `SynchronizationContext` to configure the context that the callback executes on are `Progress.New` (default `CapturedContext`), `Promise.New` (default `Synchronous`), and `Promise.Run` (default `Background`).
46 changes: 6 additions & 40 deletions Package/Core/Cancelations/Internal/CancelationInternal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ private bool TryRegister<TNodeCreator>(ref TNodeCreator nodeCreator, int tokenId
// Callback could be invoked synchronously if the token is canceled on another thread,
// so we set a flag to prevent a deadlock, then check the flag again after the hookup to see if it was invoked.
ts_isLinkingToBclToken = true;
_bclRegistration = token.Register(cancelRef =>
_bclRegistration = token.UnsafeRegister(cancelRef =>
{
// This could be invoked synchronously if the token is canceled, so we check the flag to prevent a deadlock.
if (ts_isLinkingToBclToken)
Expand All @@ -392,7 +392,7 @@ private bool TryRegister<TNodeCreator>(ref TNodeCreator nodeCreator, int tokenId
return;
}
cancelRef.UnsafeAs<CancelationRef>().Cancel();
}, this, false);
}, this);

if (!ts_isLinkingToBclToken)
{
Expand Down Expand Up @@ -658,20 +658,6 @@ private sealed class CallbackNodeImpl<TCancelable> : CancelationCallbackNode, IT

private CallbackNodeImpl() { }

#if PROMISE_DEBUG || PROTO_PROMISE_DEVELOPER_MODE
private bool _disposed;

~CallbackNodeImpl()
{
if (!_disposed)
{
// For debugging. This should never happen.
string message = $"A {GetType()} was garbage collected without it being disposed.";
ReportRejection(new UnreleasedObjectException(message), this);
}
}
#endif

[MethodImpl(InlineOption)]
private static CallbackNodeImpl<TCancelable> GetOrCreate()
{
Expand All @@ -687,10 +673,6 @@ internal static CallbackNodeImpl<TCancelable> GetOrCreate(in TCancelable cancela
var node = GetOrCreate();
node._parentId = parent._smallFields._instanceId;
node._cancelable = cancelable;
#if PROMISE_DEBUG || PROTO_PROMISE_DEVELOPER_MODE
// If the CancelationRef was attached to a BCL token, it is possible this will not be disposed, so we won't check for it.
node._disposed = parent._linkedToBclToken;
#endif
SetCreatedStacktrace(node, 2);
return node;
}
Expand Down Expand Up @@ -719,9 +701,6 @@ internal override void Dispose()
++_nodeId;
}
_cancelable = default;
#if PROMISE_DEBUG || PROTO_PROMISE_DEVELOPER_MODE
_disposed = true;
#endif
ObjectPool.MaybeRepool(this);
}
} // class CallbackNodeImpl<TCancelable>
Expand All @@ -738,20 +717,6 @@ private sealed class LinkedCancelationNode : CancelationCallbackNodeBase, ILinke

private LinkedCancelationNode() { }

#if PROMISE_DEBUG || PROTO_PROMISE_DEVELOPER_MODE
private bool _disposed;

~LinkedCancelationNode()
{
if (!_disposed)
{
// For debugging. This should never happen.
string message = "A LinkedCancelationNode was garbage collected without it being disposed.";
ReportRejection(new UnreleasedObjectException(message), _target);
}
}
#endif

[MethodImpl(InlineOption)]
private static LinkedCancelationNode GetOrCreate()
{
Expand Down Expand Up @@ -800,9 +765,6 @@ internal override void Dispose()
[MethodImpl(InlineOption)]
private void Repool()
{
#if PROMISE_DEBUG || PROTO_PROMISE_DEVELOPER_MODE
_disposed = true;
#endif
ObjectPool.MaybeRepool(this);
}

Expand Down Expand Up @@ -1268,7 +1230,11 @@ internal static CancelationRef GetOrCreateForBclTokenConvert(CancellationTokenSo
private void HookupBclCancelation(System.Threading.CancellationToken token)
{
// We don't need the synchronous invoke check when this is created.
#if NETCOREAPP3_0_OR_GREATER
var registration = token.UnsafeRegister(state => state.UnsafeAs<CancelationRef>().Cancel(), this);
#else
var registration = token.Register(state => state.UnsafeAs<CancelationRef>().Cancel(), this, false);
#endif
SetCancellationTokenRegistration(registration);
}

Expand Down
8 changes: 8 additions & 0 deletions Package/Core/Channels.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 53 additions & 0 deletions Package/Core/Channels/BoundedChannelOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#if PROTO_PROMISE_DEBUG_ENABLE || (!PROTO_PROMISE_DEBUG_DISABLE && DEBUG)
#define PROMISE_DEBUG
#else
#undef PROMISE_DEBUG
#endif

using System.Diagnostics;

namespace Proto.Promises.Channels
{
/// <summary>
/// Specifies the behavior to use when writing to a bounded channel that is already full.
/// </summary>
public enum BoundedChannelFullMode : byte
{
/// <summary>
/// Wait for space to be available in order to complete the write operation.
/// </summary>
Wait,
/// <summary>
/// Remove the newest item in the channel in order to make room for the item being written.
/// </summary>
DropNewest,
/// <summary>
/// Remove the oldest item in the channel in order to make room for the item being written.
/// </summary>
DropOldest,
/// <summary>
/// Drop the item being written.
/// </summary>
DropWrite
}

/// <summary>
/// Provides options that control the behavior of bounded <see cref="Channel{T}"/> instances.
/// </summary>
/// <typeparam name="T">Specifies the type of data that is channeled.</typeparam>
#if !PROTO_PROMISE_DEVELOPER_MODE
[DebuggerNonUserCode, StackTraceHidden]
#endif
public struct BoundedChannelOptions<T>
{
/// <summary>
/// Gets or sets the maximum number of items the bounded channel may store.
/// </summary>
public int Capacity { get; set; }

/// <summary>
/// Gets or sets the behavior incurred by write operations when the channel is full.
/// </summary>
public BoundedChannelFullMode FullMode { get; set; }
}
}
11 changes: 11 additions & 0 deletions Package/Core/Channels/BoundedChannelOptions.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit d716974

Please sign in to comment.