├── .gitignore ├── Directory.Build.props ├── Directory.Build.targets ├── LICENSE.TXT ├── PooledAwait.sln ├── PooledAwait.snk ├── docs ├── _config.yml └── index.md ├── global.json ├── nuget.config ├── readme.md ├── src └── PooledAwait │ ├── AwaitableExtensions.cs │ ├── ConfiguredYieldAwaitable.cs │ ├── FireAndForget.cs │ ├── IResettable.cs │ ├── Internal │ ├── BrowsableAttribute.cs │ ├── Counters.cs │ ├── LazyTaskStateT.cs │ ├── ManualResetValueTaskSourceCore.cs │ ├── Nothing.cs │ ├── PooledStateT.cs │ ├── StateMachineBox.cs │ ├── TaskUtils.cs │ └── ThrowHelper.cs │ ├── LazyTaskCompletionSource.cs │ ├── LazyTaskCompletionSourceT.cs │ ├── MethodBuilders │ ├── FireAndForgetMethodBuilder.cs │ ├── PooledTaskMethodBuilder.cs │ ├── PooledTaskMethodBuilderT.cs │ ├── PooledValueTaskMethodBuilder.cs │ └── PooledValueTaskMethodBuilderT.cs │ ├── Pool.cs │ ├── PoolSizeAttribute.cs │ ├── PoolT.cs │ ├── PooledAwait.csproj │ ├── PooledTask.cs │ ├── PooledTaskT.cs │ ├── PooledValueTask.cs │ ├── PooledValueTaskSource.cs │ ├── PooledValueTaskSourceT.cs │ ├── PooledValueTaskT.cs │ ├── Properties │ └── AssemblyInfo.cs │ └── ValueTaskCompletionSourceT.cs ├── tests ├── Benchmark │ ├── Benchmark.csproj │ ├── ComparisonBenchmarks.cs │ ├── PoolStrategyTests.cs │ └── Program.cs └── PooledAwait.Test │ ├── BasicTests.cs │ ├── BoxTests.cs │ ├── CompletedTaskIdentityTests.cs │ ├── ComplexAsyncTests.cs │ ├── ContextTests.cs │ ├── FireAndForgetTests.cs │ ├── LazyTaskTests.cs │ ├── NullableTypes.cs │ ├── PoolTests.cs │ ├── PooledAwait.Test.csproj │ ├── PooledValueTaskSourceTests.cs │ └── ValueTaskCompletionSourceTests.cs └── version.json /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | bin/ 3 | obj/ 4 | BenchmarkDotNet.Artifacts/ 5 | *.csproj.user -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(MSBuildThisFileDirectory)PooledAwait.snk 4 | true 5 | false 6 | preview 7 | enable 8 | mgravell 9 | NU5104;NU5125 10 | 2019 Marc Gravell 11 | true 12 | 13 | git 14 | https://github.com/mgravell/PooledAwait/ 15 | https://github.com/mgravell/PooledAwait/ 16 | MIT 17 | 18 | true 19 | embedded 20 | en-US 21 | false 22 | true 23 | 24 | 25 | true 26 | true 27 | true 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)')) 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE.TXT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Marc Gravell 4 | 5 | All rights reserved. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | Additional licenses: 26 | 27 | ManualResetValueTaskSourceCore is Copyright (c) .NET Foundation and Contributors, 28 | and is made available under The MIT License -------------------------------------------------------------------------------- /PooledAwait.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29006.145 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{6DC27E35-76A1-4DB4-9A43-BEA952B688E8}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PooledAwait", "src\PooledAwait\PooledAwait.csproj", "{140C1306-3443-4F45-B132-27197EB4D146}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{04573B33-34F0-429A-AC95-376A9D819666}" 11 | ProjectSection(SolutionItems) = preProject 12 | .gitignore = .gitignore 13 | Directory.Build.props = Directory.Build.props 14 | Directory.Build.targets = Directory.Build.targets 15 | global.json = global.json 16 | LICENSE.TXT = LICENSE.TXT 17 | nuget.config = nuget.config 18 | readme.md = readme.md 19 | version.json = version.json 20 | EndProjectSection 21 | EndProject 22 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{AF54CE9C-B78F-4023-8A66-367509185F67}" 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmark", "tests\Benchmark\Benchmark.csproj", "{CC23CF2D-D42E-4470-8BBD-A847DB7CB8CC}" 25 | EndProject 26 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PooledAwait.Test", "tests\PooledAwait.Test\PooledAwait.Test.csproj", "{1AD2D514-48A4-4631-867E-F4D5D2765C7B}" 27 | EndProject 28 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{9E246D47-3A08-4D10-A66F-DFEEC21BFF34}" 29 | ProjectSection(SolutionItems) = preProject 30 | docs\index.md = docs\index.md 31 | EndProjectSection 32 | EndProject 33 | Global 34 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 35 | Debug|Any CPU = Debug|Any CPU 36 | Release|Any CPU = Release|Any CPU 37 | EndGlobalSection 38 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 39 | {140C1306-3443-4F45-B132-27197EB4D146}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {140C1306-3443-4F45-B132-27197EB4D146}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {140C1306-3443-4F45-B132-27197EB4D146}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {140C1306-3443-4F45-B132-27197EB4D146}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {CC23CF2D-D42E-4470-8BBD-A847DB7CB8CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {CC23CF2D-D42E-4470-8BBD-A847DB7CB8CC}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {CC23CF2D-D42E-4470-8BBD-A847DB7CB8CC}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {CC23CF2D-D42E-4470-8BBD-A847DB7CB8CC}.Release|Any CPU.Build.0 = Release|Any CPU 47 | {1AD2D514-48A4-4631-867E-F4D5D2765C7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {1AD2D514-48A4-4631-867E-F4D5D2765C7B}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {1AD2D514-48A4-4631-867E-F4D5D2765C7B}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {1AD2D514-48A4-4631-867E-F4D5D2765C7B}.Release|Any CPU.Build.0 = Release|Any CPU 51 | EndGlobalSection 52 | GlobalSection(SolutionProperties) = preSolution 53 | HideSolutionNode = FALSE 54 | EndGlobalSection 55 | GlobalSection(NestedProjects) = preSolution 56 | {140C1306-3443-4F45-B132-27197EB4D146} = {6DC27E35-76A1-4DB4-9A43-BEA952B688E8} 57 | {CC23CF2D-D42E-4470-8BBD-A847DB7CB8CC} = {AF54CE9C-B78F-4023-8A66-367509185F67} 58 | {1AD2D514-48A4-4631-867E-F4D5D2765C7B} = {AF54CE9C-B78F-4023-8A66-367509185F67} 59 | EndGlobalSection 60 | GlobalSection(ExtensibilityGlobals) = postSolution 61 | SolutionGuid = {3DCCF6DC-DEB4-48FB-BD2E-2FBC48C054EC} 62 | EndGlobalSection 63 | EndGlobal 64 | -------------------------------------------------------------------------------- /PooledAwait.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgravell/PooledAwait/e1b18312be845e77751178be1793aaee95ab22c6/PooledAwait.snk -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # PooledAwait 2 | 3 | Low-allocation utilies for writing `async` methods, and related tools 4 | 5 | ### Contents 6 | 7 | - [`PooledValueTask` / `PooledValueTask`](#pooledvaluetask--pooledvaluetaskt) 8 | - [`PooledTask` / `PooledTask`](#pooledtask--pooledtaskt) 9 | - [`FireAndForget`](#fireandforget) 10 | - [`ConfiguredYieldAwaitable`](#configuredyieldawaitable) 11 | - [`ValueTaskCompletionSource`](#valuetaskcompletionsourcet) 12 | - [`PooledValueTaskSource / PooledValueTaskSource`](#pooledvaluetasksource--pooledvaluetasksourcet) 13 | - [`LazyTaskCompletionSource / LazyTaskCompletionSource`](#lazytaskcompletionsource--lazytaskcompletionsourcet) 14 | - [`Pool`](#pool) 15 | 16 | --- 17 | 18 | ## `PooledValueTask` / `PooledValueTask` 19 | 20 | These are the main tools of the library; their purpose is to remove the boxing of the async state-machine and builder that happens when a method 21 | marked `async` performs an `await` on an awaitable target that *is not yet complete*, i.e. 22 | 23 | ``` c# 24 | async ValueTask SomeMethod() 25 | { 26 | await Task.Yield(); // *is not yet complete* 27 | return 42 28 | } 29 | ``` 30 | 31 | If you've ever looked at an application that uses `async` / `await` in a memory profiler and seen things like `System.Runtime.CompilerServices.AsyncTaskMethodBuilder``1.AsyncStateMachineBox``1` 32 | or `YourLib.<g__Inner|8_0>d`, then that's what I'm talking about. You can avoid this by simply using a different return type: 33 | 34 | - `PooledValueTask` instead of `ValueTask` 35 | - `PooledValueTask` instead of `ValueTask` 36 | 37 | For `private` / `internal` methods, you can probably just *change the return type directly*: 38 | 39 | ``` c# 40 | private async PooledValueTask SomeMethod() 41 | { 42 | await Task.Yield(); // *is not yet complete* 43 | return 42 44 | } 45 | ``` 46 | 47 | For methods on your `public` API surface, you can use a "local function" to achieve the same thing without changing the exposed return type: 48 | 49 | ``` c# 50 | public ValueTask SomeMethod() // not marked async 51 | { 52 | return Impl(); 53 | async PooledValueTask() Impl() 54 | { 55 | await Task.Yield(); // *is not yet complete* 56 | return 42 57 | } 58 | } 59 | ``` 60 | 61 | (all of the `Pooled*` types have `implicit` conversion operators to their more well-recognized brethren). 62 | 63 | And that's it! That's all you have to do. The "catch" (there's always a catch) is that awaiting the same pending operation *more than once* **no longer works**: 64 | 65 | ``` c# 66 | var pending = SomeIncompleteMethodAsync(); // note no "await" here 67 | 68 | var x = await pending; 69 | var y = await pending; // BOOM! await the **same result** 70 | ``` 71 | 72 | In reality, **this almost never happens**. Usually you `await` something *once*, *almost always* right away. So... yeah. 73 | 74 | --- 75 | 76 | ## `PooledTask` / `PooledTask` 77 | 78 | These work very similarly to `PooledValueTask[]`, but for the `Task[]` API. It can't be *quite* as frugal, as in most cases a `Task[]` 79 | will still need to be allocated (unless it is the non-generic `PooledTask` signature, and the operation completes synchronously), but it 80 | still avoids the state-machine box etc. Note that this API **is not** impacted by the "you can only await it once" change (you can 81 | await these as many times as you like - they are, after all, `Task[]`), but again: *this is used incredibly rarely anyway*. 82 | 83 | ## `FireAndForget` 84 | 85 | Ever find yourself needing a fire-and-forget API? This adds one. All you do is declare the return type as `FireAndForget`: 86 | 87 | ``` c# 88 | FireAndForget SomeMethod(...) { 89 | // .. things before the first incomplete await happen on the calling thread 90 | await SomeIncompleteMethod(); 91 | // .. other bits continue running in the background 92 | } 93 | ``` 94 | 95 | As soon as the method uses `await` against an incomplete operation, the calling 96 | task regains control as though it were complete; the rest of the operation continues in the background. The caller can simply `await` 97 | the fire-and-forget method with confidence that it only runs synchronously to the first incomplete operation. If you're not in an `async` 98 | method, you can use "discard" to tell the compiler not to tell you to `await` it: 99 | 100 | ``` c# 101 | _ = SomeFireAndForgetMethodAsync(); 102 | ``` 103 | 104 | You won't get unobserved-task-exception problems. If you want to see any exceptions that happen, there is an event `FireAndForget.Exception` 105 | that you can subscribe to. Otherwise, they just evaporate. 106 | 107 | --- 108 | 109 | ## `ConfiguredYieldAwaitable` 110 | 111 | Related to `FireAndForget` - when you `await Task.Yield()` it always respects the sync-context/task-scheduler; sometimes *you don't want to*. 112 | For many awaitables there is a `.ConfigureAwait(continueOnCapturedContext: false)` method that you can use to suppress this, but 113 | not on `Task.Yield()`... *until now*. Usage is, as you would expect: 114 | 115 | ``` c# 116 | await Task.Yield().ConfigureAwait(false); 117 | ``` 118 | 119 | --- 120 | 121 | ## `ValueTaskCompletionSource` 122 | 123 | Do you make use of `TaskCompletionSource`? Do you hate that this adds another allocation *on top of* the `Task` that you actually wanted? 124 | `ValueTaskCompletionSource` is your friend. It uses smoke and magic to work like `TaskCompletionSource`, but without the extra 125 | allocation (unless it discovers that the magic isn't working for your system). Usage: 126 | 127 | ``` c# 128 | var source = ValueTaskCompletionSource.Create(); 129 | // ... 130 | source.TrySetResult(42); // etc 131 | ``` 132 | 133 | The main difference here is that you now have a `struct` instead of a `class`. If you want to test whether an instance is a *real* value 134 | (as opposed to the `default`), check `.HasTask`. 135 | 136 | --- 137 | 138 | ## `PooledValueTaskSource` / `PooledValueTaskSource` 139 | 140 | These again work like `TaskCompletionSource`, but a: for `ValueType[]`, and b: with the same zero-allocation features that 141 | `PooledValueTask` / `PooledValueTask` exhibit. Once again, the "catch" is that you can only await their `.Task` *once*. Usage: 142 | 143 | ``` c# 144 | var source = PooledValueTaskSource.Create(); 145 | // ... 146 | source.TrySetResult(42); // etc 147 | ``` 148 | 149 | --- 150 | 151 | ## `LazyTaskCompletionSource / LazyTaskCompletionSource` 152 | 153 | Sometimes, you have an API where you *aren't sure* whether someone is subscribing to the `Task`/`Task` results - for example 154 | you have properties like: 155 | 156 | ``` c# 157 | public Task SomeStepCompleted { get; } 158 | ``` 159 | 160 | It would be a shame to allocate a `Task` for this *just in case*, so `LazyTaskCompletionSource[]` allows you to *rent* state 161 | that can manage *lazily* creating a task. If the `.Task` is read before the value is set, a *source* is used to provide a 162 | pending task; if the result gets set before the value is read, then some optimizations may be possible (`Task.CompletedTask`, etc). 163 | And if the `.Task` is never queried: no task or source is allocated. These types are disposable; disposing them releases any 164 | rented state for re-use. 165 | 166 | --- 167 | 168 | ## `Pool` 169 | 170 | Ever need a light-weight basic pool of objects? That's this. Nothing fancy. The first API is a simple get/put: 171 | 172 | ``` c# 173 | var obj = Pool.TryRent() ?? new SomeType(); 174 | // ... 175 | Pool.Return(obj); 176 | ``` 177 | 178 | Note that it leaves creation to you (hence the `?? new SomeType()`), and it is the caller's responsibility to not retain and access 179 | a reference object that you have notionally returned to the pool. 180 | 181 | Considerations: 182 | 183 | - you may wish to use `try`/`finally` to put things back into the pool even if you leave through failure 184 | - if the object might **unnecessarily** keep large graphs of sub-objects "reachable" (in terms of GC), you should ensure that any references are wiped before putting an object into the pool 185 | - if the object implements `IResettable`, the pool will automatically call the `Reset()` method for you before storing items in the pool 186 | 187 | A second API is exposed for use with value-types; there are a lot of scenarios in which you have some state that you need to expose 188 | to an API that takes `object` - especially with callbacks like `WaitCallback`, `SendOrPostCallback`, `Action`, etc. The data 189 | will only be unboxed once at the receiver - so: rather than use a *regular* box, we can *rent* a box. Also, if you have multiple items of 190 | state that you need to convey - consider a value-tuple. 191 | 192 | ``` c# 193 | int id = ... 194 | string name = ... 195 | var obj = Pool.Box((id, name)); 196 | // ... probably pass obj to a callback-API 197 | ``` 198 | 199 | then later: 200 | 201 | ``` c# 202 | (var id, var name) = Pool.UnboxAndReturn<(int, string)>(obj); 203 | // use id/name as usual 204 | ``` 205 | 206 | It is the caller's responsibility to only access the state once. 207 | 208 | The pool is global (`static`) and pretty modest in size. You can control it *a bit* by adding `[PoolSize(...)]` to the custom 209 | classes and value-types that you use. 210 | 211 | 212 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "3.0.100", 4 | "rollForward": "latestMajor", 5 | "allowPrerelease": false 6 | } 7 | } -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [Documentation](https://mgravell.github.io/PooledAwait/) 2 | 3 | # What is this? 4 | 5 | You know how `async` methods that `await` something **incomplete** end up creating a few objects, right? There's 6 | the boxed state machine, an `Action` that moves it forward, a `Task[]`, etc - right? 7 | 8 | Well... what about if there just **wasn't**? 9 | 10 | And what if all you had to do was change your `async ValueTask` method to `async PooledValueTask`? 11 | 12 | And I hear you; you're saying "but I can't change the public API!". But what if a `PooledValueTask` really *was* 13 | a `ValueTask`? So you can just cheat: 14 | 15 | ``` c# 16 | public ValueTask DoTheThing() // the outer method is not async 17 | { 18 | return ReallyDoTheThing(this); 19 | static async PooledValueTask ReallyDoTheThing(SomeType obj) 20 | { 21 | ... await ... 22 | // (use obj.* instead of this.*) 23 | ... return ... 24 | } 25 | } 26 | ``` 27 | 28 | (the use of a `static` local function here avoids a `<>c__DisplayClass` wrapper from how the local-function capture context is implemented by the compiler) 29 | 30 | And how about if maybe just maybe in the future it could be ([if this happens](https://github.com/dotnet/csharplang/issues/1407)) just: 31 | 32 | ``` c# 33 | [SomeKindOfAttribute] // <=== this is the only change 34 | public async ValueTask DoTheThing() 35 | { 36 | // no changes here at all 37 | } 38 | ``` 39 | 40 | (although note that in some cases it can work *better* with the `static` trick, as above) 41 | 42 | Would that be awesome? Because that's what this is! 43 | 44 | # How does that work? 45 | 46 | The `PooledValueTask[]` etc exist mostly to define a custom **builder**. The builder in this library uses aggressive pooling of classes 47 | that replace the *boxed* approach used by default; we recycle them when the state machine completes. 48 | 49 | It also makes use of the `IValueTaskSource[]` API to allow incomplete operations to be represented without a `Task[]`, but with a custom backer. 50 | And we pool that too, recycling it when the task is *awaited*. The only downside: you can't `await` the same result *twice* now, because 51 | once you've awaited it the first time, **it has gone**. A cycling token is used to make sure you can't accidentally read the incorrect 52 | values after the result has been awaited. 53 | 54 | We can *even* do this for `Task[]`, except here we can only avoid the boxed state machine; hence `PooledTask[]` exists too. No custom backing in this case, though, since a `Task[]` will 55 | need to be allocated (except for `Task.CompletedTask`, which we special-case). 56 | 57 | # Test results 58 | 59 | Based on an operation that uses `Task.Yield()` to ensure that the operations are incomplete; ".NET" means the inbuilt out-of-the box implementation; "Pooled" means the implementation from this library. 60 | 61 | In particular, notice: 62 | 63 | - zero allocations for `PooledValueTask[]` vs `ValueTask[]` (on .NET Core; *significantly reduced* on .NET Framework) 64 | - *reduced* allocations for `PooledTask[]` vs `Task[]` 65 | - no performance degredation; just lower allocations 66 | 67 | ``` txt 68 | | Method | Job | Runtime | Categories | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated | 69 | |------- |----- |-------- |------------- |---------:|----------:|----------:|-------:|-------:|-------:|----------:| 70 | | .NET | Clr | Clr | Task | 2.159 us | 0.0427 us | 0.0474 us | 0.0508 | 0.0039 | - | 344 B | 71 | | Pooled | Clr | Clr | Task | 2.037 us | 0.0246 us | 0.0230 us | 0.0273 | 0.0039 | - | 182 B | 72 | | .NET | Core | Core | Task | 1.397 us | 0.0024 us | 0.0022 us | 0.0176 | - | - | 120 B | 73 | | Pooled | Core | Core | Task | 1.349 us | 0.0058 us | 0.0054 us | 0.0098 | - | - | 72 B | 74 | | | | | | | | | | | | | 75 | | .NET | Clr | Clr | Task | 2.065 us | 0.0200 us | 0.0167 us | 0.0508 | 0.0039 | - | 336 B | 76 | | Pooled | Clr | Clr | Task | 1.979 us | 0.0179 us | 0.0167 us | 0.0273 | 0.0039 | - | 182 B | 77 | | .NET | Core | Core | Task | 1.390 us | 0.0159 us | 0.0149 us | 0.0176 | - | - | 112 B | 78 | | Pooled | Core | Core | Task | 1.361 us | 0.0055 us | 0.0051 us | 0.0098 | - | - | 72 B | 79 | | | | | | | | | | | | | 80 | | .NET | Clr | Clr | ValueTask | 2.087 us | 0.0403 us | 0.0431 us | 0.0547 | 0.0078 | 0.0039 | 352 B | 81 | | Pooled | Clr | Clr | ValueTask | 1.924 us | 0.0248 us | 0.0220 us | 0.0137 | 0.0020 | - | 100 B | 82 | | .NET | Core | Core | ValueTask | 1.405 us | 0.0078 us | 0.0073 us | 0.0195 | - | - | 128 B | 83 | | Pooled | Core | Core | ValueTask | 1.374 us | 0.0116 us | 0.0109 us | - | - | - | - | 84 | | | | | | | | | | | | | 85 | | .NET | Clr | Clr | ValueTask | 2.056 us | 0.0206 us | 0.0183 us | 0.0508 | 0.0039 | - | 344 B | 86 | | Pooled | Clr | Clr | ValueTask | 1.948 us | 0.0388 us | 0.0416 us | 0.0137 | 0.0020 | - | 100 B | 87 | | .NET | Core | Core | ValueTask | 1.408 us | 0.0140 us | 0.0117 us | 0.0176 | - | - | 120 B | 88 | | Pooled | Core | Core | ValueTask | 1.366 us | 0.0039 us | 0.0034 us | - | - | - | - | 89 | ``` 90 | 91 | Note that *most* of the remaining allocations are actually the work-queue internals of `Task.Yield()` (i.e. how 92 | `ThreadPool.QueueUserWorkItem` works) - we've removed virtually all of the unnecessary overheads that came from the 93 | `async` machinery. Most real-world scenarios aren't using `Task.Yield()` - they are waiting on external data, etc - so 94 | they won't see these. Plus they are effectively zero on .NET Core 3. 95 | 96 | The tests do the exact same thing; the only thing that changes is the return type, i.e. whether it is 97 | `async Task`, `async ValueTask`, `async PooledTask` or `async PooledValueTask`. 98 | All of them have the same threading/execution-context/sync-context semantics; there's no cheating going on. 99 | 100 | -------------------------------------------------------------------------------- /src/PooledAwait/AwaitableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace PooledAwait 4 | { 5 | /// 6 | /// Provides async/await-related extension methods 7 | /// 8 | public static class AwaitableExtensions 9 | { 10 | /// Controls whether a yield operation should respect captured context 11 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 12 | public static ConfiguredYieldAwaitable ConfigureAwait(this YieldAwaitable _, bool continueOnCapturedContext) 13 | => new ConfiguredYieldAwaitable(continueOnCapturedContext); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/PooledAwait/ConfiguredYieldAwaitable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using System.Threading; 4 | 5 | namespace PooledAwait 6 | { 7 | /// Provides an awaitable context for switching into a target environment. 8 | public readonly struct ConfiguredYieldAwaitable 9 | { 10 | /// 11 | public override bool Equals(object? obj) => obj is ConfiguredYieldAwaitable other && other._continueOnCapturedContext == _continueOnCapturedContext; 12 | /// 13 | public override int GetHashCode() => _continueOnCapturedContext ? 1 : 0; 14 | /// 15 | public override string ToString() => nameof(ConfiguredYieldAwaitable); 16 | 17 | private readonly bool _continueOnCapturedContext; 18 | 19 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 20 | internal ConfiguredYieldAwaitable(bool continueOnCapturedContext) 21 | => _continueOnCapturedContext = continueOnCapturedContext; 22 | 23 | /// Gets an awaiter for this . 24 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 25 | public ConfiguredYieldAwaiter GetAwaiter() 26 | => new ConfiguredYieldAwaiter(_continueOnCapturedContext); 27 | 28 | /// Provides an awaitable context for switching into a target environment. 29 | public readonly struct ConfiguredYieldAwaiter : ICriticalNotifyCompletion, INotifyCompletion 30 | { 31 | /// 32 | public override bool Equals(object? obj) => obj is ConfiguredYieldAwaiter other && other._continueOnCapturedContext == _continueOnCapturedContext; 33 | /// 34 | public override int GetHashCode() => _continueOnCapturedContext ? 1 : 0; 35 | /// 36 | public override string ToString() => nameof(ConfiguredYieldAwaiter); 37 | 38 | private readonly bool _continueOnCapturedContext; 39 | 40 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 41 | internal ConfiguredYieldAwaiter(bool continueOnCapturedContext) 42 | => _continueOnCapturedContext = continueOnCapturedContext; 43 | 44 | /// Gets whether a yield is not required. 45 | public bool IsCompleted 46 | { 47 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 48 | get => false; 49 | } 50 | 51 | /// Ends the await operation. 52 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 53 | public void GetResult() { } 54 | 55 | /// Posts the back to the current context. 56 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 57 | public void OnCompleted(Action continuation) 58 | { 59 | if (_continueOnCapturedContext) YieldFlowContext(continuation, true); 60 | else YieldNoContext(continuation, true); 61 | } 62 | 63 | /// Posts the back to the current context. 64 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 65 | public void UnsafeOnCompleted(Action continuation) 66 | { 67 | if (_continueOnCapturedContext) YieldFlowContext(continuation, false); 68 | else YieldNoContext(continuation, false); 69 | } 70 | 71 | private static readonly WaitCallback s_waitCallbackRunAction = state => ((Action?)state)?.Invoke(); 72 | 73 | #if PLAT_THREADPOOLWORKITEM 74 | private sealed class ContinuationWorkItem : IThreadPoolWorkItem 75 | { 76 | private Action? _continuation; 77 | 78 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 79 | private ContinuationWorkItem() => Internal.Counters.ItemBoxAllocated.Increment(); 80 | 81 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 82 | public static ContinuationWorkItem Create(Action continuation) 83 | { 84 | var box = Pool.TryGet() ?? new ContinuationWorkItem(); 85 | box._continuation = continuation; 86 | return box; 87 | } 88 | 89 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 90 | void IThreadPoolWorkItem.Execute() 91 | { 92 | var callback = _continuation; 93 | _continuation = null; 94 | Pool.TryPut(this); 95 | callback?.Invoke(); 96 | } 97 | } 98 | #endif 99 | [MethodImpl(MethodImplOptions.NoInlining)] // no-one ever calls ConfigureAwait(true)! 100 | private static void YieldFlowContext(Action continuation, bool flowContext) 101 | { 102 | var awaiter = default(YieldAwaitable.YieldAwaiter); 103 | if (flowContext) awaiter.OnCompleted(continuation); 104 | else awaiter.UnsafeOnCompleted(continuation); 105 | } 106 | 107 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 108 | private static void YieldNoContext(Action continuation, bool flowContext) 109 | { 110 | if (flowContext) 111 | { 112 | ThreadPool.QueueUserWorkItem(s_waitCallbackRunAction, continuation); 113 | } 114 | else 115 | { 116 | #if PLAT_THREADPOOLWORKITEM 117 | ThreadPool.UnsafeQueueUserWorkItem(ContinuationWorkItem.Create(continuation), false); 118 | #elif NETSTANDARD1_3 119 | ThreadPool.QueueUserWorkItem(s_waitCallbackRunAction, continuation); 120 | #else 121 | ThreadPool.UnsafeQueueUserWorkItem(s_waitCallbackRunAction, continuation); 122 | #endif 123 | } 124 | } 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/PooledAwait/FireAndForget.cs: -------------------------------------------------------------------------------- 1 | using PooledAwait.Internal; 2 | using System; 3 | using System.Runtime.CompilerServices; 4 | using System.Threading.Tasks; 5 | 6 | namespace PooledAwait 7 | { 8 | /// 9 | /// Represents an operation that completes at the first incomplete await, 10 | /// with the remainder continuing in the background 11 | /// 12 | [AsyncMethodBuilder(typeof(MethodBuilders.FireAndForgetMethodBuilder))] 13 | public readonly struct FireAndForget 14 | { 15 | /// 16 | public override bool Equals(object? obj) => ThrowHelper.ThrowNotSupportedException(); 17 | /// 18 | public override int GetHashCode() => ThrowHelper.ThrowNotSupportedException(); 19 | /// 20 | public override string ToString() => nameof(FireAndForget); 21 | 22 | /// 23 | /// Raised when exceptions occur on fire-and-forget methods 24 | /// 25 | public static event Action? Exception; 26 | 27 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 28 | internal static void OnException(Exception exception) 29 | { 30 | if (exception != null) Exception?.Invoke(exception); 31 | } 32 | 33 | /// 34 | /// Gets the instance as a value-task 35 | /// 36 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 37 | public ValueTask AsValueTask() => default; 38 | 39 | /// 40 | /// Gets the instance as a task 41 | /// 42 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 43 | public Task AsTask() => TaskUtils.CompletedTask; 44 | 45 | /// 46 | /// Gets the instance as a task 47 | /// 48 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 49 | public static implicit operator Task(FireAndForget _) => TaskUtils.CompletedTask; 50 | 51 | /// 52 | /// Gets the instance as a value-task 53 | /// 54 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 55 | public static implicit operator ValueTask(FireAndForget _) => default; 56 | 57 | /// 58 | /// Gets the awaiter for the instance 59 | /// 60 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 61 | public ValueTaskAwaiter GetAwaiter() => default(ValueTask).GetAwaiter(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/PooledAwait/IResettable.cs: -------------------------------------------------------------------------------- 1 | namespace PooledAwait 2 | { 3 | /// 4 | /// Indicates that an object can be reset 5 | /// 6 | public interface IResettable 7 | { 8 | /// 9 | /// Resets this instance 10 | /// 11 | public void Reset(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/PooledAwait/Internal/BrowsableAttribute.cs: -------------------------------------------------------------------------------- 1 | #if NETSTANDARD1_3 2 | using System; 3 | 4 | namespace PooledAwait.Internal 5 | { 6 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = true)] 7 | internal sealed class BrowsableAttribute : Attribute 8 | { 9 | public BrowsableAttribute(bool _) { } 10 | } 11 | } 12 | #endif -------------------------------------------------------------------------------- /src/PooledAwait/Internal/Counters.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Threading; 3 | 4 | namespace PooledAwait.Internal 5 | { 6 | internal static class Counters 7 | { 8 | internal struct Counter 9 | { 10 | private long _value; 11 | [Conditional("DEBUG")] 12 | public void Increment() => Interlocked.Increment(ref _value); 13 | #if !DEBUG 14 | [System.Obsolete("Release only", false)] 15 | #endif 16 | public long Value => Interlocked.Read(ref _value); 17 | #pragma warning disable CS0618 18 | public override string ToString() => Value.ToString(); 19 | #pragma warning restore CS0618 20 | public override bool Equals(object? obj) => ThrowHelper.ThrowNotSupportedException(); 21 | public override int GetHashCode() => ThrowHelper.ThrowNotSupportedException(); 22 | public void Reset() => Interlocked.Exchange(ref _value, 0); 23 | } 24 | 25 | internal static Counter 26 | SetStateMachine, 27 | PooledStateAllocated, 28 | PooledStateRecycled, 29 | StateMachineBoxAllocated, 30 | StateMachineBoxRecycled, 31 | ItemBoxAllocated, 32 | TaskAllocated, 33 | LazyStateAllocated; 34 | 35 | #if !DEBUG 36 | [System.Obsolete("Release only", false)] 37 | #endif 38 | public static long TotalAllocations => 39 | PooledStateAllocated.Value + StateMachineBoxAllocated.Value 40 | + ItemBoxAllocated.Value + TaskAllocated.Value 41 | + SetStateMachine.Value // SetStateMachine usually means a boxed value 42 | + LazyStateAllocated.Value; 43 | 44 | internal static void Reset() 45 | { 46 | SetStateMachine.Reset(); 47 | PooledStateAllocated.Reset(); 48 | PooledStateRecycled.Reset(); 49 | StateMachineBoxAllocated.Reset(); 50 | StateMachineBoxRecycled.Reset(); 51 | ItemBoxAllocated.Reset(); 52 | TaskAllocated.Reset(); 53 | LazyStateAllocated.Reset(); 54 | } 55 | 56 | #if !DEBUG 57 | [System.Obsolete("Release only", false)] 58 | #endif 59 | internal static string Summary() 60 | => $@"SetStateMachine: {SetStateMachine.Value} 61 | PooledStateAllocated: {PooledStateAllocated.Value} 62 | PooledStateRecycled: {PooledStateRecycled.Value} 63 | StateMachineBoxAllocated: {StateMachineBoxAllocated.Value} 64 | StateMachineBoxRecycled: {StateMachineBoxRecycled.Value} 65 | ItemBoxAllocated: {ItemBoxAllocated.Value} 66 | TaskAllocated: {TaskAllocated.Value} 67 | LazyStateAllocated: {LazyStateAllocated.Value}"; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/PooledAwait/Internal/LazyTaskStateT.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace PooledAwait.Internal 7 | { 8 | internal sealed class LazyTaskState 9 | { 10 | private short _version; 11 | private T _result; 12 | private Exception? _exception; 13 | private Task? _task; 14 | private bool _isComplete; 15 | private ValueTaskCompletionSource _source; 16 | 17 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 18 | private void CheckTokenInsideLock(short token) 19 | { 20 | if (token != _version) ThrowHelper.ThrowInvalidOperationException(); 21 | } 22 | 23 | public Task GetTask(short token) 24 | { 25 | lock (this) 26 | { 27 | CheckTokenInsideLock(token); 28 | if (_task != null) { } 29 | else if (_exception is OperationCanceledException) _task = TaskUtils.TaskFactory.Canceled; 30 | else if (_exception != null) _task = TaskUtils.FromException(_exception); 31 | else if (_isComplete) _task = typeof(T) == typeof(Nothing) ? TaskUtils.CompletedTask : TaskUtils.TaskFactory.FromResult(_result); 32 | else 33 | { 34 | _source = ValueTaskCompletionSource.Create(); 35 | _task = _source.Task; 36 | } 37 | return _task; 38 | } 39 | } 40 | 41 | internal bool IsValid(short token) => Volatile.Read(ref _version) == token; 42 | internal bool HasSource 43 | { 44 | get 45 | { 46 | lock (this) { return !_source.IsNull; } 47 | } 48 | } 49 | internal bool HasTask => Volatile.Read(ref _task) != null; 50 | 51 | public bool TrySetResult(short token, T result) 52 | { 53 | lock (this) 54 | { 55 | if (_isComplete) return false; 56 | if (token != _version) return false; 57 | _isComplete = true; 58 | if (!_source.IsNull) return _source.TrySetResult(result); 59 | _result = result; 60 | return true; 61 | } 62 | } 63 | 64 | public bool TrySetException(short token, Exception exception) 65 | { 66 | lock (this) 67 | { 68 | if (_isComplete) return false; 69 | if (token != _version) return false; 70 | _isComplete = true; 71 | if (!_source.IsNull) return _source.TrySetException(exception); 72 | _exception = exception; 73 | return true; 74 | } 75 | } 76 | 77 | public bool TrySetCanceled(short token, CancellationToken cancellationToken = default) 78 | { 79 | lock (this) 80 | { 81 | if (_isComplete) return false; 82 | if (token != _version) return false; 83 | _isComplete = true; 84 | if (!_source.IsNull) return _source.TrySetCanceled(cancellationToken); 85 | _task = TaskUtils.TaskFactory.Canceled; 86 | return true; 87 | } 88 | } 89 | 90 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 91 | public static LazyTaskState Create() => Pool>.TryGet() ?? new LazyTaskState(); 92 | 93 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 94 | private LazyTaskState() 95 | { 96 | Counters.LazyStateAllocated.Increment(); 97 | _result = default!; 98 | _version = InitialVersion; 99 | } 100 | 101 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 102 | public static LazyTaskState CreateConstant(T value) 103 | { 104 | var obj = new LazyTaskState 105 | { 106 | _version = Constant 107 | }; 108 | obj.TrySetResult(Constant, value); 109 | return obj; 110 | } 111 | 112 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 113 | public static LazyTaskState CreateCanceled() 114 | { 115 | var obj = new LazyTaskState 116 | { 117 | _version = Constant 118 | }; 119 | obj.TrySetCanceled(Constant); 120 | return obj; 121 | } 122 | 123 | const short InitialVersion = 0, Constant = InitialVersion - 1; 124 | 125 | internal short Version 126 | { 127 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 128 | get => _version; 129 | } 130 | 131 | [MethodImpl(MethodImplOptions.NoInlining)] 132 | internal void Recycle(short token) 133 | { 134 | if (token == Constant) return; // never recycle constant values; this is by design 135 | 136 | if (Volatile.Read(ref _version) != token) return; // wrong version; all bets are off! 137 | 138 | if (!Volatile.Read(ref _isComplete)) // if incomplete, try to cancel 139 | { 140 | if (!TrySetCanceled(token)) return; // if that didn't work... give up - don't recycle 141 | } 142 | 143 | bool haveLock = false; 144 | try 145 | { 146 | // only if uncontested; we're not waiting in a dispose 147 | Monitor.TryEnter(this, ref haveLock); 148 | if (haveLock) 149 | { 150 | if (token == _version) 151 | { 152 | _result = default!; 153 | _exception = default; 154 | _task = default; 155 | _isComplete = false; 156 | _source = default; 157 | 158 | switch (++_version) 159 | { 160 | case InitialVersion: // don't wrap all the way around when recycling; could lead to conflicts 161 | case Constant: // don't allow things to *become* constants 162 | break; 163 | default: 164 | Pool>.TryPut(this); 165 | break; 166 | } 167 | } 168 | } 169 | } 170 | finally 171 | { 172 | if (haveLock) Monitor.Exit(this); 173 | } 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/PooledAwait/Internal/ManualResetValueTaskSourceCore.cs: -------------------------------------------------------------------------------- 1 | #if !PLAT_MRVTSC 2 | using System; 3 | using System.Diagnostics; 4 | using System.Runtime.ExceptionServices; 5 | using System.Runtime.InteropServices; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using System.Threading.Tasks.Sources; 9 | 10 | // from: https://raw.githubusercontent.com/dotnet/coreclr/master/src/System.Private.CoreLib/shared/System/Threading/Tasks/Sources/ManualResetValueTaskSourceCore.cs 11 | // original license: 12 | 13 | // Licensed to the .NET Foundation under one or more agreements. 14 | // The .NET Foundation licenses this file to you under the MIT license. 15 | // See the LICENSE file in the project root for more information. 16 | 17 | namespace PooledAwait.Internal 18 | { 19 | internal readonly struct WaitCallbackShim 20 | { 21 | public readonly Action? Continuation; 22 | public readonly object? State; 23 | private WaitCallbackShim(Action? continuation, object? state) 24 | { 25 | Continuation = continuation; 26 | State = state; 27 | } 28 | public static object Create(Action? continuation, object? state) 29 | => Pool.Box(new WaitCallbackShim(continuation, state)); 30 | 31 | private void InvokeContinuation() => Continuation?.Invoke(State); 32 | 33 | public static readonly WaitCallback Invoke = state => Pool.UnboxAndReturn(state).InvokeContinuation(); 34 | } 35 | 36 | /// Provides the core logic for implementing a manual-reset or . 37 | /// 38 | [StructLayout(LayoutKind.Auto)] 39 | public struct ManualResetValueTaskSourceCore 40 | { 41 | /// 42 | /// The callback to invoke when the operation completes if was called before the operation completed, 43 | /// or if the operation completed before a callback was supplied, 44 | /// or null if a callback hasn't yet been provided and the operation hasn't yet completed. 45 | /// 46 | private Action? _continuation; 47 | /// State to pass to . 48 | private object? _continuationState; 49 | /// to flow to the callback, or null if no flowing is required. 50 | private ExecutionContext? _executionContext; 51 | /// 52 | /// A "captured" or with which to invoke the callback, 53 | /// or null if no special context is required. 54 | /// 55 | private object? _capturedContext; 56 | /// Whether the current operation has completed. 57 | private bool _completed; 58 | /// The result with which the operation succeeded, or the default value if it hasn't yet completed or failed. 59 | /* [AllowNull, MaybeNull] */ private TResult _result; 60 | /// The exception with which the operation failed, or null if it hasn't yet completed or completed successfully. 61 | private ExceptionDispatchInfo? _error; 62 | /// The current version of this value, used to help prevent misuse. 63 | private short _version; 64 | 65 | /// Gets or sets whether to force continuations to run asynchronously. 66 | /// Continuations may run asynchronously if this is false, but they'll never run synchronously if this is true. 67 | public bool RunContinuationsAsynchronously { get; set; } 68 | 69 | /// Resets to prepare for the next operation. 70 | public void Reset() 71 | { 72 | // Reset/update state for the next use/await of this instance. 73 | _version++; 74 | _completed = false; 75 | _result = default!; // TODO-NULLABLE: Remove ! when nullable attributes are respected 76 | _error = null; 77 | _executionContext = null; 78 | _capturedContext = null; 79 | _continuation = null; 80 | _continuationState = null; 81 | } 82 | 83 | /// Completes with a successful result. 84 | /// The result. 85 | public void SetResult(TResult result) 86 | { 87 | _result = result; 88 | SignalCompletion(); 89 | } 90 | 91 | /// Complets with an error. 92 | /// 93 | public void SetException(Exception error) 94 | { 95 | _error = ExceptionDispatchInfo.Capture(error); 96 | SignalCompletion(); 97 | } 98 | 99 | /// Gets the operation version. 100 | public short Version => _version; 101 | 102 | /// Gets the status of the operation. 103 | /// Opaque value that was provided to the 's constructor. 104 | public ValueTaskSourceStatus GetStatus(short token) 105 | { 106 | ValidateToken(token); 107 | return 108 | _continuation == null || !_completed ? ValueTaskSourceStatus.Pending : 109 | _error == null ? ValueTaskSourceStatus.Succeeded : 110 | _error.SourceException is OperationCanceledException ? ValueTaskSourceStatus.Canceled : 111 | ValueTaskSourceStatus.Faulted; 112 | } 113 | 114 | /// Gets the result of the operation. 115 | /// Opaque value that was provided to the 's constructor. 116 | // [StackTraceHidden] 117 | public TResult GetResult(short token) 118 | { 119 | ValidateToken(token); 120 | if (!_completed) 121 | { 122 | ThrowHelper.ThrowInvalidOperationException(); 123 | } 124 | 125 | _error?.Throw(); 126 | return _result; 127 | } 128 | 129 | /// Schedules the continuation action for this operation. 130 | /// The continuation to invoke when the operation has completed. 131 | /// The state object to pass to when it's invoked. 132 | /// Opaque value that was provided to the 's constructor. 133 | /// The flags describing the behavior of the continuation. 134 | public void OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) 135 | { 136 | if (continuation == null) ThrowHelper.ThrowArgumentNullException(nameof(continuation)); 137 | ValidateToken(token); 138 | 139 | if ((flags & ValueTaskSourceOnCompletedFlags.FlowExecutionContext) != 0) 140 | { 141 | _executionContext = ExecutionContext.Capture(); 142 | } 143 | 144 | if ((flags & ValueTaskSourceOnCompletedFlags.UseSchedulingContext) != 0) 145 | { 146 | SynchronizationContext? sc = SynchronizationContext.Current; 147 | if (sc != null && sc.GetType() != typeof(SynchronizationContext)) 148 | { 149 | _capturedContext = sc; 150 | } 151 | else 152 | { 153 | TaskScheduler ts = TaskScheduler.Current; 154 | if (ts != TaskScheduler.Default) 155 | { 156 | _capturedContext = ts; 157 | } 158 | } 159 | } 160 | 161 | // We need to set the continuation state before we swap in the delegate, so that 162 | // if there's a race between this and SetResult/Exception and SetResult/Exception 163 | // sees the _continuation as non-null, it'll be able to invoke it with the state 164 | // stored here. However, this also means that if this is used incorrectly (e.g. 165 | // awaited twice concurrently), _continuationState might get erroneously overwritten. 166 | // To minimize the chances of that, we check preemptively whether _continuation 167 | // is already set to something other than the completion sentinel. 168 | 169 | object? oldContinuation = _continuation; 170 | if (oldContinuation == null) 171 | { 172 | _continuationState = state; 173 | oldContinuation = Interlocked.CompareExchange(ref _continuation, continuation, null); 174 | } 175 | 176 | if (oldContinuation != null) 177 | { 178 | // Operation already completed, so we need to queue the supplied callback. 179 | if (!ReferenceEquals(oldContinuation, ManualResetValueTaskSourceCoreShared.s_sentinel)) 180 | { 181 | ThrowHelper.ThrowInvalidOperationException(); 182 | } 183 | 184 | switch (_capturedContext) 185 | { 186 | case null: 187 | if (_executionContext != null) 188 | { 189 | ThreadPool.QueueUserWorkItem(WaitCallbackShim.Invoke, WaitCallbackShim.Create(continuation, state)); 190 | } 191 | else 192 | { 193 | #if NETSTANDARD1_3 194 | ThreadPool.QueueUserWorkItem( 195 | #else 196 | ThreadPool.UnsafeQueueUserWorkItem( 197 | #endif 198 | WaitCallbackShim.Invoke, WaitCallbackShim.Create(continuation, state)); 199 | } 200 | break; 201 | 202 | case SynchronizationContext sc: 203 | sc.Post(s => 204 | { 205 | var tuple = (Tuple, object?>)s!; 206 | tuple.Item1(tuple.Item2); 207 | }, Tuple.Create(continuation, state)); 208 | break; 209 | 210 | case TaskScheduler ts: 211 | Task.Factory.StartNew(continuation, state, CancellationToken.None, TaskCreationOptions.DenyChildAttach, ts); 212 | break; 213 | } 214 | } 215 | } 216 | 217 | /// Ensures that the specified token matches the current version. 218 | /// The token supplied by . 219 | private void ValidateToken(short token) 220 | { 221 | if (token != _version) 222 | { 223 | ThrowHelper.ThrowInvalidOperationException(); 224 | } 225 | } 226 | 227 | /// Signals that the operation has completed. Invoked after the result or error has been set. 228 | private void SignalCompletion() 229 | { 230 | if (_completed) 231 | { 232 | ThrowHelper.ThrowInvalidOperationException(); 233 | } 234 | _completed = true; 235 | 236 | if (_continuation != null || Interlocked.CompareExchange(ref _continuation, ManualResetValueTaskSourceCoreShared.s_sentinel, null) != null) 237 | { 238 | if (_executionContext != null) 239 | { 240 | ExecutionContext.Run( 241 | _executionContext, 242 | s_UnboxAndInvokeContextCallback, 243 | Pool.Box>(this)); 244 | } 245 | else 246 | { 247 | InvokeContinuation(); 248 | } 249 | } 250 | } 251 | 252 | static readonly ContextCallback s_UnboxAndInvokeContextCallback = state => Pool.UnboxAndReturn>(state).InvokeContinuation(); 253 | 254 | /// 255 | /// Invokes the continuation with the appropriate captured context / scheduler. 256 | /// This assumes that if is not null we're already 257 | /// running within that . 258 | /// 259 | private void InvokeContinuation() 260 | { 261 | Debug.Assert(_continuation != null); 262 | 263 | switch (_capturedContext) 264 | { 265 | case null: 266 | if (RunContinuationsAsynchronously) 267 | { 268 | if (_executionContext != null) 269 | { 270 | ThreadPool.QueueUserWorkItem(WaitCallbackShim.Invoke, WaitCallbackShim.Create(_continuation, _continuationState)); 271 | } 272 | else 273 | { 274 | #if NETSTANDARD1_3 275 | ThreadPool.QueueUserWorkItem( 276 | #else 277 | ThreadPool.UnsafeQueueUserWorkItem( 278 | #endif 279 | WaitCallbackShim.Invoke, WaitCallbackShim.Create(_continuation, _continuationState)); 280 | } 281 | } 282 | else 283 | { 284 | _continuation!(_continuationState); 285 | } 286 | break; 287 | 288 | case SynchronizationContext sc: 289 | sc.Post(s => 290 | { 291 | var state = (Tuple, object?>)s!; 292 | state.Item1(state.Item2); 293 | }, Tuple.Create(_continuation, _continuationState)); 294 | break; 295 | 296 | case TaskScheduler ts: 297 | Task.Factory.StartNew(_continuation, _continuationState, CancellationToken.None, TaskCreationOptions.DenyChildAttach, ts); 298 | break; 299 | } 300 | } 301 | } 302 | 303 | internal static class ManualResetValueTaskSourceCoreShared // separated out of generic to avoid unnecessary duplication 304 | { 305 | internal static readonly Action s_sentinel = CompletionSentinel; 306 | private static void CompletionSentinel(object? _) // named method to aid debugging 307 | { 308 | Debug.Fail("The sentinel delegate should never be invoked."); 309 | ThrowHelper.ThrowInvalidOperationException(); 310 | } 311 | } 312 | } 313 | #endif 314 | -------------------------------------------------------------------------------- /src/PooledAwait/Internal/Nothing.cs: -------------------------------------------------------------------------------- 1 | namespace PooledAwait.Internal 2 | { 3 | internal readonly struct Nothing // to express ValueTask via PooledState 4 | { 5 | public override string ToString() => nameof(Nothing); 6 | public override int GetHashCode() => 0; 7 | public override bool Equals(object? obj) => obj is Nothing; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/PooledAwait/Internal/PooledStateT.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using System.Threading; 4 | using System.Threading.Tasks.Sources; 5 | 6 | namespace PooledAwait.Internal 7 | { 8 | internal sealed class PooledState : IValueTaskSource, IValueTaskSource 9 | { 10 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 11 | public static PooledState Create(out short token) 12 | { 13 | var obj = Pool>.TryGet() ?? new PooledState(); 14 | token = obj._source.Version; 15 | return obj; 16 | } 17 | 18 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 19 | private PooledState() => Counters.PooledStateAllocated.Increment(); 20 | 21 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 22 | internal bool IsValid(short token) => _source.Version == token; 23 | 24 | private ManualResetValueTaskSourceCore _source; // needs to be mutable 25 | 26 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 27 | public T GetResult(short token) 28 | { 29 | // we only support getting the result once; doing this recycles the source and advances the token 30 | 31 | lock (SyncLock) // we need to be really paranoid about cross-threading over changing the token 32 | { 33 | var status = _source.GetStatus(token); // do this *outside* the try/finally 34 | try // so that we don't increment the counter if someone asks for the wrong value 35 | { 36 | switch(status) 37 | { 38 | case ValueTaskSourceStatus.Canceled: 39 | ThrowHelper.ThrowTaskCanceledException(); 40 | break; 41 | case ValueTaskSourceStatus.Pending: 42 | Monitor.Wait(SyncLock); 43 | break; 44 | } 45 | return _source.GetResult(token); 46 | } 47 | finally 48 | { 49 | _source.Reset(); 50 | if (_source.Version != TaskUtils.InitialTaskSourceVersion) 51 | { 52 | Pool>.TryPut(this); 53 | Counters.PooledStateRecycled.Increment(); 54 | } 55 | } 56 | } 57 | } 58 | 59 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 60 | void IValueTaskSource.GetResult(short token) => GetResult(token); 61 | 62 | [MethodImpl(MethodImplOptions.NoInlining)] 63 | private void SignalResult(short token) 64 | { 65 | lock (SyncLock) 66 | { 67 | if (token == _source.Version && _source.GetStatus(token) != ValueTaskSourceStatus.Pending) 68 | { 69 | Monitor.Pulse(SyncLock); 70 | } 71 | } 72 | } 73 | 74 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 75 | public ValueTaskSourceStatus GetStatus(short token) => _source.GetStatus(token); 76 | 77 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 78 | public void OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) 79 | => _source.OnCompleted(continuation, state, token, flags); 80 | 81 | private object SyncLock 82 | { 83 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 84 | get => this; 85 | } 86 | 87 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 88 | public bool TrySetException(Exception error, short token) 89 | { 90 | if (token == _source.Version) 91 | { 92 | 93 | switch (_source.GetStatus(token)) 94 | { 95 | case ValueTaskSourceStatus.Pending: 96 | _source.SetException(error); 97 | // only need to signal if SetException didn't inline a handler 98 | if (token == _source.Version) SignalResult(token); 99 | return true; 100 | } 101 | } 102 | return false; 103 | } 104 | 105 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 106 | public bool TrySetResult(T result, short token) 107 | { 108 | if (token == _source.Version) 109 | { 110 | switch (_source.GetStatus(token)) 111 | { 112 | case ValueTaskSourceStatus.Pending: 113 | _source.SetResult(result); 114 | // only need to signal if SetResult didn't inline a handler 115 | if (token == _source.Version) SignalResult(token); 116 | return true; 117 | } 118 | } 119 | return false; 120 | } 121 | 122 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 123 | public bool TrySetCanceled(short token) 124 | => TrySetException(TaskUtils.SharedTaskCanceledException, token); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/PooledAwait/Internal/StateMachineBox.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace PooledAwait.Internal 7 | { 8 | internal sealed class StateMachineBox 9 | #if PLAT_THREADPOOLWORKITEM 10 | : IThreadPoolWorkItem 11 | #endif 12 | where TStateMachine : IAsyncStateMachine 13 | { 14 | private readonly Action _execute; 15 | private TStateMachine _stateMachine; 16 | 17 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 18 | private StateMachineBox() 19 | { 20 | _stateMachine = default!; 21 | _execute = Execute; 22 | Counters.StateMachineBoxAllocated.Increment(); 23 | } 24 | 25 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 26 | private static StateMachineBox Create(ref TStateMachine stateMachine) 27 | { 28 | var box = Pool>.TryGet() ?? new StateMachineBox(); 29 | box._stateMachine = stateMachine; 30 | return box; 31 | } 32 | 33 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 34 | public static void AwaitOnCompleted( 35 | ref TAwaiter awaiter, ref TStateMachine stateMachine) 36 | where TAwaiter : INotifyCompletion 37 | { 38 | var box = Create(ref stateMachine); 39 | if (typeof(TAwaiter) == typeof(YieldAwaitable.YieldAwaiter)) 40 | { 41 | Yield(box, true); 42 | } 43 | else 44 | { 45 | awaiter.OnCompleted(box._execute); 46 | } 47 | } 48 | 49 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 50 | public static void AwaitUnsafeOnCompleted( 51 | ref TAwaiter awaiter, ref TStateMachine stateMachine) 52 | where TAwaiter : ICriticalNotifyCompletion 53 | { 54 | var box = Create(ref stateMachine); 55 | if (typeof(TAwaiter) == typeof(YieldAwaitable.YieldAwaiter)) 56 | { 57 | Yield(box, false); 58 | } 59 | else 60 | { 61 | awaiter.UnsafeOnCompleted(box._execute); 62 | } 63 | } 64 | 65 | private static void Yield(StateMachineBox box, bool flowContext) 66 | { 67 | // heavily inspired by YieldAwaitable.QueueContinuation 68 | 69 | var syncContext = SynchronizationContext.Current; 70 | if (syncContext != null && syncContext.GetType() != typeof(SynchronizationContext)) 71 | { 72 | syncContext.Post(s_SendOrPostCallback, box); 73 | } 74 | else 75 | { 76 | var taskScheduler = TaskScheduler.Current; 77 | if (!ReferenceEquals(taskScheduler, TaskScheduler.Default)) 78 | { 79 | Task.Factory.StartNew(box._execute, default, TaskCreationOptions.PreferFairness, taskScheduler); 80 | } 81 | else if (flowContext) 82 | { 83 | ThreadPool.QueueUserWorkItem(s_WaitCallback, box); 84 | } 85 | else 86 | { 87 | #if PLAT_THREADPOOLWORKITEM 88 | ThreadPool.UnsafeQueueUserWorkItem(box, false); 89 | #elif NETSTANDARD1_3 90 | ThreadPool.QueueUserWorkItem(s_WaitCallback, box); 91 | #else 92 | ThreadPool.UnsafeQueueUserWorkItem(s_WaitCallback, box); 93 | #endif 94 | } 95 | } 96 | } 97 | 98 | static readonly SendOrPostCallback s_SendOrPostCallback = state => ((StateMachineBox?)state!)?.Execute(); 99 | static readonly WaitCallback s_WaitCallback = state => ((StateMachineBox?)state)?.Execute(); 100 | 101 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 102 | public void Execute() 103 | { 104 | // extract the state 105 | var tmp = _stateMachine; 106 | 107 | // recycle the instance 108 | _stateMachine = default!; 109 | Pool>.TryPut(this); 110 | Counters.StateMachineBoxRecycled.Increment(); 111 | 112 | // progress the state machine 113 | tmp.MoveNext(); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/PooledAwait/Internal/TaskUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.CompilerServices; 4 | using System.Threading.Tasks; 5 | using System.Threading.Tasks.Sources; 6 | 7 | namespace PooledAwait.Internal 8 | { 9 | // NET45 lacks some useful Task APIs; shim over them 10 | internal static class TaskUtils 11 | { 12 | internal static readonly short InitialTaskSourceVersion = new ManualResetValueTaskSourceCore().Version; 13 | 14 | public static readonly TaskCanceledException SharedTaskCanceledException = new TaskCanceledException(); 15 | #if NET45 16 | public static readonly Task CompletedTask = TaskFactory.FromResult(default); 17 | 18 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 19 | public static Task FromException(Exception exception) 20 | { 21 | var source = ValueTaskCompletionSource.Create(); 22 | source.TrySetException(exception); 23 | return source.Task; 24 | } 25 | 26 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 27 | public static Task FromException(Exception exception) => FromException(exception); 28 | #else 29 | public static readonly Task CompletedTask = Task.CompletedTask; 30 | 31 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 32 | public static Task FromException(Exception exception) => Task.FromException(exception); 33 | 34 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 35 | public static Task FromException(Exception exception) => Task.FromException(exception); 36 | #endif 37 | 38 | 39 | internal static class TaskFactory 40 | { 41 | // draws from AsyncMethodBuilder, but less boxing 42 | 43 | public static readonly Task Canceled = CreateCanceled(); 44 | 45 | static Task CreateCanceled() 46 | { 47 | var source = ValueTaskCompletionSource.Create(); 48 | source.TrySetCanceled(); 49 | return source.Task; 50 | } 51 | 52 | private static readonly TaskCache _cache = (TaskCache)CreateCacheForType(); 53 | 54 | private static object CreateCacheForType() 55 | { 56 | if (typeof(TResult) == typeof(Nothing)) return new NothingTaskCache(); 57 | if (typeof(TResult) == typeof(int)) return new Int32TaskCache(); 58 | if (typeof(TResult) == typeof(int?)) return new NullableInt32TaskCache(); 59 | if (typeof(TResult) == typeof(bool)) return new BooleanTaskCache(); 60 | if (typeof(TResult) == typeof(bool?)) return new NullableBooleanTaskCache(); 61 | 62 | Type underlyingType = Nullable.GetUnderlyingType(typeof(TResult)) ?? typeof(TResult); 63 | if (underlyingType == typeof(uint) 64 | || underlyingType == typeof(byte) 65 | || underlyingType == typeof(sbyte) 66 | || underlyingType == typeof(char) 67 | || underlyingType == typeof(decimal) 68 | || underlyingType == typeof(long) 69 | || underlyingType == typeof(ulong) 70 | || underlyingType == typeof(short) 71 | || underlyingType == typeof(ushort) 72 | || underlyingType == typeof(float) 73 | || underlyingType == typeof(double) 74 | || underlyingType == typeof(IntPtr) 75 | || underlyingType == typeof(UIntPtr) 76 | ) return new DefaultEquatableTaskCache(); 77 | 78 | if (typeof(TResult) == typeof(string)) return new StringTaskCache(); 79 | #if !NETSTANDARD1_3 80 | if (!typeof(TResult).IsValueType) return new ObjectTaskCache(); 81 | #endif 82 | return new TaskCache(); 83 | } 84 | 85 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 86 | public static Task FromResult(TResult result) => _cache.FromResult(result); 87 | } 88 | 89 | class TaskCache 90 | { 91 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 92 | internal virtual Task FromResult(TResult value) => Task.FromResult(value); 93 | } 94 | class NothingTaskCache : TaskCache 95 | { 96 | private static readonly Task s_Instance = Task.FromResult(default); 97 | internal override Task FromResult(Nothing value) => s_Instance; 98 | } 99 | class DefaultEquatableTaskCache : TaskCache 100 | { 101 | private static readonly Task s_Default = Task.FromResult(default!); 102 | private static readonly EqualityComparer _comparer = EqualityComparer.Default; 103 | internal override Task FromResult(TResult value) 104 | => _comparer.Equals(value, default!) ? s_Default : base.FromResult(value); 105 | } 106 | class ObjectTaskCache : TaskCache 107 | { 108 | private static readonly Task s_Null = Task.FromResult(default!); 109 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 110 | internal override Task FromResult(TResult value) 111 | => value == null ? s_Null : base.FromResult(value); 112 | } 113 | sealed class StringTaskCache : ObjectTaskCache 114 | { 115 | private static readonly Task s_Empty = Task.FromResult(""); 116 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 117 | internal override Task FromResult(string value) 118 | => value == "" ? s_Empty : base.FromResult(value); 119 | } 120 | sealed class BooleanTaskCache : TaskCache 121 | { 122 | static readonly Task s_True = Task.FromResult(true), s_False = Task.FromResult(false); 123 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 124 | internal override Task FromResult(bool value) => value ? s_True : s_False; 125 | } 126 | sealed class NullableBooleanTaskCache : TaskCache 127 | { 128 | static readonly Task s_True = Task.FromResult((bool?)true), s_False = Task.FromResult((bool?)false), 129 | s_Null = Task.FromResult((bool?)null); 130 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 131 | internal override Task FromResult(bool? value) => 132 | value.HasValue ? (value.GetValueOrDefault() ? s_True : s_False) : s_Null; 133 | } 134 | sealed class Int32TaskCache : TaskCache 135 | { 136 | const int MIN_INC = -1, MAX_EXC = 11; 137 | static readonly Task[] s_Known = CreateKnown(); 138 | static Task[] CreateKnown() 139 | { 140 | var arr = new Task[MAX_EXC - MIN_INC]; 141 | for (int i = 0; i < arr.Length; i++) 142 | arr[i] = Task.FromResult(i + MIN_INC); 143 | return arr; 144 | } 145 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 146 | internal override Task FromResult(int value) 147 | => value >= MIN_INC && value < MAX_EXC ? s_Known[value - MIN_INC] : base.FromResult(value); 148 | } 149 | sealed class NullableInt32TaskCache : TaskCache 150 | { 151 | const int MIN_INC = -1, MAX_EXC = 11; 152 | static readonly Task[] s_Known = CreateKnown(); 153 | static readonly Task s_Null = Task.FromResult((int?)null); 154 | static Task[] CreateKnown() 155 | { 156 | var arr = new Task[MAX_EXC - MIN_INC]; 157 | for (int i = 0; i < arr.Length; i++) 158 | arr[i] = Task.FromResult((int?)(i + MIN_INC)); 159 | return arr; 160 | } 161 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 162 | internal override Task FromResult(int? nullable) 163 | { 164 | if (nullable.HasValue) 165 | { 166 | int value = nullable.GetValueOrDefault(); 167 | return value >= MIN_INC && value < MAX_EXC ? s_Known[value - MIN_INC] : base.FromResult(nullable); 168 | } 169 | return s_Null; 170 | } 171 | } 172 | 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/PooledAwait/Internal/ThrowHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using System.Threading.Tasks; 4 | 5 | namespace PooledAwait.Internal 6 | { 7 | internal static class ThrowHelper 8 | { 9 | [MethodImpl(MethodImplOptions.NoInlining)] 10 | internal static void ThrowInvalidOperationException(string? message = null) 11 | { 12 | if (string.IsNullOrWhiteSpace(message)) throw new InvalidOperationException(); 13 | else throw new InvalidOperationException(message); 14 | } 15 | 16 | [MethodImpl(MethodImplOptions.NoInlining)] 17 | internal static T ThrowInvalidOperationException(string? message = null) 18 | { 19 | ThrowInvalidOperationException(message); 20 | return default!; 21 | } 22 | 23 | [MethodImpl(MethodImplOptions.NoInlining)] 24 | internal static T ThrowNotSupportedException() => throw new NotSupportedException(); 25 | 26 | [MethodImpl(MethodImplOptions.NoInlining)] 27 | internal static void ThrowArgumentNullException(string paramName) => throw new ArgumentNullException(paramName); 28 | 29 | [MethodImpl(MethodImplOptions.NoInlining)] 30 | internal static void ThrowTaskCanceledException() => throw new TaskCanceledException(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/PooledAwait/LazyTaskCompletionSource.cs: -------------------------------------------------------------------------------- 1 | using PooledAwait.Internal; 2 | using System; 3 | using System.Runtime.CompilerServices; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace PooledAwait 8 | { 9 | /// 10 | /// Like a ValueTaskCompletionSource, but the actual task will only become allocated 11 | /// if the .Task is consumed; this is useful for APIs where the consumer *may* consume a task 12 | /// 13 | public readonly struct LazyTaskCompletionSource : IDisposable 14 | { 15 | 16 | private static LazyTaskCompletionSource _completed, _canceled; 17 | 18 | /// 19 | /// A global LazyTaskCompletionSource that represents a completed operation 20 | /// 21 | public static LazyTaskCompletionSource CompletedTask 22 | { 23 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 24 | get => _completed.IsValid ? _completed : _completed = new LazyTaskCompletionSource(LazyTaskState.CreateConstant(default)); 25 | } 26 | 27 | /// 28 | /// A global LazyTaskCompletionSource that represents a cancelled operation 29 | /// 30 | public static LazyTaskCompletionSource CanceledTask 31 | { 32 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 33 | get => _canceled.IsValid ? _canceled : _canceled = new LazyTaskCompletionSource(LazyTaskState.CreateCanceled()); 34 | } 35 | 36 | /// 37 | public override bool Equals(object? obj) => obj is LazyTaskCompletionSource ltcs && _state == ltcs._state && _token == ltcs._token; 38 | /// 39 | public override int GetHashCode() => (_state == null ? 0 : _state.GetHashCode()) ^ _token; 40 | /// 41 | public override string ToString() => nameof(LazyTaskCompletionSource); 42 | 43 | private readonly LazyTaskState _state; 44 | private readonly short _token; 45 | 46 | /// 47 | /// Gets the task associated with this instance 48 | /// 49 | public Task Task => _state?.GetTask(_token) ?? ThrowHelper.ThrowInvalidOperationException(); 50 | 51 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 52 | private LazyTaskCompletionSource(LazyTaskState state) 53 | { 54 | _state = state; 55 | _token = state.Version; 56 | } 57 | 58 | /// 59 | /// Create a new instance; this instance should be disposed when it is known to be unwanted 60 | /// 61 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 62 | public static LazyTaskCompletionSource Create() 63 | => new LazyTaskCompletionSource(LazyTaskState.Create()); 64 | 65 | /// 66 | /// Set the result of the operation 67 | /// 68 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 69 | public bool TrySetResult() => _state != null && _state.TrySetResult(_token, default); 70 | 71 | /// 72 | /// Set the result of the operation 73 | /// 74 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 75 | public bool TrySetCanceled(CancellationToken cancellationToken = default) => _state != null && _state.TrySetCanceled(_token, cancellationToken); 76 | 77 | /// 78 | /// Set the result of the operation 79 | /// 80 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 81 | public void SetResult() 82 | { 83 | if (!TrySetResult()) ThrowHelper.ThrowInvalidOperationException(); 84 | } 85 | 86 | /// 87 | /// Set the result of the operation 88 | /// 89 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 90 | public void SetCanceled(CancellationToken cancellationToken = default) 91 | { 92 | if (!TrySetCanceled(cancellationToken)) ThrowHelper.ThrowInvalidOperationException(); 93 | } 94 | 95 | /// 96 | /// Set the result of the operation 97 | /// 98 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 99 | public bool TrySetException(Exception exception) => _state != null && _state.TrySetException(_token, exception); 100 | 101 | /// 102 | /// Set the result of the operation 103 | /// 104 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 105 | public void SetException(Exception exception) 106 | { 107 | if (!TrySetException(exception)) ThrowHelper.ThrowInvalidOperationException(); 108 | } 109 | 110 | /// 111 | /// Release all resources associated with this operation 112 | /// 113 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 114 | public void Dispose() => _state?.Recycle(_token); 115 | 116 | internal bool IsValid => _state != null && _state.IsValid(_token); 117 | internal bool HasSource => _state != null && _state.HasSource; 118 | internal bool HasTask => _state != null && _state.HasTask; 119 | 120 | /// 121 | /// Indicates whether this is an invalid default instance 122 | /// 123 | public bool IsNull 124 | { 125 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 126 | get =>_state == null; 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/PooledAwait/LazyTaskCompletionSourceT.cs: -------------------------------------------------------------------------------- 1 | using PooledAwait.Internal; 2 | using System; 3 | using System.Runtime.CompilerServices; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace PooledAwait 8 | { 9 | /// 10 | /// Like a ValueTaskCompletionSource, but the actual task will only become allocated 11 | /// if the .Task is consumed; this is useful for APIs where the consumer *may* consume a task 12 | /// 13 | public readonly struct LazyTaskCompletionSource : IDisposable 14 | { 15 | /// 16 | public override bool Equals(object? obj) => obj is LazyTaskCompletionSource ltcs && _state == ltcs._state && _token == ltcs._token; 17 | /// 18 | public override int GetHashCode() => (_state == null ? 0 : _state.GetHashCode()) ^ _token; 19 | /// 20 | public override string ToString() => nameof(LazyTaskCompletionSource); 21 | 22 | private readonly LazyTaskState _state; 23 | private readonly short _token; 24 | 25 | /// 26 | /// Gets the task associated with this instance 27 | /// 28 | public Task Task => (Task)(_state?.GetTask(_token) ?? ThrowHelper.ThrowInvalidOperationException>()); 29 | 30 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 31 | private LazyTaskCompletionSource(LazyTaskState state) 32 | { 33 | _state = state; 34 | _token = state.Version; 35 | } 36 | 37 | /// 38 | /// Create a new instance; this instance should be disposed when it is known to be unwanted 39 | /// 40 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 41 | public static LazyTaskCompletionSource Create() 42 | => new LazyTaskCompletionSource(LazyTaskState.Create()); 43 | 44 | /// 45 | /// Create a new instance; this instance will never by recycled 46 | /// 47 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 48 | public static LazyTaskCompletionSource CreateConstant(T value) 49 | => new LazyTaskCompletionSource(LazyTaskState.CreateConstant(value)); 50 | 51 | 52 | private static LazyTaskCompletionSource _canceled; 53 | 54 | /// 55 | /// A global LazyTaskCompletionSource that represents a cancelled operation 56 | /// 57 | public static LazyTaskCompletionSource CanceledTask 58 | { 59 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 60 | get => _canceled.IsValid ? _canceled : _canceled = new LazyTaskCompletionSource(LazyTaskState.CreateCanceled()); 61 | } 62 | 63 | /// 64 | /// Set the result of the operation 65 | /// 66 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 67 | public bool TrySetResult(T result) => _state != null && _state.TrySetResult(_token, result); 68 | 69 | /// 70 | /// Set the result of the operation 71 | /// 72 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 73 | public void SetResult(T result) 74 | { 75 | if (!TrySetResult(result)) ThrowHelper.ThrowInvalidOperationException(); 76 | } 77 | 78 | /// 79 | /// Set the result of the operation 80 | /// 81 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 82 | public bool TrySetCanceled(CancellationToken cancellationToken = default) 83 | => _state != null && _state.TrySetCanceled(_token, cancellationToken); 84 | 85 | /// 86 | /// Set the result of the operation 87 | /// 88 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 89 | public void SetCanceled(CancellationToken cancellationToken = default) 90 | { 91 | if (!TrySetCanceled(cancellationToken)) ThrowHelper.ThrowInvalidOperationException(); 92 | } 93 | 94 | /// 95 | /// Set the result of the operation 96 | /// 97 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 98 | public bool TrySetException(Exception exception) => _state != null && _state.TrySetException(_token, exception); 99 | 100 | /// 101 | /// Set the result of the operation 102 | /// 103 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 104 | public void SetException(Exception exception) 105 | { 106 | if (!TrySetException(exception)) ThrowHelper.ThrowInvalidOperationException(); 107 | } 108 | 109 | /// 110 | /// Release all resources associated with this operation 111 | /// 112 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 113 | public void Dispose() => _state?.Recycle(_token); 114 | 115 | internal bool IsValid => _state != null && _state.IsValid(_token); 116 | internal bool HasSource => _state != null && _state.HasSource; 117 | internal bool HasTask => _state != null && _state.HasTask; 118 | 119 | /// 120 | /// Indicates whether this is an invalid default instance 121 | /// 122 | public bool IsNull 123 | { 124 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 125 | get => _state == null; 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/PooledAwait/MethodBuilders/FireAndForgetMethodBuilder.cs: -------------------------------------------------------------------------------- 1 | using PooledAwait.Internal; 2 | using System; 3 | using System.ComponentModel; 4 | using System.Runtime.CompilerServices; 5 | 6 | #pragma warning disable CS1591 7 | 8 | namespace PooledAwait.MethodBuilders 9 | { 10 | /// 11 | /// This type is not intended for direct usage 12 | /// 13 | [Browsable(false)] 14 | [EditorBrowsable(EditorBrowsableState.Never)] 15 | public struct FireAndForgetMethodBuilder 16 | { 17 | public override bool Equals(object? obj) => ThrowHelper.ThrowNotSupportedException(); 18 | public override int GetHashCode() => ThrowHelper.ThrowNotSupportedException(); 19 | public override string ToString() => nameof(FireAndForgetMethodBuilder); 20 | 21 | [Browsable(false)] 22 | [EditorBrowsable(EditorBrowsableState.Never)] 23 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 24 | public static FireAndForgetMethodBuilder Create() => default; 25 | 26 | [Browsable(false)] 27 | [EditorBrowsable(EditorBrowsableState.Never)] 28 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 29 | public void SetStateMachine(IAsyncStateMachine _) => Counters.SetStateMachine.Increment(); 30 | 31 | [Browsable(false)] 32 | [EditorBrowsable(EditorBrowsableState.Never)] 33 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 34 | public void SetResult() { } 35 | 36 | [Browsable(false)] 37 | [EditorBrowsable(EditorBrowsableState.Never)] 38 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 39 | public void SetException(Exception exception) => FireAndForget.OnException(exception); 40 | 41 | [Browsable(false)] 42 | [EditorBrowsable(EditorBrowsableState.Never)] 43 | public FireAndForget Task 44 | { 45 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 46 | get => default; 47 | } 48 | 49 | [Browsable(false)] 50 | [EditorBrowsable(EditorBrowsableState.Never)] 51 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 52 | public void AwaitOnCompleted( 53 | ref TAwaiter awaiter, ref TStateMachine stateMachine) 54 | where TAwaiter : INotifyCompletion 55 | where TStateMachine : IAsyncStateMachine 56 | => StateMachineBox.AwaitOnCompleted(ref awaiter, ref stateMachine); 57 | 58 | [Browsable(false)] 59 | [EditorBrowsable(EditorBrowsableState.Never)] 60 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 61 | public void AwaitUnsafeOnCompleted( 62 | ref TAwaiter awaiter, ref TStateMachine stateMachine) 63 | where TAwaiter : ICriticalNotifyCompletion 64 | where TStateMachine : IAsyncStateMachine 65 | => StateMachineBox.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); 66 | 67 | [Browsable(false)] 68 | [EditorBrowsable(EditorBrowsableState.Never)] 69 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 70 | public void Start(ref TStateMachine stateMachine) 71 | where TStateMachine : IAsyncStateMachine => stateMachine.MoveNext(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/PooledAwait/MethodBuilders/PooledTaskMethodBuilder.cs: -------------------------------------------------------------------------------- 1 | using PooledAwait.Internal; 2 | using System; 3 | using System.ComponentModel; 4 | using System.Runtime.CompilerServices; 5 | using SystemTask = System.Threading.Tasks.Task; 6 | 7 | #pragma warning disable CS1591 8 | 9 | namespace PooledAwait.MethodBuilders 10 | { 11 | /// 12 | /// This type is not intended for direct usage 13 | /// 14 | [Browsable(false)] 15 | [EditorBrowsable(EditorBrowsableState.Never)] 16 | public struct PooledTaskMethodBuilder 17 | { 18 | public override bool Equals(object? obj) => ThrowHelper.ThrowNotSupportedException(); 19 | public override int GetHashCode() => ThrowHelper.ThrowNotSupportedException(); 20 | public override string ToString() => nameof(PooledTaskMethodBuilder); 21 | 22 | private ValueTaskCompletionSource _source; 23 | private Exception _exception; 24 | 25 | [Browsable(false)] 26 | [EditorBrowsable(EditorBrowsableState.Never)] 27 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 28 | public static PooledTaskMethodBuilder Create() => default; 29 | 30 | [Browsable(false)] 31 | [EditorBrowsable(EditorBrowsableState.Never)] 32 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 33 | public void SetStateMachine(IAsyncStateMachine _) => Counters.SetStateMachine.Increment(); 34 | 35 | [Browsable(false)] 36 | [EditorBrowsable(EditorBrowsableState.Never)] 37 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 38 | public void SetResult() 39 | { 40 | _source.TrySetResult(default); 41 | } 42 | 43 | [Browsable(false)] 44 | [EditorBrowsable(EditorBrowsableState.Never)] 45 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 46 | public void SetException(Exception exception) 47 | { 48 | _source.TrySetException(exception); 49 | _exception = exception; 50 | } 51 | 52 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 53 | private void EnsureHasTask() 54 | { 55 | if (_source.IsNull) _source = ValueTaskCompletionSource.Create(); 56 | } 57 | 58 | [Browsable(false)] 59 | [EditorBrowsable(EditorBrowsableState.Never)] 60 | public PooledTask Task 61 | { 62 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 63 | get 64 | { 65 | SystemTask task; 66 | if (!_source.IsNull) task = _source.Task; 67 | else if (_exception is OperationCanceledException) task = TaskUtils.TaskFactory.Canceled; 68 | else if (_exception != null) task = TaskUtils.FromException(_exception); 69 | else task = TaskUtils.CompletedTask; 70 | return new PooledTask(task); 71 | } 72 | } 73 | 74 | [Browsable(false)] 75 | [EditorBrowsable(EditorBrowsableState.Never)] 76 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 77 | public void AwaitOnCompleted( 78 | ref TAwaiter awaiter, ref TStateMachine stateMachine) 79 | where TAwaiter : INotifyCompletion 80 | where TStateMachine : IAsyncStateMachine 81 | { 82 | EnsureHasTask(); 83 | StateMachineBox.AwaitOnCompleted(ref awaiter, ref stateMachine); 84 | } 85 | 86 | [Browsable(false)] 87 | [EditorBrowsable(EditorBrowsableState.Never)] 88 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 89 | public void AwaitUnsafeOnCompleted( 90 | ref TAwaiter awaiter, ref TStateMachine stateMachine) 91 | where TAwaiter : ICriticalNotifyCompletion 92 | where TStateMachine : IAsyncStateMachine 93 | { 94 | EnsureHasTask(); 95 | StateMachineBox.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); 96 | } 97 | 98 | [Browsable(false)] 99 | [EditorBrowsable(EditorBrowsableState.Never)] 100 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 101 | public void Start(ref TStateMachine stateMachine) 102 | where TStateMachine : IAsyncStateMachine => stateMachine.MoveNext(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/PooledAwait/MethodBuilders/PooledTaskMethodBuilderT.cs: -------------------------------------------------------------------------------- 1 | using PooledAwait.Internal; 2 | using System; 3 | using System.ComponentModel; 4 | using System.Runtime.CompilerServices; 5 | using System.Threading.Tasks; 6 | using SystemTask = System.Threading.Tasks.Task; 7 | 8 | #pragma warning disable CS1591 9 | 10 | namespace PooledAwait.MethodBuilders 11 | { 12 | /// 13 | /// This type is not intended for direct usage 14 | /// 15 | [Browsable(false)] 16 | [EditorBrowsable(EditorBrowsableState.Never)] 17 | public struct PooledTaskMethodBuilder 18 | { 19 | public override bool Equals(object? obj) => ThrowHelper.ThrowNotSupportedException(); 20 | public override int GetHashCode() => ThrowHelper.ThrowNotSupportedException(); 21 | public override string ToString() => nameof(PooledTaskMethodBuilder); 22 | 23 | private ValueTaskCompletionSource _source; 24 | private Exception _exception; 25 | private T _result; 26 | 27 | [Browsable(false)] 28 | [EditorBrowsable(EditorBrowsableState.Never)] 29 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 30 | public static PooledTaskMethodBuilder Create() => default; 31 | 32 | [Browsable(false)] 33 | [EditorBrowsable(EditorBrowsableState.Never)] 34 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 35 | public void SetStateMachine(IAsyncStateMachine _) => Counters.SetStateMachine.Increment(); 36 | 37 | [Browsable(false)] 38 | [EditorBrowsable(EditorBrowsableState.Never)] 39 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 40 | public void SetResult(T result) 41 | { 42 | _source.TrySetResult(result); 43 | _result = result; 44 | } 45 | 46 | [Browsable(false)] 47 | [EditorBrowsable(EditorBrowsableState.Never)] 48 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 49 | public void SetException(Exception exception) 50 | { 51 | _source.TrySetException(exception); 52 | _exception = exception; 53 | } 54 | 55 | [Browsable(false)] 56 | [EditorBrowsable(EditorBrowsableState.Never)] 57 | public PooledTask Task 58 | { 59 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 60 | get 61 | { 62 | Task task; 63 | if (!_source.IsNull) task = _source.Task; 64 | else if (_exception is OperationCanceledException) task = TaskUtils.TaskFactory.Canceled; 65 | else if (_exception != null) task = TaskUtils.FromException(_exception); 66 | else task = TaskUtils.TaskFactory.FromResult(_result); 67 | return new PooledTask(task); 68 | } 69 | } 70 | 71 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 72 | private void EnsureHasTask() 73 | { 74 | if (_source.IsNull) _source = ValueTaskCompletionSource.Create(); 75 | } 76 | 77 | [Browsable(false)] 78 | [EditorBrowsable(EditorBrowsableState.Never)] 79 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 80 | public void AwaitOnCompleted( 81 | ref TAwaiter awaiter, ref TStateMachine stateMachine) 82 | where TAwaiter : INotifyCompletion 83 | where TStateMachine : IAsyncStateMachine 84 | { 85 | EnsureHasTask(); 86 | StateMachineBox.AwaitOnCompleted(ref awaiter, ref stateMachine); 87 | } 88 | 89 | [Browsable(false)] 90 | [EditorBrowsable(EditorBrowsableState.Never)] 91 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 92 | public void AwaitUnsafeOnCompleted( 93 | ref TAwaiter awaiter, ref TStateMachine stateMachine) 94 | where TAwaiter : ICriticalNotifyCompletion 95 | where TStateMachine : IAsyncStateMachine 96 | { 97 | EnsureHasTask(); 98 | StateMachineBox.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); 99 | } 100 | 101 | [Browsable(false)] 102 | [EditorBrowsable(EditorBrowsableState.Never)] 103 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 104 | public void Start(ref TStateMachine stateMachine) 105 | where TStateMachine : IAsyncStateMachine => stateMachine.MoveNext(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/PooledAwait/MethodBuilders/PooledValueTaskMethodBuilder.cs: -------------------------------------------------------------------------------- 1 | using PooledAwait.Internal; 2 | using System; 3 | using System.ComponentModel; 4 | using System.Runtime.CompilerServices; 5 | 6 | #pragma warning disable CS1591 7 | 8 | namespace PooledAwait.MethodBuilders 9 | { 10 | /// 11 | /// This type is not intended for direct usage 12 | /// 13 | [Browsable(false)] 14 | [EditorBrowsable(EditorBrowsableState.Never)] 15 | public struct PooledValueTaskMethodBuilder 16 | { 17 | public override bool Equals(object? obj) => ThrowHelper.ThrowNotSupportedException(); 18 | public override int GetHashCode() => ThrowHelper.ThrowNotSupportedException(); 19 | public override string ToString() => nameof(PooledValueTaskMethodBuilder); 20 | 21 | private PooledValueTaskSource _source; 22 | 23 | [Browsable(false)] 24 | [EditorBrowsable(EditorBrowsableState.Never)] 25 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 26 | public static PooledValueTaskMethodBuilder Create() => default; 27 | 28 | [Browsable(false)] 29 | [EditorBrowsable(EditorBrowsableState.Never)] 30 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 31 | public void SetStateMachine(IAsyncStateMachine _) => Counters.SetStateMachine.Increment(); 32 | 33 | [Browsable(false)] 34 | [EditorBrowsable(EditorBrowsableState.Never)] 35 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 36 | public void SetResult() 37 | { 38 | _source.TrySetResult(); 39 | } 40 | 41 | [Browsable(false)] 42 | [EditorBrowsable(EditorBrowsableState.Never)] 43 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 44 | public void SetException(Exception exception) 45 | { 46 | EnsureHasTask(); 47 | _source.TrySetException(exception); 48 | } 49 | 50 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 51 | private void EnsureHasTask() 52 | { 53 | if (!_source.HasTask) _source = PooledValueTaskSource.Create(); 54 | } 55 | 56 | [Browsable(false)] 57 | [EditorBrowsable(EditorBrowsableState.Never)] 58 | public PooledValueTask Task 59 | { 60 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 61 | get => _source.PooledTask; 62 | } 63 | 64 | [Browsable(false)] 65 | [EditorBrowsable(EditorBrowsableState.Never)] 66 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 67 | public void AwaitOnCompleted( 68 | ref TAwaiter awaiter, ref TStateMachine stateMachine) 69 | where TAwaiter : INotifyCompletion 70 | where TStateMachine : IAsyncStateMachine 71 | { 72 | EnsureHasTask(); 73 | StateMachineBox.AwaitOnCompleted(ref awaiter, ref stateMachine); 74 | } 75 | 76 | [Browsable(false)] 77 | [EditorBrowsable(EditorBrowsableState.Never)] 78 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 79 | public void AwaitUnsafeOnCompleted( 80 | ref TAwaiter awaiter, ref TStateMachine stateMachine) 81 | where TAwaiter : ICriticalNotifyCompletion 82 | where TStateMachine : IAsyncStateMachine 83 | { 84 | EnsureHasTask(); 85 | StateMachineBox.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); 86 | } 87 | 88 | [Browsable(false)] 89 | [EditorBrowsable(EditorBrowsableState.Never)] 90 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 91 | public void Start(ref TStateMachine stateMachine) 92 | where TStateMachine : IAsyncStateMachine => stateMachine.MoveNext(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/PooledAwait/MethodBuilders/PooledValueTaskMethodBuilderT.cs: -------------------------------------------------------------------------------- 1 | using PooledAwait.Internal; 2 | using System; 3 | using System.ComponentModel; 4 | using System.Runtime.CompilerServices; 5 | 6 | #pragma warning disable CS1591 7 | 8 | namespace PooledAwait.MethodBuilders 9 | { 10 | /// 11 | /// This type is not intended for direct usage 12 | /// 13 | [Browsable(false)] 14 | [EditorBrowsable(EditorBrowsableState.Never)] 15 | public struct PooledValueTaskMethodBuilder 16 | { 17 | public override bool Equals(object? obj) => ThrowHelper.ThrowNotSupportedException(); 18 | public override int GetHashCode() => ThrowHelper.ThrowNotSupportedException(); 19 | public override string ToString() => nameof(PooledValueTaskMethodBuilder); 20 | 21 | private PooledValueTaskSource _source; 22 | 23 | [Browsable(false)] 24 | [EditorBrowsable(EditorBrowsableState.Never)] 25 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 26 | public static PooledValueTaskMethodBuilder Create() => default; 27 | 28 | [Browsable(false)] 29 | [EditorBrowsable(EditorBrowsableState.Never)] 30 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 31 | public void SetStateMachine(IAsyncStateMachine _) => Counters.SetStateMachine.Increment(); 32 | 33 | [Browsable(false)] 34 | [EditorBrowsable(EditorBrowsableState.Never)] 35 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 36 | public void SetResult(T result) 37 | { 38 | if (_source.HasTask) _source.TrySetResult(result); 39 | else _source = new PooledValueTaskSource(result); 40 | } 41 | 42 | [Browsable(false)] 43 | [EditorBrowsable(EditorBrowsableState.Never)] 44 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 45 | public void SetException(Exception exception) 46 | { 47 | EnsureHasTask(); 48 | _source.TrySetException(exception); 49 | } 50 | 51 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 52 | private void EnsureHasTask() 53 | { 54 | if (!_source.HasTask) _source = PooledValueTaskSource.Create(); 55 | } 56 | 57 | [Browsable(false)] 58 | [EditorBrowsable(EditorBrowsableState.Never)] 59 | public PooledValueTask Task 60 | { 61 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 62 | get => _source.PooledTask; 63 | } 64 | 65 | [Browsable(false)] 66 | [EditorBrowsable(EditorBrowsableState.Never)] 67 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 68 | public void AwaitOnCompleted( 69 | ref TAwaiter awaiter, ref TStateMachine stateMachine) 70 | where TAwaiter : INotifyCompletion 71 | where TStateMachine : IAsyncStateMachine 72 | { 73 | EnsureHasTask(); 74 | StateMachineBox.AwaitOnCompleted(ref awaiter, ref stateMachine); 75 | } 76 | 77 | [Browsable(false)] 78 | [EditorBrowsable(EditorBrowsableState.Never)] 79 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 80 | public void AwaitUnsafeOnCompleted( 81 | ref TAwaiter awaiter, ref TStateMachine stateMachine) 82 | where TAwaiter : ICriticalNotifyCompletion 83 | where TStateMachine : IAsyncStateMachine 84 | { 85 | EnsureHasTask(); 86 | StateMachineBox.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); 87 | } 88 | 89 | [Browsable(false)] 90 | [EditorBrowsable(EditorBrowsableState.Never)] 91 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 92 | public void Start(ref TStateMachine stateMachine) 93 | where TStateMachine : IAsyncStateMachine => stateMachine.MoveNext(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/PooledAwait/Pool.cs: -------------------------------------------------------------------------------- 1 | using PooledAwait.Internal; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace PooledAwait 5 | { 6 | /// 7 | /// Utility methods for boxing value types efficiently, in particular for 8 | /// avoid boxes and capture contexts in callbacks 9 | /// 10 | public static class Pool 11 | { 12 | /// 13 | /// Gets an instance from the pool if possible 14 | /// 15 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 16 | public static T? TryRent() where T : class 17 | => Pool.TryGet(); 18 | 19 | /// 20 | /// Puts an instance back into the pool 21 | /// 22 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 23 | public static void Return(T value) where T : class 24 | { 25 | if (value is IResettable reset) reset.Reset(); 26 | Pool.TryPut(value); 27 | } 28 | 29 | /// 30 | /// Wraps a value-type into a boxed instance, using an object pool; 31 | /// consider using value-tuples in particular 32 | /// 33 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 34 | public static object Box(in T value) where T : struct 35 | => ItemBox.Create(in value); 36 | 37 | /// 38 | /// Unwraps a value-type from a boxed instance and recycles 39 | /// the instance, which should not be touched again 40 | /// 41 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 42 | public static T UnboxAndReturn(object obj) where T : struct 43 | => ItemBox.UnboxAndRecycle(obj); 44 | 45 | internal sealed class ItemBox where T : struct 46 | { 47 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 48 | private ItemBox() => Counters.ItemBoxAllocated.Increment(); 49 | 50 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 51 | public static ItemBox Create(in T value) 52 | { 53 | var box = Pool>.TryGet() ?? new ItemBox(); 54 | box._value = value; 55 | return box; 56 | } 57 | 58 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 59 | public static T UnboxAndRecycle(object obj) 60 | { 61 | var box = (ItemBox)obj; 62 | var value = box._value; 63 | box._value = default; 64 | Pool>.TryPut(box); 65 | return value; 66 | } 67 | #pragma warning disable IDE0044 // make field readonly? no, IDE, you're wrong 68 | private T _value; 69 | #pragma warning restore IDE0044 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/PooledAwait/PoolSizeAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | #if !NETSTANDARD1_3 4 | namespace PooledAwait 5 | { 6 | /// 7 | /// Controls the number of elements to store in the pool 8 | /// 9 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = true)] 10 | public sealed class PoolSizeAttribute : Attribute 11 | { 12 | /// 13 | /// The number of elements to store in the pool 14 | /// 15 | public int Size { get; } 16 | 17 | /// 18 | /// Create a new PoolSizeAttribute instance 19 | /// 20 | public PoolSizeAttribute(int size) => Size = size; 21 | } 22 | } 23 | #endif 24 | -------------------------------------------------------------------------------- /src/PooledAwait/PoolT.cs: -------------------------------------------------------------------------------- 1 | using PooledAwait.Internal; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Runtime.CompilerServices; 6 | using System.Threading; 7 | 8 | namespace PooledAwait 9 | { 10 | /// 11 | /// A general-purpose pool of object references; it is the caller's responsibility 12 | /// to ensure that overlapped usage does not occur 13 | /// 14 | internal static class Pool where T : class 15 | { 16 | internal static readonly int Size = CalculateSize(); 17 | private static readonly Queue _queue = new Queue(Size); 18 | private volatile static int _countEstimate; 19 | 20 | [ThreadStatic] 21 | private static T? ts_local; 22 | 23 | static int CalculateSize() 24 | { 25 | const int DefaultSize = 16; 26 | int size = DefaultSize; 27 | 28 | var type = typeof(T); 29 | if (type == typeof(object)) ThrowHelper.ThrowInvalidOperationException("Pool is not supported; please use a more specific type"); 30 | 31 | #if !NETSTANDARD1_3 32 | const int MinSize = 0, MaxSize = 256; 33 | 34 | if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Pool.ItemBox<>)) 35 | { // if doing a boxed T, tell us about the T - not the box 36 | type = type.GetGenericArguments()[0]; 37 | } 38 | 39 | var attrib = (PoolSizeAttribute?)Attribute.GetCustomAttribute(type, typeof(PoolSizeAttribute), true); 40 | if (attrib != null) 41 | { 42 | if (attrib.Size < MinSize) size = MinSize; 43 | else if (attrib.Size > MaxSize) size = MaxSize; 44 | else size = attrib.Size; 45 | } 46 | #endif 47 | return size; 48 | } 49 | 50 | internal static int Count() 51 | { 52 | lock (_queue) 53 | { 54 | return _queue.Count; 55 | } 56 | } 57 | 58 | /// 59 | /// Gets an instance from the pool if possible 60 | /// 61 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 62 | public static T? TryGet() 63 | { 64 | var tmp = ts_local; 65 | ts_local = null; 66 | return tmp ?? FromPool(); 67 | 68 | static T? FromPool() 69 | { 70 | if (_countEstimate != 0) 71 | { 72 | lock (_queue) 73 | { 74 | int count = _queue.Count; 75 | if (count != 0) 76 | { 77 | _countEstimate = count - 1; 78 | return _queue.Dequeue(); 79 | } 80 | } 81 | } 82 | return null; 83 | } 84 | } 85 | 86 | /// 87 | /// Puts an instance back into the pool 88 | /// 89 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 90 | public static void TryPut(T value) 91 | { 92 | if (value != null) 93 | { 94 | if (ts_local == null) 95 | { 96 | ts_local = value; 97 | return; 98 | } 99 | ToPool(value); 100 | } 101 | static void ToPool(T value) 102 | { 103 | if (_countEstimate < Size) 104 | { 105 | lock (_queue) 106 | { 107 | int count = _queue.Count; 108 | if (count < Size) 109 | { 110 | _countEstimate = count + 1; 111 | _queue.Enqueue(value); 112 | } 113 | } 114 | } 115 | } 116 | } 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/PooledAwait/PooledAwait.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.0;netstandard2.1;netstandard2.0;net45;net461;netstandard1.3 5 | true 6 | false 7 | false 8 | PooledAwait 9 | true 10 | 11 | 12 | 13 | true 14 | 15 | 16 | true 17 | 18 | 19 | $(DefineConstants);PLAT_MRVTSC 20 | 21 | 22 | 23 | $(DefineConstants);PLAT_MRVTSC 24 | true 25 | 26 | 27 | 28 | $(DefineConstants);PLAT_MRVTSC 29 | true 30 | 31 | 32 | 33 | $(DefineConstants);PLAT_THREADPOOLWORKITEM;PLAT_MRVTSC 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/PooledAwait/PooledTask.cs: -------------------------------------------------------------------------------- 1 | using PooledAwait.Internal; 2 | using System; 3 | using System.Runtime.CompilerServices; 4 | using System.Threading.Tasks; 5 | 6 | namespace PooledAwait 7 | { 8 | /// 9 | /// A Task, but with a custom builder 10 | /// 11 | [AsyncMethodBuilder(typeof(MethodBuilders.PooledTaskMethodBuilder))] 12 | public readonly struct PooledTask 13 | { 14 | /// 15 | public override bool Equals(object? obj) => obj is PooledTask pt && _task == pt._task; 16 | /// 17 | public override int GetHashCode() => _task == null ? 0 : _task.GetHashCode(); 18 | /// 19 | public override string ToString() => nameof(PooledTask); 20 | 21 | private readonly Task _task; 22 | 23 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 24 | internal PooledTask(Task task) => _task = task; 25 | 26 | /// 27 | /// Gets the instance as a task 28 | /// 29 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 30 | public Task AsTask() => _task ?? TaskUtils.CompletedTask; 31 | 32 | /// 33 | /// Gets the instance as a task 34 | /// 35 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 36 | 37 | public static implicit operator Task(in PooledTask task) => task.AsTask(); 38 | 39 | /// 40 | /// Gets the awaiter for the task 41 | /// 42 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 43 | public TaskAwaiter GetAwaiter() => AsTask().GetAwaiter(); 44 | 45 | /// 46 | /// Gets the configured awaiter for the task 47 | /// 48 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 49 | public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext) 50 | => AsTask().ConfigureAwait(continueOnCapturedContext); 51 | 52 | /// 53 | /// Indicates whether this is an invalid default instance 54 | /// 55 | public bool IsNull 56 | { 57 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 58 | get => _task == null; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/PooledAwait/PooledTaskT.cs: -------------------------------------------------------------------------------- 1 | using PooledAwait.Internal; 2 | using System; 3 | using System.Runtime.CompilerServices; 4 | using System.Threading.Tasks; 5 | 6 | namespace PooledAwait 7 | { 8 | /// 9 | /// A Task, but with a custom builder 10 | /// 11 | [AsyncMethodBuilder(typeof(MethodBuilders.PooledTaskMethodBuilder<>))] 12 | public readonly struct PooledTask 13 | { 14 | /// 15 | public override bool Equals(object? obj) => obj is PooledTask pt && _task == pt._task; 16 | /// 17 | public override int GetHashCode() => _task == null ? 0 : _task.GetHashCode(); 18 | /// 19 | public override string ToString() => nameof(PooledTask); 20 | 21 | private readonly Task? _task; 22 | 23 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 24 | internal PooledTask(Task task) => _task = task; 25 | 26 | /// 27 | /// Gets the instance as a task 28 | /// 29 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 30 | public Task AsTask() => _task ?? ThrowHelper.ThrowInvalidOperationException>(); 31 | 32 | /// 33 | /// Gets the instance as a task 34 | /// 35 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 36 | 37 | public static implicit operator Task(in PooledTask task) => task.AsTask(); 38 | 39 | /// 40 | /// Gets the awaiter for the task 41 | /// 42 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 43 | public TaskAwaiter GetAwaiter() => AsTask().GetAwaiter(); 44 | 45 | /// 46 | /// Gets the configured awaiter for the task 47 | /// 48 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 49 | public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext) 50 | => AsTask().ConfigureAwait(continueOnCapturedContext); 51 | 52 | /// 53 | /// Indicates whether this is an invalid default instance 54 | /// 55 | public bool IsNull 56 | { 57 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 58 | get => _task == null; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/PooledAwait/PooledValueTask.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using System.Threading.Tasks; 4 | using System.Threading.Tasks.Sources; 5 | 6 | namespace PooledAwait 7 | { 8 | /// 9 | /// A ValueTask with a custom source and builder 10 | /// 11 | [AsyncMethodBuilder(typeof(MethodBuilders.PooledValueTaskMethodBuilder))] 12 | public readonly struct PooledValueTask 13 | { 14 | /// 15 | public override bool Equals(object? obj) => obj is PooledValueTask pvt && _source == pvt._source && _token == pvt._token; 16 | /// 17 | public override int GetHashCode() => (_source == null ? 0 : _source.GetHashCode()) ^ _token; 18 | /// 19 | public override string ToString() => nameof(PooledValueTask); 20 | 21 | private readonly IValueTaskSource _source; 22 | private readonly short _token; 23 | 24 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 25 | internal PooledValueTask(IValueTaskSource source, short token) 26 | { 27 | _source = source; 28 | _token = token; 29 | } 30 | 31 | /// 32 | /// Gets the instance as a value-task 33 | /// 34 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 35 | public ValueTask AsValueTask() => _source == null ? default : new ValueTask(_source, _token); 36 | 37 | /// 38 | /// Gets the instance as a value-task 39 | /// 40 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 41 | public static implicit operator ValueTask(in PooledValueTask task) => task.AsValueTask(); 42 | 43 | /// 44 | /// Gets the awaiter for the task 45 | /// 46 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 47 | public ValueTaskAwaiter GetAwaiter() => AsValueTask().GetAwaiter(); 48 | 49 | /// 50 | /// Gets the configured awaiter for the task 51 | /// 52 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 53 | public ConfiguredValueTaskAwaitable ConfigureAwait(bool continueOnCapturedContext) 54 | => AsValueTask().ConfigureAwait(continueOnCapturedContext); 55 | 56 | /// 57 | /// Rents a task-source that will be recycled when the task is awaited 58 | /// 59 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 60 | internal static PooledValueTaskSource CreateSource() => PooledValueTaskSource.Create(); 61 | 62 | /// 63 | /// Indicates whether this is an invalid default instance 64 | /// 65 | public bool IsNull 66 | { 67 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 68 | get => _source == null; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/PooledAwait/PooledValueTaskSource.cs: -------------------------------------------------------------------------------- 1 | using PooledAwait.Internal; 2 | using System; 3 | using System.Runtime.CompilerServices; 4 | using System.Threading.Tasks; 5 | 6 | namespace PooledAwait 7 | { 8 | /// 9 | /// A task-source that automatically recycles when the task is awaited 10 | /// 11 | public readonly struct PooledValueTaskSource 12 | { 13 | /// 14 | public override bool Equals(object? obj) => obj is PooledValueTaskSource pvt && _source == pvt._source && _token == pvt._token; 15 | /// 16 | public override int GetHashCode() => (_source == null ? 0 : _source.GetHashCode()) ^ _token; 17 | /// 18 | public override string ToString() => nameof(PooledValueTaskSource); 19 | 20 | /// 21 | /// Gets the task that corresponds to this instance; it can only be awaited once 22 | /// 23 | public ValueTask Task 24 | { 25 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 26 | get => _source == null ? default : new ValueTask(_source, _token); 27 | } 28 | 29 | /// 30 | /// Indicates whether this instance is well-defined against a value task instance 31 | /// 32 | public bool HasTask 33 | { 34 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 35 | get => _source != null; 36 | } 37 | 38 | internal PooledValueTask PooledTask 39 | { 40 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 41 | get => new PooledValueTask(_source, _token); 42 | } 43 | 44 | /// 45 | /// Rents a task-source that will be recycled when the task is awaited 46 | /// 47 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 48 | public static PooledValueTaskSource Create() 49 | { 50 | var source = PooledState.Create(out var token); 51 | return new PooledValueTaskSource(source, token); 52 | } 53 | 54 | private readonly PooledState _source; 55 | private readonly short _token; 56 | 57 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 58 | internal PooledValueTaskSource(PooledState source, short token) 59 | { 60 | _source = source; 61 | _token = token; 62 | } 63 | 64 | /// 65 | /// Test whether the source is valid 66 | /// 67 | public bool IsValid 68 | { 69 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 70 | get => _source != null && _source.IsValid(_token); 71 | } 72 | 73 | /// 74 | /// Set the result of the operation 75 | /// 76 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 77 | public bool TrySetResult() => _source != null && _source.TrySetResult(default, _token); 78 | 79 | /// 80 | /// Set the result of the operation 81 | /// 82 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 83 | public void SetResult() 84 | { 85 | if (!TrySetResult()) ThrowHelper.ThrowInvalidOperationException(); 86 | } 87 | 88 | /// 89 | /// Set the result of the operation 90 | /// 91 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 92 | public bool TrySetException(Exception error) => _source != null && _source.TrySetException(error, _token); 93 | 94 | /// 95 | /// Set the result of the operation 96 | /// 97 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 98 | public void SetException(Exception exception) 99 | { 100 | if (!TrySetException(exception)) ThrowHelper.ThrowInvalidOperationException(); 101 | } 102 | 103 | /// 104 | /// Set the result of the operation 105 | /// 106 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 107 | public bool TrySetCanceled() => _source != null && _source.TrySetCanceled(_token); 108 | 109 | /// 110 | /// Set the result of the operation 111 | /// 112 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 113 | public void SetCanceled() 114 | { 115 | if (!TrySetCanceled()) ThrowHelper.ThrowInvalidOperationException(); 116 | } 117 | 118 | /// 119 | /// Indicates whether this is an invalid default instance 120 | /// 121 | public bool IsNull 122 | { 123 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 124 | get => _source == null; 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/PooledAwait/PooledValueTaskSourceT.cs: -------------------------------------------------------------------------------- 1 | using PooledAwait.Internal; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Runtime.CompilerServices; 5 | using System.Threading.Tasks; 6 | 7 | namespace PooledAwait 8 | { 9 | /// 10 | /// A task-source that automatically recycles when the task is awaited 11 | /// 12 | public readonly struct PooledValueTaskSource 13 | { 14 | /// 15 | public override bool Equals(object? obj) => obj is PooledValueTaskSource pvt && _token == pvt._token && 16 | (_source != null ? _source == pvt._source : (pvt._source == null && EqualityComparer.Default.Equals(_value, pvt._value))); 17 | /// 18 | public override int GetHashCode() => (_source == null ? EqualityComparer.Default.GetHashCode(_value) : _source.GetHashCode()) ^ _token; 19 | /// 20 | public override string ToString() => nameof(PooledValueTaskSource); 21 | 22 | /// 23 | /// Gets the task that corresponds to this instance; it can only be awaited once 24 | /// 25 | public ValueTask Task 26 | { 27 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 28 | get => _source == null ? new ValueTask(_value) : new ValueTask(_source, _token); 29 | } 30 | 31 | /// 32 | /// Indicates whether this instance is well-defined against a value task instance 33 | /// 34 | public bool HasTask 35 | { 36 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 37 | get => _source != null; 38 | } 39 | 40 | internal PooledValueTask PooledTask 41 | { 42 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 43 | get => _source == null ? new PooledValueTask(_value) : new PooledValueTask(_source, _token); 44 | } 45 | 46 | /// 47 | /// Rents a task-source that will be recycled when the task is awaited 48 | /// 49 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 50 | public static PooledValueTaskSource Create() 51 | { 52 | var source = PooledState.Create(out var token); 53 | return new PooledValueTaskSource(source, token); 54 | } 55 | 56 | private readonly PooledState? _source; 57 | private readonly short _token; 58 | private readonly T _value; 59 | 60 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 61 | internal PooledValueTaskSource(PooledState source, short token) 62 | { 63 | _source = source; 64 | _token = token; 65 | _value = default!; 66 | } 67 | 68 | /// 69 | /// Create a new PooledValueTaskSource that will yield a constant value without ever renting/recycling any background state 70 | /// 71 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 72 | public PooledValueTaskSource(T value) 73 | { 74 | _source = null; 75 | _token = default; 76 | _value = value!; 77 | } 78 | 79 | /// 80 | /// Test whether the source is valid 81 | /// 82 | public bool IsValid 83 | { 84 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 85 | get => _source != null && _source.IsValid(_token); 86 | } 87 | 88 | /// 89 | /// Set the result of the operation 90 | /// 91 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 92 | public bool TrySetResult(T result) => _source != null && _source.TrySetResult(result, _token); 93 | 94 | /// 95 | /// Set the result of the operation 96 | /// 97 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 98 | public void SetResult(T result) 99 | { 100 | if (!TrySetResult(result)) ThrowHelper.ThrowInvalidOperationException(); 101 | } 102 | 103 | /// 104 | /// Set the result of the operation 105 | /// 106 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 107 | public bool TrySetException(Exception error) => _source != null && _source.TrySetException(error, _token); 108 | 109 | /// 110 | /// Set the result of the operation 111 | /// 112 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 113 | public void SetException(Exception exception) 114 | { 115 | if (!TrySetException(exception)) ThrowHelper.ThrowInvalidOperationException(); 116 | } 117 | 118 | /// 119 | /// Set the result of the operation 120 | /// 121 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 122 | public bool TrySetCanceled() => _source != null && _source.TrySetCanceled(_token); 123 | 124 | /// 125 | /// Set the result of the operation 126 | /// 127 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 128 | public void SetCanceled() 129 | { 130 | if (!TrySetCanceled()) ThrowHelper.ThrowInvalidOperationException(); 131 | } 132 | 133 | /// 134 | /// Indicates whether this is an invalid default instance 135 | /// 136 | public bool IsNull 137 | { 138 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 139 | get => _source == null; 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/PooledAwait/PooledValueTaskT.cs: -------------------------------------------------------------------------------- 1 | using PooledAwait.Internal; 2 | using System; 3 | using System.Runtime.CompilerServices; 4 | using System.Threading.Tasks; 5 | 6 | namespace PooledAwait 7 | { 8 | /// 9 | /// A ValueTask with a custom source and builder 10 | /// 11 | [AsyncMethodBuilder(typeof(MethodBuilders.PooledValueTaskMethodBuilder<>))] 12 | public readonly struct PooledValueTask 13 | { 14 | /// 15 | public override bool Equals(object? obj) => obj is PooledValueTask pvt && _source == pvt._source && _token == pvt._token; 16 | /// 17 | public override int GetHashCode() => (_source == null ? 0 : _source.GetHashCode()) ^ _token; 18 | /// 19 | public override string ToString() => nameof(PooledValueTask); 20 | 21 | private readonly PooledState? _source; 22 | private readonly short _token; 23 | private readonly T _result; 24 | 25 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 26 | internal PooledValueTask(PooledState source, short token) 27 | { 28 | _source = source; 29 | _token = token; 30 | _result = default!; 31 | } 32 | 33 | /// 34 | /// Creates a value-task with a fixed value 35 | /// 36 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 37 | public PooledValueTask(T result) 38 | { 39 | _source = default; 40 | _token = default; 41 | _result = result; 42 | } 43 | 44 | /// 45 | /// Gets the instance as a value-task 46 | /// 47 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 48 | public ValueTask AsValueTask() => _source == null ? new ValueTask(_result) : new ValueTask(_source, _token); 49 | 50 | /// 51 | /// Gets the instance as a value-task 52 | /// 53 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 54 | public static implicit operator ValueTask(in PooledValueTask task) => task.AsValueTask(); 55 | 56 | /// 57 | /// Gets the awaiter for the task 58 | /// 59 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 60 | public ValueTaskAwaiter GetAwaiter() => AsValueTask().GetAwaiter(); 61 | 62 | /// 63 | /// Gets the configured awaiter for the task 64 | /// 65 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 66 | public ConfiguredValueTaskAwaitable ConfigureAwait(bool continueOnCapturedContext) 67 | => AsValueTask().ConfigureAwait(continueOnCapturedContext); 68 | 69 | /// 70 | /// Rents a task-source that will be recycled when the task is awaited 71 | /// 72 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 73 | internal static PooledValueTaskSource CreateSource() => PooledValueTaskSource.Create(); 74 | 75 | /// 76 | /// Indicates whether this is an invalid default instance 77 | /// 78 | public bool IsNull 79 | { 80 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 81 | get => _source == null; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/PooledAwait/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("Benchmark, PublicKey=0024000004800000940000000602000000240000525341310004000001000100d136a87a0c11ded39332f7ead1f2ce53256a42a350ee847df1dc520bc781210a5f1fe73b3f91a4001fdcfa35340a212e70443de46e6928510f57a55f4ab9b95257f5067d4de4be8cdad460781866b20a6ca20f66302df61b52e55e16c080cad95aae8b94ffdd54718b9963d0240b0d0d5afe655b05c91771f7dc8a314387d3c3")] 4 | 5 | [assembly: InternalsVisibleTo("PooledAwait.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100d136a87a0c11ded39332f7ead1f2ce53256a42a350ee847df1dc520bc781210a5f1fe73b3f91a4001fdcfa35340a212e70443de46e6928510f57a55f4ab9b95257f5067d4de4be8cdad460781866b20a6ca20f66302df61b52e55e16c080cad95aae8b94ffdd54718b9963d0240b0d0d5afe655b05c91771f7dc8a314387d3c3")] -------------------------------------------------------------------------------- /src/PooledAwait/ValueTaskCompletionSourceT.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using PooledAwait.Internal; 6 | 7 | #if !NETSTANDARD1_3 8 | using System.Reflection; 9 | #endif 10 | 11 | namespace PooledAwait 12 | { 13 | /// 14 | /// Lightweight implementation of TaskCompletionSource 15 | /// 16 | /// When possible, this will bypass TaskCompletionSource completely 17 | public readonly struct ValueTaskCompletionSource 18 | { 19 | /// 20 | public override bool Equals(object? obj) => obj is ValueTaskCompletionSource other && _state == other._state; 21 | /// 22 | public override int GetHashCode() => _state == null ? 0 : _state.GetHashCode(); 23 | /// 24 | public override string ToString() => "ValueTaskCompletionSource"; 25 | 26 | #if !NETSTANDARD1_3 27 | private static readonly Func, Exception, bool>? s_TrySetException = TryCreate(nameof(TrySetException)); 28 | private static readonly Func, T, bool>? s_TrySetResult = TryCreate(nameof(TrySetResult)); 29 | private static readonly Func, CancellationToken, bool>? s_TrySetCanceled = TryCreate(nameof(TrySetCanceled)); 30 | private static readonly bool s_Optimized = ValidateOptimized(); 31 | #endif 32 | private readonly object _state; 33 | 34 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 35 | private ValueTaskCompletionSource(object state) => _state = state; 36 | 37 | /// 38 | /// Gets the instance as a task 39 | /// 40 | public Task Task 41 | { 42 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 43 | get => _state as Task ?? ((TaskCompletionSource)_state).Task; 44 | } 45 | 46 | /// 47 | /// Indicates whether this is an invalid default instance 48 | /// 49 | public bool IsNull 50 | { 51 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 52 | get => _state == null; 53 | } 54 | 55 | internal bool IsOptimized => _state is Task; 56 | 57 | /// 58 | /// Create an instance pointing to a new task 59 | /// 60 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 61 | public static ValueTaskCompletionSource Create() => 62 | #if !NETSTANDARD1_3 63 | s_Optimized ? CreateOptimized() : 64 | #endif 65 | CreateFallback(); 66 | 67 | #if !NETSTANDARD1_3 68 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 69 | internal static ValueTaskCompletionSource CreateOptimized() 70 | { 71 | Counters.TaskAllocated.Increment(); 72 | return new ValueTaskCompletionSource( 73 | System.Runtime.Serialization.FormatterServices.GetUninitializedObject(typeof(Task))); 74 | } 75 | #endif 76 | 77 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 78 | internal static ValueTaskCompletionSource CreateFallback() 79 | { 80 | Counters.TaskAllocated.Increment(); 81 | return new ValueTaskCompletionSource(new TaskCompletionSource()); 82 | } 83 | 84 | /// 85 | /// Set the outcome of the operation 86 | /// 87 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 88 | public bool TrySetException(Exception exception) 89 | { 90 | #if !NETSTANDARD1_3 91 | if (_state is Task task) 92 | { 93 | var result = s_TrySetException!(task, exception); 94 | if (!result && !task.IsCompleted) SpinUntilCompleted(task); 95 | return result; 96 | } 97 | #endif 98 | return _state != null && ((TaskCompletionSource)_state).TrySetException(exception); 99 | } 100 | 101 | /// 102 | /// Set the result of the operation 103 | /// 104 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 105 | public void SetException(Exception exception) 106 | { 107 | if (!TrySetException(exception)) ThrowHelper.ThrowInvalidOperationException(); 108 | } 109 | 110 | /// 111 | /// Set the outcome of the operation 112 | /// 113 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 114 | public bool TrySetCanceled(CancellationToken cancellationToken = default) 115 | { 116 | #if !NETSTANDARD1_3 117 | if (_state is Task task) 118 | { 119 | var result = s_TrySetCanceled!(task, cancellationToken); 120 | if (!result && !task.IsCompleted) SpinUntilCompleted(task); 121 | return result; 122 | } 123 | #endif 124 | return _state != null && ((TaskCompletionSource)_state).TrySetCanceled( 125 | #if !NET45 126 | cancellationToken 127 | #endif 128 | ); 129 | } 130 | 131 | /// 132 | /// Set the result of the operation 133 | /// 134 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 135 | public void SetCanceled(CancellationToken cancellationToken = default) 136 | { 137 | if (!TrySetCanceled(cancellationToken)) ThrowHelper.ThrowInvalidOperationException(); 138 | } 139 | 140 | /// 141 | /// Set the outcome of the operation 142 | /// 143 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 144 | public bool TrySetResult(T value) 145 | { 146 | #if !NETSTANDARD1_3 147 | if (_state is Task task) 148 | { 149 | var result = s_TrySetResult!(task, value); 150 | if (!result && !task.IsCompleted) SpinUntilCompleted(task); 151 | return result; 152 | } 153 | #endif 154 | return _state != null && ((TaskCompletionSource)_state).TrySetResult(value); 155 | } 156 | 157 | /// 158 | /// Set the result of the operation 159 | /// 160 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 161 | public void SetResult(T value) 162 | { 163 | if (!TrySetResult(value)) ThrowHelper.ThrowInvalidOperationException(); 164 | } 165 | 166 | #if !NETSTANDARD1_3 167 | [MethodImpl(MethodImplOptions.NoInlining)] 168 | private static Func, TArg, bool>? TryCreate(string methodName) 169 | { 170 | try 171 | { 172 | return (Func, TArg, bool>)Delegate.CreateDelegate( 173 | typeof(Func, TArg, bool>), 174 | typeof(Task).GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, 175 | null, new[] { typeof(TArg) }, null)!); 176 | } 177 | catch { return null; } 178 | } 179 | 180 | [MethodImpl(MethodImplOptions.NoInlining)] 181 | private static bool ValidateOptimized() 182 | { 183 | try 184 | { 185 | // perform feature tests of our voodoo 186 | var source = CreateOptimized(); 187 | var task = source.Task; 188 | if (task == null) return false; 189 | if (task.IsCompleted) return false; 190 | 191 | if (!source.TrySetResult(default!)) return false; 192 | if (task.Status != TaskStatus.RanToCompletion) return false; 193 | 194 | source = CreateOptimized(); 195 | task = source.Task; 196 | if (!source.TrySetException(new InvalidOperationException())) return false; 197 | if (!task.IsCompleted) return false; 198 | if (!task.IsFaulted) return false; 199 | try 200 | { 201 | _ = task.Result; 202 | return false; 203 | } 204 | catch (AggregateException ex) when (ex.InnerException is InvalidOperationException) { } 205 | if (!(task.Exception?.InnerException is InvalidOperationException)) return false; 206 | return true; 207 | } 208 | catch { return false; } 209 | } 210 | 211 | [MethodImpl(MethodImplOptions.NoInlining)] 212 | private void SpinUntilCompleted(Task task) 213 | { 214 | // Spin wait until the completion is finalized by another thread. 215 | var sw = new SpinWait(); 216 | while (!task.IsCompleted) 217 | sw.SpinOnce(); 218 | } 219 | #endif 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /tests/Benchmark/Benchmark.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp3.1;net472 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/Benchmark/ComparisonBenchmarks.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | using BenchmarkDotNet.Attributes; 4 | using BenchmarkDotNet.Configs; 5 | using BenchmarkDotNet.Jobs; 6 | 7 | using PooledAwait; 8 | 9 | namespace Benchmark 10 | { 11 | [MemoryDiagnoser, SimpleJob(RuntimeMoniker.NetCoreApp31), SimpleJob(RuntimeMoniker.Net48)] 12 | [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] 13 | [CategoriesColumn] 14 | public class ComparisonBenchmarks 15 | { 16 | // note: all the benchmarks use Task/Task for the public API, because BenchmarkDotNet 17 | // doesn't work reliably with more exotic task-types (even just ValueTask fails); instead, 18 | // we'll obscure the cost of the outer awaitable by doing a relatively large number of 19 | // iterations, so that we're only really measuring the inner loop 20 | private const int InnerOps = 1000; 21 | 22 | // [Params(false, true)] 23 | public bool ConfigureAwait { get; set; } = false; 24 | 25 | [Benchmark(OperationsPerInvoke = InnerOps, Description = ".NET")] 26 | [BenchmarkCategory("Task")] 27 | public async Task ViaTaskT() 28 | { 29 | int sum = 0; 30 | for (int i = 0; i < InnerOps; i++) 31 | sum += await Inner(1, 2).ConfigureAwait(ConfigureAwait); 32 | return sum; 33 | 34 | static async Task Inner(int x, int y) 35 | { 36 | int i = x; 37 | await Task.Yield(); 38 | i *= y; 39 | await Task.Yield(); 40 | return 5 * i; 41 | } 42 | } 43 | 44 | [Benchmark(OperationsPerInvoke = InnerOps, Description = ".NET")] 45 | [BenchmarkCategory("Task")] 46 | public async Task ViaTask() 47 | { 48 | for (int i = 0; i < InnerOps; i++) 49 | await Inner().ConfigureAwait(ConfigureAwait); 50 | 51 | static async Task Inner() 52 | { 53 | await Task.Yield(); 54 | await Task.Yield(); 55 | } 56 | } 57 | 58 | [Benchmark(OperationsPerInvoke = InnerOps, Description = ".NET")] 59 | [BenchmarkCategory("ValueTask")] 60 | public async Task ViaValueTaskT() 61 | { 62 | int sum = 0; 63 | for (int i = 0; i < InnerOps; i++) 64 | sum += await Inner(1, 2).ConfigureAwait(ConfigureAwait); 65 | return sum; 66 | 67 | static async ValueTask Inner(int x, int y) 68 | { 69 | int i = x; 70 | await Task.Yield(); 71 | i *= y; 72 | await Task.Yield(); 73 | return 5 * i; 74 | } 75 | } 76 | 77 | [Benchmark(OperationsPerInvoke = InnerOps, Description = ".NET")] 78 | [BenchmarkCategory("ValueTask")] 79 | public async Task ViaValueTask() 80 | { 81 | for (int i = 0; i < InnerOps; i++) 82 | await Inner().ConfigureAwait(ConfigureAwait); 83 | 84 | static async ValueTask Inner() 85 | { 86 | await Task.Yield(); 87 | await Task.Yield(); 88 | } 89 | } 90 | 91 | [Benchmark(OperationsPerInvoke = InnerOps, Description = "Pooled")] 92 | [BenchmarkCategory("ValueTask")] 93 | public async Task ViaPooledValueTaskT() 94 | { 95 | int sum = 0; 96 | for (int i = 0; i < InnerOps; i++) 97 | sum += await Inner(1, 2).ConfigureAwait(ConfigureAwait); 98 | return sum; 99 | 100 | static async PooledValueTask Inner(int x, int y) 101 | { 102 | int i = x; 103 | await Task.Yield(); 104 | i *= y; 105 | await Task.Yield(); 106 | return 5 * i; 107 | } 108 | } 109 | 110 | [Benchmark(OperationsPerInvoke = InnerOps, Description = "Pooled")] 111 | [BenchmarkCategory("ValueTask")] 112 | public async Task ViaPooledValueTask() 113 | { 114 | for (int i = 0; i < InnerOps; i++) 115 | await Inner().ConfigureAwait(ConfigureAwait); 116 | 117 | static async PooledValueTask Inner() 118 | { 119 | await Task.Yield(); 120 | await Task.Yield(); 121 | } 122 | } 123 | 124 | [Benchmark(OperationsPerInvoke = InnerOps, Description = "Pooled")] 125 | [BenchmarkCategory("Task")] 126 | public async Task ViaPooledTaskT() 127 | { 128 | int sum = 0; 129 | for (int i = 0; i < InnerOps; i++) 130 | sum += await Inner(1, 2).ConfigureAwait(ConfigureAwait); 131 | return sum; 132 | 133 | static async PooledTask Inner(int x, int y) 134 | { 135 | int i = x; 136 | await Task.Yield(); 137 | i *= y; 138 | await Task.Yield(); 139 | return 5 * i; 140 | } 141 | } 142 | 143 | [Benchmark(OperationsPerInvoke = InnerOps, Description = "Pooled")] 144 | [BenchmarkCategory("Task")] 145 | public async Task ViaPooledTask() 146 | { 147 | for (int i = 0; i < InnerOps; i++) 148 | await Inner().ConfigureAwait(ConfigureAwait); 149 | 150 | static async PooledTask Inner() 151 | { 152 | await Task.Yield(); 153 | await Task.Yield(); 154 | } 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /tests/Benchmark/Program.cs: -------------------------------------------------------------------------------- 1 |  2 | 3 | #if DEBUG 4 | using System; 5 | using System.Threading.Tasks; 6 | using PooledAwait.Internal; 7 | #else 8 | using BenchmarkDotNet.Running; 9 | #endif 10 | 11 | namespace Benchmark 12 | { 13 | public static class Program 14 | { 15 | #if DEBUG 16 | static async Task Main() 17 | { 18 | var obj = new ComparisonBenchmarks(); 19 | 20 | for (int i = 0; i < 100; i++) 21 | await obj.ViaPooledValueTask(); 22 | 23 | Console.WriteLine(nameof(Counters.PooledStateAllocated) + ": " + Counters.PooledStateAllocated); // 2 24 | Console.WriteLine(nameof(Counters.PooledStateRecycled) + ": " + Counters.PooledStateRecycled); // 100100 25 | Console.WriteLine(nameof(Counters.StateMachineBoxAllocated) + ": " + Counters.StateMachineBoxAllocated); // 2 26 | Console.WriteLine(nameof(Counters.StateMachineBoxRecycled) + ": " + Counters.StateMachineBoxRecycled); // 299979 27 | Console.WriteLine(nameof(Counters.SetStateMachine) + ": " + Counters.SetStateMachine); // 0 28 | } 29 | #else 30 | static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); 31 | #endif 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/PooledAwait.Test/BoxTests.cs: -------------------------------------------------------------------------------- 1 | using PooledAwait.Internal; 2 | using System; 3 | using Xunit; 4 | 5 | namespace PooledAwait.Test 6 | { 7 | [Collection("Sequential")] 8 | public class BoxTests 9 | { 10 | [Fact] 11 | public void BoxUnboxWorks() 12 | { 13 | static object Create() 14 | { 15 | int id = 42; 16 | string name = "abc"; 17 | var obj = Pool.Box((id, name)); 18 | return obj; 19 | } 20 | 21 | static void Consume(object obj) 22 | { 23 | (var id, var name) = Pool.UnboxAndReturn<(int, string)>(obj); 24 | Assert.Equal(42, id); 25 | Assert.Equal("abc", name); 26 | } 27 | 28 | Consume(Create()); 29 | Counters.Reset(); 30 | Consume(Create()); 31 | #if DEBUG 32 | Assert.Equal(0, Counters.TotalAllocations); 33 | #endif 34 | } 35 | 36 | [Fact] 37 | public void DefaultPoolSize() => Assert.Equal(16, Pool.Size); 38 | 39 | [Fact] 40 | public void CusomSizeClass() => Assert.Equal(42, Pool.Size); 41 | 42 | [Fact] 43 | public void MinPoolSize() => Assert.Equal(0, Pool.Size); 44 | 45 | [Fact] 46 | public void MaxPoolSize() => Assert.Equal(256, Pool.Size); 47 | 48 | [Fact] 49 | public void CusomStructBoxed() => Assert.Equal(14, Pool>.Size); 50 | 51 | class Default { } 52 | 53 | [PoolSize(42)] 54 | class CustomClass { } 55 | 56 | [PoolSize(14)] 57 | struct CustomStruct { } 58 | 59 | [PoolSize(-12)] 60 | class MinClass { } 61 | 62 | 63 | [PoolSize(12314114)] 64 | class MaxClass { } 65 | 66 | [Fact] 67 | public void CannotUsePoolObject() 68 | { 69 | var ex = Assert.Throws(() => Pool.Size); 70 | Assert.IsType(ex.InnerException); 71 | Assert.Equal("Pool is not supported; please use a more specific type", ex?.InnerException?.Message); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/PooledAwait.Test/CompletedTaskIdentityTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Xunit; 4 | 5 | namespace PooledAwait.Test 6 | { 7 | [Collection("Sequential")] 8 | public class CompletedTaskIdentityTests 9 | { 10 | [Fact] 11 | public void NoType() 12 | { 13 | using var x = LazyTaskCompletionSource.Create(); 14 | Assert.True(x.TrySetResult()); 15 | Assert.Same(x.Task, Task.CompletedTask); 16 | } 17 | 18 | [Theory] 19 | [InlineData(true)] 20 | [InlineData(false)] 21 | public void AllBooleans(bool result) 22 | { 23 | using var x = LazyTaskCompletionSource.Create(); 24 | using var y = LazyTaskCompletionSource.Create(); 25 | Assert.True(x.TrySetResult(result)); 26 | Assert.True(y.TrySetResult(result)); 27 | Assert.Same(x.Task, y.Task); 28 | } 29 | 30 | [Theory] 31 | [InlineData(true)] 32 | [InlineData(false)] 33 | [InlineData(null)] 34 | public void AllNullableBooleans(bool? result) 35 | { 36 | using var x = LazyTaskCompletionSource.Create(); 37 | using var y = LazyTaskCompletionSource.Create(); 38 | Assert.True(x.TrySetResult(result)); 39 | Assert.True(y.TrySetResult(result)); 40 | Assert.Same(x.Task, y.Task); 41 | } 42 | 43 | [Theory] 44 | [InlineData(-1)] 45 | [InlineData(0)] 46 | [InlineData(1)] 47 | [InlineData(2)] 48 | [InlineData(3)] 49 | [InlineData(4)] 50 | [InlineData(5)] 51 | [InlineData(6)] 52 | [InlineData(7)] 53 | [InlineData(8)] 54 | [InlineData(9)] 55 | [InlineData(10)] 56 | public void CommonIntegers(int result) 57 | { 58 | using var x = LazyTaskCompletionSource.Create(); 59 | using var y = LazyTaskCompletionSource.Create(); 60 | Assert.True(x.TrySetResult(result)); 61 | Assert.True(y.TrySetResult(result)); 62 | Assert.Same(x.Task, y.Task); 63 | } 64 | 65 | [Theory] 66 | [InlineData(-2)] 67 | [InlineData(11)] 68 | public void UnommonIntegers(int result) 69 | { 70 | using var x = LazyTaskCompletionSource.Create(); 71 | using var y = LazyTaskCompletionSource.Create(); 72 | Assert.True(x.TrySetResult(result)); 73 | Assert.True(y.TrySetResult(result)); 74 | Assert.NotSame(x.Task, y.Task); 75 | } 76 | 77 | [Theory] 78 | [InlineData(null)] 79 | [InlineData(-1)] 80 | [InlineData(0)] 81 | [InlineData(1)] 82 | [InlineData(2)] 83 | [InlineData(3)] 84 | [InlineData(4)] 85 | [InlineData(5)] 86 | [InlineData(6)] 87 | [InlineData(7)] 88 | [InlineData(8)] 89 | [InlineData(9)] 90 | [InlineData(10)] 91 | public void CommonNullableIntegers(int? result) 92 | { 93 | using var x = LazyTaskCompletionSource.Create(); 94 | using var y = LazyTaskCompletionSource.Create(); 95 | Assert.True(x.TrySetResult(result)); 96 | Assert.True(y.TrySetResult(result)); 97 | Assert.Same(x.Task, y.Task); 98 | } 99 | 100 | [Theory] 101 | [InlineData(-2)] 102 | [InlineData(11)] 103 | public void UnommonNullableIntegers(int? result) 104 | { 105 | using var x = LazyTaskCompletionSource.Create(); 106 | using var y = LazyTaskCompletionSource.Create(); 107 | Assert.True(x.TrySetResult(result)); 108 | Assert.True(y.TrySetResult(result)); 109 | Assert.NotSame(x.Task, y.Task); 110 | } 111 | 112 | [Theory] 113 | [InlineData("")] 114 | [InlineData(null)] 115 | public void CommonStrings(string result) 116 | { 117 | using var x = LazyTaskCompletionSource.Create(); 118 | using var y = LazyTaskCompletionSource.Create(); 119 | Assert.True(x.TrySetResult(result)); 120 | Assert.True(y.TrySetResult(result)); 121 | Assert.Same(x.Task, y.Task); 122 | } 123 | 124 | [Fact] 125 | public void Single() => TestStructDefaultIdentity(); 126 | [Fact] 127 | public void Double() => TestStructDefaultIdentity(); 128 | [Fact] 129 | public void Boolean() => TestStructDefaultIdentity(); 130 | [Fact] 131 | public void Char() => TestStructDefaultIdentity(); 132 | [Fact] 133 | public void SByte() => TestStructDefaultIdentity(); 134 | [Fact] 135 | public void Int16() => TestStructDefaultIdentity(); 136 | [Fact] 137 | public void Int32() => TestStructDefaultIdentity(); 138 | [Fact] 139 | public void Int64() => TestStructDefaultIdentity(); 140 | [Fact] 141 | public void Byte() => TestStructDefaultIdentity(); 142 | [Fact] 143 | public void UInt16() => TestStructDefaultIdentity(); 144 | [Fact] 145 | public void UInt32() => TestStructDefaultIdentity(); 146 | [Fact] 147 | public void UInt64() => TestStructDefaultIdentity(); 148 | [Fact] 149 | public void IntPtr() => TestStructDefaultIdentity(); 150 | [Fact] 151 | public void UIntPtr() => TestStructDefaultIdentity(); 152 | 153 | private static void TestStructDefaultIdentity() where T : struct 154 | { 155 | using (var x = LazyTaskCompletionSource.Create()) 156 | using (var y = LazyTaskCompletionSource.Create()) 157 | { 158 | Assert.True(x.TrySetResult(default)); 159 | Assert.True(y.TrySetResult(default)); 160 | Assert.Same(x.Task, y.Task); 161 | } 162 | 163 | using var nx = LazyTaskCompletionSource.Create(); 164 | using var ny = LazyTaskCompletionSource.Create(); 165 | Assert.True(nx.TrySetResult(default)); 166 | Assert.True(ny.TrySetResult(default)); 167 | Assert.Same(nx.Task, ny.Task); 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /tests/PooledAwait.Test/ComplexAsyncTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | 7 | namespace PooledAwait.Test 8 | { 9 | public class ComplexAsyncTests 10 | { 11 | [Fact] 12 | public async Task HorriblyThreaded_Task() 13 | { 14 | var tasks = new Task[100]; 15 | for (int i = 0; i < tasks.Length; i++) 16 | tasks[i] = Impl(i); 17 | 18 | for(int i = 0; i < tasks.Length; i++) 19 | { 20 | Assert.Equal(i, await tasks[i]); 21 | } 22 | 23 | async static PooledTask Impl(int j) 24 | { 25 | for (int i = 0; i < 50; i++) 26 | { 27 | await Task.Yield(); 28 | } 29 | return j; 30 | } 31 | } 32 | 33 | [Fact] 34 | public async Task HorriblyThreaded_ValueTask() 35 | { 36 | var tasks = new ValueTask[100]; 37 | for (int i = 0; i < tasks.Length; i++) 38 | tasks[i] = Impl(i); 39 | 40 | for (int i = 0; i < tasks.Length; i++) 41 | { 42 | Assert.Equal(i, await tasks[i]); 43 | } 44 | 45 | async static PooledValueTask Impl(int j) 46 | { 47 | for (int i = 0; i < 50; i++) 48 | { 49 | await Task.Yield(); 50 | } 51 | return j; 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/PooledAwait.Test/ContextTests.cs: -------------------------------------------------------------------------------- 1 | #if NETCOREAPP3_0 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Runtime.CompilerServices; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | using Xunit.Abstractions; 9 | 10 | namespace PooledAwait.Test 11 | { 12 | [Collection("Sequential")] 13 | public class ContextTests 14 | { 15 | #pragma warning disable CS8618, CS0649 // uninitialized 16 | private readonly ITestOutputHelper Log; 17 | #pragma warning restore CS8618, CS0649 18 | 19 | // public ContextTests(ITestOutputHelper log) => Log = log; */ 20 | 21 | [Fact] 22 | public async Task SyncContextRespected_Task() 23 | { 24 | using (var ctx = MySyncContext.Create(Log)) 25 | { 26 | Assert.Equal(0, ctx.PostCount); 27 | Assert.Equal(0, ctx.SendCount); 28 | await Impl(); 29 | async Task Impl() 30 | { 31 | await Task.Yield(); 32 | Assert.Same(ctx, SynchronizationContext.Current); 33 | await Task.Yield().ConfigureAwait(true); 34 | Assert.Same(ctx, SynchronizationContext.Current); 35 | await Task.Yield().ConfigureAwait(false); 36 | Assert.Null(SynchronizationContext.Current); 37 | } 38 | Assert.True(ctx.PostCount >= 2 && ctx.PostCount <= 3); 39 | Assert.Equal(0, ctx.SendCount); 40 | } 41 | await Task.Yield(); 42 | } 43 | 44 | [Fact] 45 | public async Task SyncContextRespected_ValueTask() 46 | { 47 | using (var ctx = MySyncContext.Create(Log)) 48 | { 49 | Assert.Equal(0, ctx.PostCount); 50 | Assert.Equal(0, ctx.SendCount); 51 | await Impl(); 52 | async ValueTask Impl() 53 | { 54 | await Task.Yield(); 55 | Assert.Same(ctx, SynchronizationContext.Current); 56 | await Task.Yield().ConfigureAwait(true); 57 | Assert.Same(ctx, SynchronizationContext.Current); 58 | await Task.Yield().ConfigureAwait(false); 59 | Assert.Null(SynchronizationContext.Current); 60 | } 61 | Assert.True(ctx.PostCount >= 2 && ctx.PostCount <= 3); 62 | Assert.Equal(0, ctx.SendCount); 63 | } 64 | await Task.Yield(); 65 | } 66 | 67 | [Fact] 68 | public async Task SyncContextRespected_PooledTask() 69 | { 70 | using (var ctx = MySyncContext.Create(Log)) 71 | { 72 | Assert.Equal(0, ctx.PostCount); 73 | Assert.Equal(0, ctx.SendCount); 74 | await Impl(); 75 | async PooledTask Impl() 76 | { 77 | await Task.Yield(); 78 | Assert.Same(ctx, SynchronizationContext.Current); 79 | await Task.Yield().ConfigureAwait(true); 80 | Assert.Same(ctx, SynchronizationContext.Current); 81 | await Task.Yield().ConfigureAwait(false); 82 | Assert.Null(SynchronizationContext.Current); 83 | } 84 | Assert.True(ctx.PostCount >= 2 && ctx.PostCount <= 3); 85 | Assert.Equal(0, ctx.SendCount); 86 | } 87 | await Task.Yield(); 88 | } 89 | 90 | [Fact] 91 | public async Task SyncContextRespected_PooledValueTask() 92 | { 93 | using (var ctx = MySyncContext.Create(Log)) 94 | { 95 | Assert.Equal(0, ctx.PostCount); 96 | Assert.Equal(0, ctx.SendCount); 97 | await Impl(); 98 | async PooledValueTask Impl() 99 | { 100 | await Task.Yield(); 101 | Assert.Same(ctx, SynchronizationContext.Current); 102 | await Task.Yield().ConfigureAwait(true); 103 | Assert.Same(ctx, SynchronizationContext.Current); 104 | await Task.Yield().ConfigureAwait(false); 105 | Assert.Null(SynchronizationContext.Current); 106 | } 107 | Assert.True(ctx.PostCount >= 2 && ctx.PostCount <= 3); 108 | Assert.Equal(0, ctx.SendCount); 109 | } 110 | await Task.Yield(); 111 | } 112 | 113 | [Fact] 114 | public async Task TaskSchedulerRespected_Task() 115 | { 116 | using (var ctx = MyTaskScheduler.Create(Log)) 117 | { 118 | Assert.Equal(0, ctx.Enqueued); 119 | Assert.Equal(0, ctx.Dequeued); 120 | 121 | Assert.NotSame(TaskScheduler.Default, ctx); 122 | Assert.Same(TaskScheduler.Default, TaskScheduler.Current); 123 | await Task.Factory.StartNew(SyncOverAsync, default, default, ctx); 124 | Assert.Same(TaskScheduler.Default, TaskScheduler.Current); 125 | 126 | void SyncOverAsync() 127 | { 128 | Assert.Same(ctx, TaskScheduler.Current); 129 | Impl().GetAwaiter().GetResult(); 130 | Assert.Same(ctx, TaskScheduler.Current); 131 | } 132 | async Task Impl() 133 | { 134 | Assert.Same(ctx, TaskScheduler.Current); 135 | await Task.Yield(); 136 | Assert.Same(ctx, TaskScheduler.Current); 137 | await Task.Yield().ConfigureAwait(true); 138 | Assert.Same(ctx, TaskScheduler.Current); 139 | await Task.Yield().ConfigureAwait(false); 140 | Assert.Same(TaskScheduler.Default, TaskScheduler.Current); 141 | } 142 | Assert.Equal(ctx.Enqueued, ctx.Dequeued); 143 | Assert.Equal(3, ctx.Enqueued); 144 | } 145 | await Task.Yield(); 146 | } 147 | 148 | [Fact] 149 | public async Task TaskSchedulerRespected_ValueTask() 150 | { 151 | using (var ctx = MyTaskScheduler.Create(Log)) 152 | { 153 | Assert.Equal(0, ctx.Enqueued); 154 | Assert.Equal(0, ctx.Dequeued); 155 | 156 | Assert.NotSame(TaskScheduler.Default, ctx); 157 | Assert.Same(TaskScheduler.Default, TaskScheduler.Current); 158 | await Task.Factory.StartNew(SyncOverAsync, default, default, ctx); 159 | Assert.Same(TaskScheduler.Default, TaskScheduler.Current); 160 | 161 | void SyncOverAsync() 162 | { 163 | Assert.Same(ctx, TaskScheduler.Current); 164 | Impl().GetAwaiter().GetResult(); 165 | Assert.Same(ctx, TaskScheduler.Current); 166 | } 167 | async ValueTask Impl() 168 | { 169 | Assert.Same(ctx, TaskScheduler.Current); 170 | await Task.Yield(); 171 | Assert.Same(ctx, TaskScheduler.Current); 172 | await Task.Yield().ConfigureAwait(true); 173 | Assert.Same(ctx, TaskScheduler.Current); 174 | await Task.Yield().ConfigureAwait(false); 175 | Assert.Same(TaskScheduler.Default, TaskScheduler.Current); 176 | } 177 | Assert.Equal(ctx.Enqueued, ctx.Dequeued); 178 | Assert.Equal(3, ctx.Enqueued); 179 | } 180 | await Task.Yield(); 181 | } 182 | 183 | [Fact] 184 | public async Task TaskSchedulerRespected_PooledTask() 185 | { 186 | using (var ctx = MyTaskScheduler.Create(Log)) 187 | { 188 | Assert.Equal(0, ctx.Enqueued); 189 | Assert.Equal(0, ctx.Dequeued); 190 | 191 | Assert.NotSame(TaskScheduler.Default, ctx); 192 | Assert.Same(TaskScheduler.Default, TaskScheduler.Current); 193 | await Task.Factory.StartNew(SyncOverAsync, default, default, ctx); 194 | Assert.Same(TaskScheduler.Default, TaskScheduler.Current); 195 | 196 | void SyncOverAsync() 197 | { 198 | Assert.Same(ctx, TaskScheduler.Current); 199 | Impl().GetAwaiter().GetResult(); 200 | Assert.Same(ctx, TaskScheduler.Current); 201 | } 202 | async PooledTask Impl() 203 | { 204 | Assert.Same(ctx, TaskScheduler.Current); 205 | await Task.Yield(); 206 | Assert.Same(ctx, TaskScheduler.Current); 207 | await Task.Yield().ConfigureAwait(true); 208 | Assert.Same(ctx, TaskScheduler.Current); 209 | await Task.Yield().ConfigureAwait(false); 210 | Assert.Same(TaskScheduler.Default, TaskScheduler.Current); 211 | } 212 | Assert.Equal(ctx.Enqueued, ctx.Dequeued); 213 | Assert.Equal(3, ctx.Enqueued); 214 | } 215 | await Task.Yield(); 216 | } 217 | 218 | [Fact] 219 | public async Task TaskSchedulerRespected_PooledValueTask() 220 | { 221 | using (var ctx = MyTaskScheduler.Create(Log)) 222 | { 223 | Assert.Equal(0, ctx.Enqueued); 224 | Assert.Equal(0, ctx.Dequeued); 225 | 226 | Assert.NotSame(TaskScheduler.Default, ctx); 227 | Assert.Same(TaskScheduler.Default, TaskScheduler.Current); 228 | await Task.Factory.StartNew(SyncOverAsync, default, default, ctx); 229 | Assert.Same(TaskScheduler.Default, TaskScheduler.Current); 230 | 231 | void SyncOverAsync() 232 | { 233 | Assert.Same(ctx, TaskScheduler.Current); 234 | Impl().GetAwaiter().GetResult(); 235 | Assert.Same(ctx, TaskScheduler.Current); 236 | } 237 | async PooledValueTask Impl() 238 | { 239 | Assert.Same(ctx, TaskScheduler.Current); 240 | await Task.Yield(); 241 | Assert.Same(ctx, TaskScheduler.Current); 242 | await Task.Yield().ConfigureAwait(true); 243 | Assert.Same(ctx, TaskScheduler.Current); 244 | await Task.Yield().ConfigureAwait(false); 245 | Assert.Same(TaskScheduler.Default, TaskScheduler.Current); 246 | } 247 | Assert.Equal(ctx.Enqueued, ctx.Dequeued); 248 | Assert.Equal(3, ctx.Enqueued); 249 | } 250 | await Task.Yield(); 251 | } 252 | 253 | sealed class MySyncContext : SynchronizationContext, IDisposable 254 | { 255 | int _postCount, _sendCount; 256 | private readonly ITestOutputHelper _log; 257 | 258 | public int PostCount => Volatile.Read(ref _postCount); 259 | public int SendCount => Volatile.Read(ref _sendCount); 260 | public void Reset() 261 | { 262 | Volatile.Write(ref _postCount, 0); 263 | Volatile.Write(ref _sendCount, 0); 264 | } 265 | 266 | class Box : IThreadPoolWorkItem, IResettable 267 | { 268 | private SynchronizationContext? _context; 269 | private SendOrPostCallback? _callback; 270 | private object? _state; 271 | 272 | private Box() { } 273 | public static Box Create(SynchronizationContext context, SendOrPostCallback callback, object state) 274 | { 275 | var box = Pool.TryRent() ?? new Box(); 276 | box._context = context; 277 | box._callback = callback; 278 | box._state = state; 279 | return box; 280 | } 281 | 282 | public void Execute() 283 | { 284 | var old = Current; 285 | SetSynchronizationContext(_context); 286 | try 287 | { 288 | _callback?.Invoke(_state); 289 | } 290 | finally 291 | { 292 | SetSynchronizationContext(old); 293 | Pool.Return(this); 294 | } 295 | } 296 | 297 | public void Reset() 298 | { 299 | _context = null; 300 | _callback = null; 301 | _state = null; 302 | } 303 | } 304 | public override void Post(SendOrPostCallback d, object? state) 305 | { 306 | if (d == null) return; 307 | Interlocked.Increment(ref _postCount); 308 | Log(d, state); 309 | ThreadPool.UnsafeQueueUserWorkItem(Box.Create(this, d, state!), true); 310 | } 311 | 312 | private void Log(SendOrPostCallback d, object? state, [CallerMemberName] string? caller = null) 313 | { 314 | _log?.WriteLine($"[{caller}]: {d.Method.Name} on {d?.Target?.GetType().FullName} with {state?.GetType().FullName}"); 315 | } 316 | 317 | public override void Send(SendOrPostCallback d, object? state) 318 | { 319 | if (d == null) return; 320 | Interlocked.Increment(ref _sendCount); 321 | Log(d, state); 322 | d(state); 323 | } 324 | private readonly SynchronizationContext? _old; 325 | private MySyncContext(ITestOutputHelper log) 326 | { 327 | _log = log; 328 | _old = Current; 329 | } 330 | public void Dispose() 331 | { 332 | SetSynchronizationContext(_old); 333 | } 334 | 335 | public static MySyncContext Create(ITestOutputHelper log) 336 | { 337 | var ctx = new MySyncContext(log); 338 | SetSynchronizationContext(ctx); 339 | return ctx; 340 | } 341 | } 342 | 343 | sealed class MyTaskScheduler : TaskScheduler, IDisposable 344 | { 345 | private readonly ITestOutputHelper _log; 346 | readonly Queue _queue = new Queue(); 347 | 348 | protected override IEnumerable GetScheduledTasks() 349 | { 350 | lock (_queue) { return _queue.ToArray(); } 351 | } 352 | public static MyTaskScheduler Create(ITestOutputHelper log) => new MyTaskScheduler(log); 353 | private MyTaskScheduler(ITestOutputHelper log) 354 | { 355 | _log = log; 356 | for (int i = 0; i < 3; i++) 357 | { 358 | new Thread(obj => ((MyTaskScheduler)obj!).Run()).Start(this); 359 | } 360 | } 361 | private void Run() 362 | { 363 | _log?.WriteLine(nameof(Run)); 364 | while (true) 365 | { 366 | Task next; 367 | lock (_queue) 368 | { 369 | if (_queue.Count == 0) 370 | { 371 | if (_disposed) break; 372 | Monitor.Wait(_queue, 1000); 373 | continue; 374 | } 375 | next = _queue.Dequeue(); 376 | } 377 | _dequeued++; 378 | _log?.WriteLine($"{nameof(TryExecuteTask)}: {next}"); 379 | var result = TryExecuteTask(next); 380 | _log?.WriteLine($"result: {result}"); 381 | } 382 | _log?.WriteLine("(exit worker)"); 383 | } 384 | protected override void QueueTask(Task task) 385 | { 386 | lock (_queue) 387 | { 388 | _log?.WriteLine(nameof(QueueTask)); 389 | _queue.Enqueue(task); 390 | if (_queue.Count == 1) Monitor.Pulse(_queue); 391 | _enqueued++; 392 | } 393 | } 394 | protected override bool TryDequeue(Task task) 395 | { 396 | lock (_queue) 397 | { 398 | _log?.WriteLine(nameof(TryDequeue)); 399 | if (_queue.Count != 0 && _queue.Peek() == task) 400 | { 401 | _queue.Dequeue(); 402 | return true; 403 | } 404 | } 405 | return false; 406 | } 407 | protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) 408 | { 409 | _log?.WriteLine($"{nameof(TryExecuteTaskInline)}: {task}"); 410 | var result = TryExecuteTask(task); 411 | _log?.WriteLine($"result: {result}"); 412 | return result; 413 | } 414 | private volatile bool _disposed = false; 415 | public void Dispose() 416 | { 417 | _disposed = true; 418 | lock (_queue) { Monitor.PulseAll(_queue); } 419 | _log?.WriteLine(nameof(Dispose)); 420 | } 421 | private volatile int _enqueued, _dequeued; 422 | public int Enqueued => _enqueued; 423 | public int Dequeued => _dequeued; 424 | } 425 | } 426 | } 427 | #endif -------------------------------------------------------------------------------- /tests/PooledAwait.Test/FireAndForgetTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Xunit; 4 | 5 | namespace PooledAwait.Test 6 | { 7 | [Collection("Sequential")] 8 | public class FireAndForgetTests 9 | { 10 | static void LockedAdd(List list, T value) 11 | { 12 | lock(list) { list.Add(value); } 13 | } 14 | 15 | [Fact] 16 | public async Task AsValueTask() 17 | { 18 | var list = new List(); 19 | var allDone = PooledValueTask.CreateSource(); 20 | 21 | LockedAdd(list, "a"); 22 | await TestAsync(); 23 | LockedAdd(list, "b"); 24 | await allDone.Task; 25 | LockedAdd(list, "c"); 26 | 27 | Assert.Equal("a,d,e,f,b,c", string.Join(",", list)); 28 | 29 | async ValueTask TestAsync() 30 | { 31 | LockedAdd(list, "d"); 32 | await Task.Delay(50); 33 | LockedAdd(list, "e"); 34 | await Task.Yield(); 35 | LockedAdd(list, "f"); 36 | await Task.Yield(); 37 | allDone.TrySetResult(); // this is just so we know all added 38 | } 39 | } 40 | 41 | [Fact] 42 | public async Task AsFireAndForget() 43 | { 44 | var list = new List(); 45 | var allDone = PooledValueTask.CreateSource(); 46 | 47 | LockedAdd(list, "a"); 48 | await TestAsync(); 49 | LockedAdd(list, "b"); 50 | await allDone.Task; 51 | LockedAdd(list, "c"); 52 | 53 | Assert.Equal("a,d,b,e,f,c", string.Join(",", list)); 54 | 55 | async FireAndForget TestAsync() 56 | { 57 | LockedAdd(list, "d"); 58 | await Task.Delay(50); 59 | LockedAdd(list, "e"); 60 | await Task.Yield(); 61 | LockedAdd(list, "f"); 62 | await Task.Yield(); 63 | allDone.TrySetResult();// this is just so we know all added 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/PooledAwait.Test/LazyTaskTests.cs: -------------------------------------------------------------------------------- 1 | using PooledAwait.Internal; 2 | using System; 3 | using System.Threading.Tasks; 4 | using System.Threading.Tasks.Sources; 5 | using Xunit; 6 | 7 | namespace PooledAwait.Test 8 | { 9 | [Collection("Sequential")] 10 | public class LazyTaskTests 11 | { 12 | [Fact] 13 | public void ConstantsAreImmortal() 14 | { 15 | var obj = LazyTaskCompletionSource.CreateConstant(42); 16 | Assert.True(obj.IsValid); 17 | Assert.Equal(42, obj.Task.Result); 18 | obj.Dispose(); 19 | Assert.True(obj.IsValid); 20 | Assert.Equal(42, obj.Task.Result); 21 | } 22 | 23 | [Fact] 24 | public void ConstantCanceledAreImmortal() 25 | { 26 | var obj = LazyTaskCompletionSource.CanceledTask; 27 | Assert.True(obj.IsValid); 28 | Assert.Equal(TaskStatus.Canceled, obj.Task.Status); 29 | obj.Dispose(); 30 | Assert.True(obj.IsValid); 31 | Assert.Equal(TaskStatus.Canceled, obj.Task.Status); 32 | } 33 | 34 | [Fact] 35 | public void ConstantsAreImmortal_NonGeneric() 36 | { 37 | var obj = LazyTaskCompletionSource.CompletedTask; 38 | Assert.True(obj.IsValid); 39 | Assert.Same(Task.CompletedTask, obj.Task); 40 | obj.Dispose(); 41 | Assert.True(obj.IsValid); 42 | Assert.Same(Task.CompletedTask, obj.Task); 43 | } 44 | 45 | [Fact] 46 | public void ConstantCanceledAreImmortal_NonGeneric() 47 | { 48 | var obj = LazyTaskCompletionSource.CanceledTask; 49 | Assert.True(obj.IsValid); 50 | Assert.Equal(TaskStatus.Canceled, obj.Task.Status); 51 | obj.Dispose(); 52 | Assert.True(obj.IsValid); 53 | Assert.Equal(TaskStatus.Canceled, obj.Task.Status); 54 | } 55 | 56 | [Fact] 57 | public void NormalValuesAreNotImmortal() 58 | { 59 | var obj = LazyTaskCompletionSource.Create(); 60 | obj.SetResult(42); 61 | Assert.True(obj.IsValid); 62 | Assert.Equal(42, obj.Task.Result); 63 | obj.Dispose(); 64 | Assert.False(obj.IsValid); 65 | Assert.Throws(() => obj.Task.Result); 66 | } 67 | 68 | [Fact] 69 | public void NormalValuesAreNotImmortal_NonGeneric() 70 | { 71 | var obj = LazyTaskCompletionSource.Create(); 72 | obj.SetResult(); 73 | Assert.True(obj.IsValid); 74 | obj.Task.Wait(); 75 | obj.Dispose(); 76 | Assert.False(obj.IsValid); 77 | Assert.Throws(() => obj.Task.Wait()); 78 | } 79 | 80 | [Fact] 81 | public void DefaultLazy() 82 | { 83 | var source = default(LazyTaskCompletionSource); 84 | Assert.False(source.IsValid); 85 | Assert.False(source.HasTask); 86 | Assert.False(source.HasSource); 87 | Assert.False(source.TrySetCanceled()); 88 | Assert.False(source.TrySetException(new Exception())); 89 | Assert.False(source.TrySetResult()); 90 | 91 | Assert.ThrowsAsync(() => source.Task); 92 | } 93 | 94 | [Fact] 95 | public void CreateAndDispose() 96 | { 97 | using (var source = LazyTaskCompletionSource.Create()) { } 98 | Counters.Reset(); 99 | using (var source = LazyTaskCompletionSource.Create()) { } 100 | #if DEBUG 101 | Assert.Equal(0, Counters.TotalAllocations); 102 | #endif 103 | LazyTaskCompletionSource clone; 104 | using (var source = LazyTaskCompletionSource.Create()) 105 | { 106 | Assert.True(source.IsValid); 107 | Assert.False(source.HasTask); 108 | Assert.False(source.HasSource); 109 | #if DEBUG 110 | Assert.Equal(0, Counters.TotalAllocations); 111 | #endif 112 | var task = source.Task; 113 | Assert.True(source.IsValid); 114 | Assert.True(source.HasTask); 115 | Assert.True(source.HasSource); 116 | #if DEBUG 117 | Assert.Equal(1, Counters.TotalAllocations); 118 | #endif 119 | clone = source; 120 | } 121 | Assert.False(clone.IsValid); 122 | } 123 | 124 | class TestException : Exception { } 125 | 126 | [Theory] 127 | [InlineData(ValueTaskSourceStatus.Canceled, true)] 128 | [InlineData(ValueTaskSourceStatus.Succeeded, true)] 129 | [InlineData(ValueTaskSourceStatus.Faulted, true)] 130 | [InlineData(ValueTaskSourceStatus.Canceled, false)] 131 | [InlineData(ValueTaskSourceStatus.Succeeded, false)] 132 | [InlineData(ValueTaskSourceStatus.Faulted, false)] 133 | public async Task TaskFirst(ValueTaskSourceStatus status, bool async) 134 | { 135 | using var source = LazyTaskCompletionSource.Create(); 136 | Assert.False(source.HasTask); 137 | Assert.False(source.HasSource); 138 | var task = source.Task; 139 | Assert.True(source.HasTask); 140 | Assert.True(source.HasSource); 141 | switch (status) 142 | { 143 | case ValueTaskSourceStatus.Canceled: 144 | Assert.True(source.TrySetCanceled()); 145 | Assert.False(source.TrySetCanceled()); 146 | try 147 | { 148 | if (async) await task; 149 | else task.GetAwaiter().GetResult(); 150 | } 151 | catch (OperationCanceledException) { } 152 | break; 153 | case ValueTaskSourceStatus.Faulted: 154 | Assert.True(source.TrySetException(new TestException())); 155 | Assert.False(source.TrySetException(new TestException())); 156 | try 157 | { 158 | if (async) await task; 159 | else task.GetAwaiter().GetResult(); 160 | } 161 | catch (TestException) { } 162 | break; 163 | case ValueTaskSourceStatus.Succeeded: 164 | Assert.True(source.TrySetResult()); 165 | Assert.False(source.TrySetResult()); 166 | if (async) await task; 167 | else task.Wait(); 168 | Assert.NotSame(task, TaskUtils.CompletedTask); 169 | break; 170 | } 171 | } 172 | 173 | [Theory] 174 | [InlineData(ValueTaskSourceStatus.Canceled, true)] 175 | [InlineData(ValueTaskSourceStatus.Succeeded, true)] 176 | [InlineData(ValueTaskSourceStatus.Faulted, true)] 177 | [InlineData(ValueTaskSourceStatus.Canceled, false)] 178 | [InlineData(ValueTaskSourceStatus.Succeeded, false)] 179 | [InlineData(ValueTaskSourceStatus.Faulted, false)] 180 | public async Task TaskLast(ValueTaskSourceStatus status, bool async) 181 | { 182 | using var source = LazyTaskCompletionSource.Create(); 183 | switch (status) 184 | { 185 | case ValueTaskSourceStatus.Canceled: 186 | Assert.True(source.TrySetCanceled()); 187 | Assert.False(source.TrySetCanceled()); 188 | break; 189 | case ValueTaskSourceStatus.Faulted: 190 | Assert.True(source.TrySetException(new TestException())); 191 | Assert.False(source.TrySetException(new TestException())); 192 | break; 193 | case ValueTaskSourceStatus.Succeeded: 194 | Assert.True(source.TrySetResult()); 195 | Assert.False(source.TrySetResult()); 196 | break; 197 | } 198 | Assert.Equal(status == ValueTaskSourceStatus.Canceled, source.HasTask); 199 | Assert.False(source.HasSource); 200 | var task = source.Task; 201 | Assert.True(source.HasTask); 202 | Assert.False(source.HasSource); 203 | switch (status) 204 | { 205 | case ValueTaskSourceStatus.Canceled: 206 | try 207 | { 208 | if (async) await task; 209 | else task.GetAwaiter().GetResult(); 210 | } 211 | catch (OperationCanceledException) { } 212 | break; 213 | case ValueTaskSourceStatus.Faulted: 214 | try 215 | { 216 | if (async) await task; 217 | else task.GetAwaiter().GetResult(); 218 | } 219 | catch (TestException) { } 220 | break; 221 | case ValueTaskSourceStatus.Succeeded: 222 | if (async) await task; 223 | else task.Wait(); 224 | Assert.Same(task, TaskUtils.CompletedTask); 225 | break; 226 | } 227 | } 228 | 229 | 230 | 231 | [Fact] 232 | public void DefaultLazyT() 233 | { 234 | var source = default(LazyTaskCompletionSource); 235 | Assert.False(source.IsValid); 236 | Assert.False(source.HasTask); 237 | Assert.False(source.HasSource); 238 | Assert.False(source.TrySetCanceled()); 239 | Assert.False(source.TrySetException(new Exception())); 240 | Assert.False(source.TrySetResult(42)); 241 | 242 | Assert.ThrowsAsync(() => source.Task); 243 | } 244 | 245 | [Fact] 246 | public void CreateAndDisposeT() 247 | { 248 | using (var source = LazyTaskCompletionSource.Create()) { } 249 | Counters.Reset(); 250 | using (var source = LazyTaskCompletionSource.Create()) { } 251 | #if DEBUG 252 | Assert.Equal(0, Counters.TotalAllocations); 253 | #endif 254 | LazyTaskCompletionSource clone; 255 | using (var source = LazyTaskCompletionSource.Create()) 256 | { 257 | Assert.True(source.IsValid); 258 | Assert.False(source.HasTask); 259 | Assert.False(source.HasSource); 260 | #if DEBUG 261 | Assert.Equal(0, Counters.TotalAllocations); 262 | #endif 263 | var task = source.Task; 264 | Assert.True(source.IsValid); 265 | Assert.True(source.HasTask); 266 | Assert.True(source.HasSource); 267 | #if DEBUG 268 | Assert.Equal(1, Counters.TotalAllocations); 269 | #endif 270 | clone = source; 271 | } 272 | Assert.False(clone.IsValid); 273 | } 274 | 275 | [Theory] 276 | [InlineData(ValueTaskSourceStatus.Canceled, true)] 277 | [InlineData(ValueTaskSourceStatus.Succeeded, true)] 278 | [InlineData(ValueTaskSourceStatus.Faulted, true)] 279 | [InlineData(ValueTaskSourceStatus.Canceled, false)] 280 | [InlineData(ValueTaskSourceStatus.Succeeded, false)] 281 | [InlineData(ValueTaskSourceStatus.Faulted, false)] 282 | public async Task TaskFirstT(ValueTaskSourceStatus status, bool async) 283 | { 284 | using var source = LazyTaskCompletionSource.Create(); 285 | Assert.False(source.HasTask); 286 | Assert.False(source.HasSource); 287 | var task = source.Task; 288 | Assert.True(source.HasTask); 289 | Assert.True(source.HasSource); 290 | switch (status) 291 | { 292 | case ValueTaskSourceStatus.Canceled: 293 | Assert.True(source.TrySetCanceled()); 294 | Assert.False(source.TrySetCanceled()); 295 | try 296 | { 297 | if (async) await task; 298 | else task.GetAwaiter().GetResult(); 299 | } 300 | catch (OperationCanceledException) { } 301 | break; 302 | case ValueTaskSourceStatus.Faulted: 303 | Assert.True(source.TrySetException(new TestException())); 304 | Assert.False(source.TrySetException(new TestException())); 305 | try 306 | { 307 | if (async) await task; 308 | else task.GetAwaiter().GetResult(); 309 | } 310 | catch (TestException) { } 311 | break; 312 | case ValueTaskSourceStatus.Succeeded: 313 | Assert.True(source.TrySetResult(42)); 314 | Assert.False(source.TrySetResult(42)); 315 | if (async) Assert.Equal(42, await task); 316 | else Assert.Equal(42, task.Result); 317 | 318 | Assert.NotSame(task, TaskUtils.CompletedTask); 319 | break; 320 | } 321 | } 322 | 323 | [Theory] 324 | [InlineData(ValueTaskSourceStatus.Canceled, true)] 325 | [InlineData(ValueTaskSourceStatus.Succeeded, true)] 326 | [InlineData(ValueTaskSourceStatus.Faulted, true)] 327 | [InlineData(ValueTaskSourceStatus.Canceled, false)] 328 | [InlineData(ValueTaskSourceStatus.Succeeded, false)] 329 | [InlineData(ValueTaskSourceStatus.Faulted, false)] 330 | public async Task TaskLastT(ValueTaskSourceStatus status, bool async) 331 | { 332 | using var source = LazyTaskCompletionSource.Create(); 333 | switch (status) 334 | { 335 | case ValueTaskSourceStatus.Canceled: 336 | Assert.True(source.TrySetCanceled()); 337 | Assert.False(source.TrySetCanceled()); 338 | break; 339 | case ValueTaskSourceStatus.Faulted: 340 | Assert.True(source.TrySetException(new TestException())); 341 | Assert.False(source.TrySetException(new TestException())); 342 | break; 343 | case ValueTaskSourceStatus.Succeeded: 344 | Assert.True(source.TrySetResult(42)); 345 | Assert.False(source.TrySetResult(42)); 346 | break; 347 | } 348 | Assert.Equal(status == ValueTaskSourceStatus.Canceled, source.HasTask); 349 | Assert.False(source.HasSource); 350 | var task = source.Task; 351 | Assert.True(source.HasTask); 352 | Assert.False(source.HasSource); 353 | switch (status) 354 | { 355 | case ValueTaskSourceStatus.Canceled: 356 | try 357 | { 358 | if (async) await task; 359 | else task.GetAwaiter().GetResult(); 360 | } 361 | catch (OperationCanceledException) { } 362 | break; 363 | case ValueTaskSourceStatus.Faulted: 364 | try 365 | { 366 | if (async) await task; 367 | else task.GetAwaiter().GetResult(); 368 | } 369 | catch (TestException) { } 370 | break; 371 | case ValueTaskSourceStatus.Succeeded: 372 | if (async) Assert.Equal(42, await task); 373 | else Assert.Equal(42, task.Result); 374 | 375 | Assert.NotSame(task, TaskUtils.CompletedTask); 376 | break; 377 | } 378 | } 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /tests/PooledAwait.Test/NullableTypes.cs: -------------------------------------------------------------------------------- 1 | using PooledAwait.Internal; 2 | using PooledAwait.MethodBuilders; 3 | using System; 4 | using System.Linq; 5 | using System.Runtime.CompilerServices; 6 | using Xunit; 7 | 8 | namespace PooledAwait.Test 9 | { 10 | public class NullableTypes 11 | { 12 | [Theory] 13 | [InlineData(typeof(FireAndForget))] 14 | [InlineData(typeof(PooledTask))] 15 | [InlineData(typeof(PooledTask<>))] 16 | [InlineData(typeof(PooledValueTask))] 17 | [InlineData(typeof(PooledValueTask<>))] 18 | [InlineData(typeof(PooledValueTaskSource))] 19 | [InlineData(typeof(PooledValueTaskSource<>))] 20 | [InlineData(typeof(ValueTaskCompletionSource<>))] 21 | [InlineData(typeof(FireAndForgetMethodBuilder))] 22 | [InlineData(typeof(PooledTaskMethodBuilder))] 23 | [InlineData(typeof(PooledTaskMethodBuilder<>))] 24 | [InlineData(typeof(PooledValueTaskMethodBuilder))] 25 | [InlineData(typeof(PooledValueTaskMethodBuilder<>))] 26 | [InlineData(typeof(LazyTaskCompletionSource))] 27 | [InlineData(typeof(LazyTaskCompletionSource<>))] 28 | [InlineData(typeof(Nothing))] 29 | [InlineData(typeof(Counters.Counter))] 30 | [InlineData(typeof(ConfiguredYieldAwaitable))] 31 | [InlineData(typeof(ConfiguredYieldAwaitable.ConfiguredYieldAwaiter))] 32 | public void ValueTypesOverrideAllMethods(Type type) 33 | { 34 | Assert.Same(type, type.GetMethod(nameof(ToString), Type.EmptyTypes)!.DeclaringType); 35 | Assert.Same(type, type.GetMethod(nameof(GetHashCode), Type.EmptyTypes)!.DeclaringType); 36 | Assert.Same(type, type.GetMethod(nameof(Equals), new Type[] { typeof(object) })!.DeclaringType); 37 | 38 | #if NETCOREAPP3_0 39 | Assert.Equal(!s_allowedMutable.Contains(type), Attribute.IsDefined(type, typeof(IsReadOnlyAttribute))); 40 | #endif 41 | } 42 | #if NETCOREAPP3_0 43 | static readonly Type[] s_allowedMutable = 44 | { 45 | typeof(Counters.Counter), 46 | typeof(FireAndForgetMethodBuilder), 47 | typeof(PooledTaskMethodBuilder), 48 | typeof(PooledTaskMethodBuilder<>), 49 | typeof(PooledValueTaskMethodBuilder), 50 | typeof(PooledValueTaskMethodBuilder<>), 51 | }; 52 | #endif 53 | 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/PooledAwait.Test/PoolTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | using Xunit.Abstractions; 7 | 8 | namespace PooledAwait.Test 9 | { 10 | public class PoolTests 11 | { 12 | public PoolTests(ITestOutputHelper log) => Log = log; 13 | private readonly ITestOutputHelper Log; 14 | 15 | [PoolSize(100)] 16 | class SomeType { } 17 | 18 | static void FlushAndVerify() 19 | { 20 | // note: only good for the current thread! 21 | while (Pool.TryGet() != null) { } 22 | 23 | var count = Pool.Count(); 24 | Assert.True(0 == count, $"expected 0 values"); 25 | } 26 | 27 | [Fact] 28 | public void WorksAsExpected_One() 29 | { 30 | FlushAndVerify(); 31 | 32 | Assert.Null(Pool.TryGet()); 33 | var obj = new SomeType(); 34 | Pool.TryPut(obj); 35 | var got = Pool.TryGet(); 36 | Assert.Same(obj, got); 37 | Assert.Null(Pool.TryGet()); 38 | 39 | FlushAndVerify(); 40 | } 41 | 42 | [Fact] 43 | public void WorksAsExpected_Two() 44 | { 45 | FlushAndVerify(); 46 | 47 | Assert.Null(Pool.TryGet()); 48 | var obj0 = new SomeType(); 49 | var obj1 = new SomeType(); 50 | Pool.TryPut(obj0); 51 | Pool.TryPut(obj1); 52 | 53 | var got0 = Pool.TryGet(); 54 | var got1 = Pool.TryGet(); 55 | // order is respected here because of the thread-static 56 | Assert.Same(obj0, got0); 57 | Assert.Same(obj1, got1); 58 | Assert.Null(Pool.TryGet()); 59 | 60 | FlushAndVerify(); 61 | } 62 | 63 | [Fact] 64 | public void WorksAsExpected_Three() 65 | { 66 | FlushAndVerify(); 67 | 68 | Assert.Null(Pool.TryGet()); 69 | var obj0 = new SomeType(); 70 | var obj1 = new SomeType(); 71 | var obj2 = new SomeType(); 72 | Pool.TryPut(obj0); 73 | Pool.TryPut(obj1); 74 | Pool.TryPut(obj2); 75 | 76 | var got0 = Pool.TryGet(); 77 | var got1 = Pool.TryGet(); 78 | var got2 = Pool.TryGet(); 79 | // note: order is respected here because of the thread-static 80 | Assert.Same(obj0, got0); 81 | // order gets respected here because: queue 82 | Assert.Same(obj1, got1); 83 | Assert.Same(obj2, got2); 84 | Assert.Null(Pool.TryGet()); 85 | 86 | FlushAndVerify(); 87 | } 88 | 89 | [Fact] 90 | public void WorksAsExpected_Full() 91 | { 92 | FlushAndVerify(); 93 | 94 | // note: order doesn't matter 95 | HashSet knownObjects = new HashSet(); 96 | for (int i = 0; i <= Pool.Size; i++) 97 | { 98 | var obj = new SomeType(); 99 | Pool.TryPut(obj); 100 | knownObjects.Add(obj); 101 | } 102 | 103 | // we should now have a full pool; should be able to drain that many 104 | for (int i = 0; i <= Pool.Size; i++) 105 | { 106 | var obj = Pool.TryGet(); 107 | Assert.NotNull(obj); 108 | Assert.True(knownObjects.Remove(obj!)); 109 | } 110 | 111 | // we should have accounted for everything 112 | Assert.Empty(knownObjects); 113 | 114 | // next get should be nil 115 | Assert.Null(Pool.TryGet()); 116 | 117 | FlushAndVerify(); 118 | } 119 | 120 | [Fact] 121 | public void HammerParallel() 122 | { 123 | FlushAndVerify(); 124 | for (int i = 0; i <= Pool.Size; i++) 125 | { 126 | var obj = new SomeType(); 127 | Pool.TryPut(obj); 128 | } 129 | 130 | const int Repeats = 5000, Workers = 100; 131 | SomeType?[] Results = new SomeType?[Workers]; 132 | HashSet seen = new HashSet(); 133 | for (int j = 0; j < Repeats; j++) 134 | { 135 | Array.Clear(Results, 0, Results.Length); 136 | seen.Clear(); 137 | 138 | var result = Parallel.For(0, Results.Length, i => 139 | { 140 | Results[i] = Pool.TryGet(); 141 | }); 142 | Assert.True(result.IsCompleted); 143 | int found = 0; 144 | foreach (var obj in Results) 145 | { 146 | if (obj != null) 147 | { 148 | Assert.True(seen.Add(obj)); 149 | found++; 150 | Pool.TryPut(obj); 151 | } 152 | } 153 | Assert.True(found >= Pool.Size * .075, "too few"); 154 | Assert.True(found <= Pool.Size + 1, "too many"); 155 | } 156 | FlushAndVerify(); 157 | } 158 | 159 | [Fact] 160 | public void HammerThreads() 161 | { 162 | FlushAndVerify(); 163 | for (int i = 0; i < Pool.Size + 1; i++) 164 | { 165 | Pool.TryPut(new SomeType()); 166 | } 167 | int failureCount = 0; 168 | DateTime end = DateTime.Now.AddSeconds(2); 169 | void DoWork() 170 | { 171 | try 172 | { 173 | while (DateTime.Now < end) 174 | { 175 | for (int i = 0; i < 5000; i++) 176 | { 177 | var obj0 = Pool.TryGet(); 178 | var obj1 = Pool.TryGet(); 179 | var obj2 = Pool.TryGet(); 180 | 181 | if (obj0 != null) Pool.TryPut(obj0); 182 | if (obj1 != null) Pool.TryPut(obj1); 183 | if (obj2 != null) Pool.TryPut(obj2); 184 | } 185 | } 186 | } 187 | catch (Exception ex) 188 | { 189 | Interlocked.Increment(ref failureCount); 190 | if (Log != null) 191 | { 192 | lock (Log) 193 | { 194 | Log.WriteLine(ex.Message); 195 | } 196 | } 197 | } 198 | } 199 | Thread[] threads = new Thread[Environment.ProcessorCount]; 200 | for (int i = 0; i < threads.Length; i++) 201 | { 202 | threads[i] = new Thread(DoWork); 203 | threads[i].Start(); 204 | } 205 | for (int i = 0; i < threads.Length; i++) 206 | { 207 | threads[i].Join(); 208 | } 209 | Assert.Equal(0, Volatile.Read(ref failureCount)); 210 | 211 | FlushAndVerify(); 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /tests/PooledAwait.Test/PooledAwait.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1;netcoreapp2.1;net462 5 | false 6 | false 7 | preview 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/PooledAwait.Test/PooledValueTaskSourceTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Xunit; 4 | 5 | namespace PooledAwait.Test 6 | { 7 | [Collection("Sequential")] 8 | public class PooledValueTaskSourceTests 9 | { 10 | [Fact] 11 | public void CreateAndSet() 12 | { 13 | var source = PooledValueTaskSource.Create(); 14 | var task = source.Task; 15 | 16 | Assert.False(task.IsCompleted); 17 | Assert.True(source.IsValid); 18 | 19 | Assert.True(source.TrySetResult(42)); 20 | 21 | Assert.True(task.IsCompleted); 22 | Assert.Equal(42, task.Result); 23 | 24 | // now it has been fetched, it should have been reset/recycled 25 | Assert.False(source.IsValid); 26 | Assert.False(source.TrySetResult(43)); 27 | Assert.Throws(() => _ = task.Result); 28 | } 29 | 30 | [Fact] 31 | public async Task CreateAndSetAsync() 32 | { 33 | var source = PooledValueTaskSource.Create(); 34 | var task = source.Task; 35 | 36 | Assert.False(task.IsCompleted); 37 | Assert.True(source.IsValid); 38 | 39 | Assert.True(source.TrySetResult(42)); 40 | 41 | Assert.True(task.IsCompleted); 42 | Assert.Equal(42, await task); 43 | 44 | // now it has been fetched, it should have been reset/recycled 45 | Assert.False(source.IsValid); 46 | Assert.False(source.TrySetResult(43)); 47 | await Assert.ThrowsAsync(async () => _ = await task); 48 | } 49 | 50 | [Fact] 51 | public void ReadSync() 52 | { 53 | var source = PooledValueTaskSource.Create(); 54 | _ = Delayed(); 55 | Assert.Equal(42, source.Task.Result); 56 | 57 | async FireAndForget Delayed() 58 | { 59 | await Task.Delay(100); 60 | source.TrySetResult(42); 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/PooledAwait.Test/ValueTaskCompletionSourceTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using PooledAwait.Internal; 3 | using Xunit; 4 | using System.Threading.Tasks; 5 | 6 | namespace PooledAwait.Test 7 | { 8 | [Collection("Sequential")] 9 | public class ValueTaskCompletionSourceTests 10 | { 11 | [Fact] 12 | public void DefaultInstanceBehavior() 13 | { 14 | ValueTaskCompletionSource source = default; 15 | Assert.True(source.IsNull); 16 | Assert.False(source.IsOptimized); 17 | 18 | Assert.False(source.TrySetResult(42)); 19 | } 20 | 21 | [Theory] 22 | [InlineData(true)] 23 | [InlineData(false)] 24 | public void DefaultTask(bool shouldFault) 25 | { 26 | var source = ValueTaskCompletionSource.Create(); 27 | Counters.Reset(); 28 | source = ValueTaskCompletionSource.Create(); 29 | #if DEBUG 30 | Assert.Equal(1, Counters.TaskAllocated.Value); 31 | #endif 32 | Assert.True(source.IsOptimized); 33 | 34 | Verify(source, shouldFault); 35 | } 36 | 37 | [Theory] 38 | [InlineData(true)] 39 | [InlineData(false)] 40 | public void OptimizedTask(bool shouldFault) 41 | { 42 | var source = ValueTaskCompletionSource.CreateOptimized(); 43 | Counters.Reset(); 44 | source = ValueTaskCompletionSource.CreateOptimized(); 45 | #if DEBUG 46 | Assert.Equal(1, Counters.TaskAllocated.Value); 47 | #endif 48 | Assert.True(source.IsOptimized); 49 | 50 | Verify(source, shouldFault); 51 | } 52 | 53 | [Theory] 54 | [InlineData(true)] 55 | [InlineData(false)] 56 | public void FallbackTask(bool shouldFault) 57 | { 58 | var source = ValueTaskCompletionSource.CreateFallback(); 59 | Counters.Reset(); 60 | source = ValueTaskCompletionSource.CreateFallback(); 61 | #if DEBUG 62 | Assert.Equal(1, Counters.TaskAllocated.Value); 63 | #endif 64 | Assert.False(source.IsOptimized); 65 | 66 | Verify(source, shouldFault); 67 | } 68 | 69 | private void Verify(ValueTaskCompletionSource source, bool shouldFault) 70 | { 71 | var task = source.Task; 72 | Assert.False(task.IsCompleted); 73 | Assert.False(task.Status == TaskStatus.RanToCompletion); 74 | 75 | if (shouldFault) 76 | { 77 | Assert.True(source.TrySetException(new FormatException())); 78 | Assert.False(source.TrySetResult(42)); 79 | Assert.False(source.TrySetException(new FormatException())); 80 | 81 | for (int i = 0; i < 2; i++) // can check multiple times 82 | { 83 | Assert.True(task.IsFaulted); 84 | Assert.True(task.IsCompleted); 85 | Assert.False(task.Status == TaskStatus.RanToCompletion); 86 | var ex = Assert.Throws(() => task.Result); 87 | Assert.IsType(ex.InnerException); 88 | } 89 | } 90 | else 91 | { 92 | Assert.True(source.TrySetResult(42)); 93 | Assert.False(source.TrySetResult(42)); 94 | Assert.False(source.TrySetException(new FormatException())); 95 | 96 | for (int i = 0; i < 2; i++) // can check multiple times 97 | { 98 | Assert.False(task.IsFaulted); 99 | Assert.True(task.IsCompleted); 100 | Assert.True(task.Status == TaskStatus.RanToCompletion); 101 | Assert.Equal(42, task.Result); 102 | } 103 | } 104 | 105 | Assert.False(source.IsNull); // still good 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/AArnott/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", 3 | "version": "1.0", 4 | "assemblyVersion": "1.0", 5 | "publicReleaseRefSpec": [ 6 | "^refs/heads/master$", 7 | "^refs/tags/v\\d+\\.\\d+" 8 | ] 9 | } --------------------------------------------------------------------------------