├── .editorconfig
├── .gitignore
├── AsyncCollections.Benchmark
├── .gitignore
├── App.config
├── AsyncBatchQueueBenchmark.cs
├── AsyncCollections.Benchmark.csproj
├── AsyncQueueBenchmark.cs
├── BenchmarkConfig.cs
├── BlockingCollectionAdapter.cs
├── NitoAsyncCollectionAdapter.cs
├── Program.cs
├── Properties
│ └── AssemblyInfo.cs
├── TplBatchBlockAdapter.cs
├── TplDataflowAdapter.cs
└── packages.config
├── AsyncCollections.Test
├── .gitignore
├── Assertions.cs
├── AsyncBatchQueueTest.cs
├── AsyncBoundedPriorityQueueTest.cs
├── AsyncCollectionTakeFromAnyTest.cs
├── AsyncCollectionTest.cs
├── AsyncCollections.Test.csproj
├── AsyncQueueTest.cs
├── Properties
│ └── AssemblyInfo.cs
└── packages.config
├── AsyncCollections.sln
├── AsyncCollections
├── .gitignore
├── AnyResult.cs
├── AsyncBatchQueue.cs
├── AsyncBoundedPriorityQueue.cs
├── AsyncCollection.cs
├── AsyncCollections.csproj
├── AsyncQueue.cs
├── AsyncStack.cs
├── IAsyncBatchCollection.cs
├── IAsyncCollection.cs
├── Internal
│ ├── BitArray32.cs
│ ├── BoxedEnumerator.cs
│ ├── CancelledTask.cs
│ ├── CompletionSourceAwaiterFactory.cs
│ ├── ExclusiveCompletionSourceGroup.cs
│ ├── IAwaiter.cs
│ ├── IAwaiterFactory.cs
│ ├── SelectManyStructEnumererator.cs
│ └── TaskExtensions.cs
├── PrioritizedItem.cs
├── Properties
│ └── AssemblyInfo.cs
├── TimerAsyncBatchQueue.cs
├── ValueTask
│ ├── ConfiguredValueTaskAwaitable.cs
│ ├── ValueTask.cs
│ └── ValueTaskAwaiter.cs
├── nuget
│ └── AsyncCollections.nuspec
└── packages.config
├── LICENSE
├── NuGet.Config
└── README.md
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 |
3 | charset = utf-8-bom
4 | insert_final_newline = true
5 | trim_trailing_whitespace = false
6 |
7 | [*.cs]
8 |
9 | end_of_line = crlf
10 | indent_style = tab
11 |
12 | csharp_style_conditional_delegate_call = true : warning
13 | csharp_style_expression_bodied_accessors = true : warning
14 | csharp_style_expression_bodied_constructors = true : warning
15 | csharp_style_expression_bodied_indexers = true : warning
16 | csharp_style_expression_bodied_methods = true : warning
17 | csharp_style_expression_bodied_operators = true : warning
18 | csharp_style_expression_bodied_properties = true : warning
19 | csharp_style_inlined_variable_declaration = true : warning
20 | csharp_style_pattern_matching_over_as_with_null_check = true : warning
21 | csharp_style_pattern_matching_over_is_with_cast_check = true : warning
22 | csharp_style_throw_expression = true : warning
23 | csharp_style_var_elsewhere = false : warning
24 | csharp_style_var_for_built_in_types = false : warning
25 | csharp_style_var_when_type_is_apparent = false : warning
26 | csharp_new_line_before_catch = true
27 | csharp_new_line_before_else = true
28 | csharp_new_line_before_finally = true
29 | csharp_new_line_before_members_in_anonymous_types = true
30 | csharp_new_line_before_members_in_object_initializers = true
31 | csharp_new_line_before_open_brace = all
32 | csharp_new_line_between_query_expression_clauses = true
33 |
34 | csharp_prefer_braces = false : suggestion
35 | csharp_indent_block_contents = true
36 | csharp_indent_braces = false
37 | csharp_indent_case_contents = true
38 | csharp_indent_case_contents_when_block = false
39 | csharp_indent_switch_labels = true
40 | csharp_indent_labels = no_change
41 |
42 | csharp_space_after_cast = true
43 | csharp_space_after_comma = true
44 | csharp_space_after_dot = false
45 | csharp_space_after_semicolon_in_for_statement = true
46 | csharp_space_before_comma = false
47 | csharp_space_before_dot = false
48 | csharp_space_before_semicolon_in_for_statement = false
49 | csharp_space_after_keywords_in_control_flow_statements = true
50 | csharp_space_around_binary_operators = before_and_after
51 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
52 | csharp_space_between_method_declaration_name_and_open_parenthesis = false
53 | csharp_space_between_method_declaration_parameter_list_parentheses = true
54 | csharp_space_between_method_call_name_and_opening_parenthesis = false
55 | csharp_space_between_method_call_empty_parameter_list_parentheses = false
56 | csharp_space_between_method_call_parameter_list_parentheses = true
57 | csharp_space_between_parentheses = control_flow_statements, expressions
58 | csharp_space_before_open_square_brackets = false
59 | csharp_space_between_empty_square_brackets = false
60 | csharp_space_between_square_brackets = true
61 | csharp_space_after_colon_in_inheritance_clause = true
62 | csharp_space_before_colon_in_inheritance_clause = true
63 | csharp_preserve_single_line_statements = false
64 | csharp_preserve_single_line_blocks = true
65 |
66 | dotnet_sort_system_directives_first = true
67 | dotnet_style_coalesce_expression = true : warning
68 | dotnet_style_collection_initializer = true : suggestion
69 | dotnet_style_explicit_tuple_names = true : warning
70 | dotnet_style_null_propagation = true : warning
71 | dotnet_style_object_initializer = true : suggestion
72 | dotnet_style_predefined_type_for_locals_parameters_members = true : warning
73 | dotnet_style_predefined_type_for_member_access = false : warning
74 | dotnet_style_qualification_for_event = false : warning
75 | dotnet_style_qualification_for_field = false : warning
76 | dotnet_style_qualification_for_method = false : warning
77 | dotnet_style_qualification_for_property = false : warning
78 |
79 | # IInterface
80 |
81 | dotnet_naming_rule.interface_rule.symbols = all_interfaces
82 | dotnet_naming_rule.interface_rule.style = interface_style
83 | dotnet_naming_rule.interface_rule.severity = warning
84 |
85 | dotnet_naming_symbols.all_interfaces.applicable_kinds = interface
86 | dotnet_naming_symbols.all_interfaces.applicable_accessibilities = *
87 |
88 | dotnet_naming_style.interface_style.required_prefix = I
89 | dotnet_naming_style.interface_style.capitalization = pascal_case
90 |
91 | # _privateField
92 |
93 | dotnet_naming_rule.private_field_rule.symbols = private_fields
94 | dotnet_naming_rule.private_field_rule.style = private_field_style
95 | dotnet_naming_rule.private_field_rule.severity = warning
96 |
97 | dotnet_naming_symbols.private_fields.applicable_kinds = field
98 | dotnet_naming_symbols.private_fields.applicable_accessibilities = private
99 |
100 | dotnet_naming_style.private_field_style.required_prefix = _
101 | dotnet_naming_style.private_field_style.capitalization = camel_case
102 |
103 | # parameters
104 |
105 | dotnet_naming_rule.parameter_rule.symbols = parameters
106 | dotnet_naming_rule.parameter_rule.style = camel_case_style
107 | dotnet_naming_rule.parameter_rule.severity = warning
108 |
109 | dotnet_naming_symbols.parameters.applicable_kinds = parameter
110 |
111 | dotnet_naming_style.camel_case_style.capitalization = camel_case
112 |
113 | # Type
114 |
115 | dotnet_naming_rule.type_rule.symbols = all_types
116 | dotnet_naming_rule.type_rule.style = pascal_case_style
117 | dotnet_naming_rule.type_rule.severity = warning
118 |
119 | dotnet_naming_symbols.all_types.applicable_kinds = class, struct, enum
120 | dotnet_naming_symbols.all_types.applicable_accessibilities = *
121 |
122 | # async Task SomeMethodAsync
123 |
124 | dotnet_naming_rule.async_method_rule.symbols = async_methods
125 | dotnet_naming_rule.async_method_rule.style = async_method_style
126 | dotnet_naming_rule.async_method_rule.severity = suggestion
127 |
128 | dotnet_naming_symbols.async_methods.applicable_kinds = method
129 | dotnet_naming_symbols.async_methods.applicable_accessibilities = *
130 | dotnet_naming_symbols.async_methods.required_modifiers = async
131 |
132 | dotnet_naming_style.async_method_style.required_suffix = Async
133 | dotnet_naming_style.async_method_style.capitalization = pascal_case
134 |
135 | # NonFieldMembers
136 |
137 | dotnet_naming_rule.member_rule.symbols = non_field_members
138 | dotnet_naming_rule.member_rule.style = pascal_case_style
139 | dotnet_naming_rule.member_rule.severity = warning
140 |
141 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, method, event
142 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = *
143 |
144 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case
145 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.suo
2 | /packages/
3 | *.csproj.user
4 | /AsyncCollections.sln.ide/
5 |
--------------------------------------------------------------------------------
/AsyncCollections.Benchmark/.gitignore:
--------------------------------------------------------------------------------
1 | /bin/
2 | /obj/
3 |
--------------------------------------------------------------------------------
/AsyncCollections.Benchmark/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/AsyncCollections.Benchmark/AsyncBatchQueueBenchmark.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using BenchmarkDotNet.Attributes;
8 | using BenchmarkDotNet.Configs;
9 | using BenchmarkDotNet.Diagnostics.Windows;
10 | using BenchmarkDotNet.Jobs;
11 | using HellBrick.Collections;
12 |
13 | namespace HellBrick.AsyncCollections.Benchmark
14 | {
15 | [Config( typeof( BenchmarkConfig ) )]
16 | public class AsyncBatchQueueBenchmark
17 | {
18 | [Params( 1, 3 )]
19 | public int ConsumerTasks { get; set; }
20 |
21 | [Params( 1, 3 )]
22 | public int ProducerTasks { get; set; }
23 |
24 | private const int _itemsAddedPerThread = 9999;
25 | private const int _batchSize = 32;
26 |
27 | [Benchmark( Description = "HellBrick.AsyncCollections.AsyncBatchQueue" )]
28 | public void HellBrickAsyncBatchQueue() => DdosQueue( new AsyncBatchQueue( _batchSize ) );
29 |
30 | [Benchmark( Description = "System.Threading.Tasks.Dataflow.BatchBlock" )]
31 | public void DataflowBatchBlock() => DdosQueue( new TplBatchBlockAdapter( _batchSize ) );
32 |
33 | private void DdosQueue( IAsyncBatchCollection queue )
34 | {
35 | int itemsAddedTotal = ProducerTasks * _itemsAddedPerThread;
36 | IntHolder itemsTakenHolder = new IntHolder() { Value = 0 };
37 | CancellationTokenSource consumerCancelSource = new CancellationTokenSource();
38 | Task[] consumerTasks = Enumerable.Range( 0, ConsumerTasks )
39 | .Select( _ => Task.Run( () => RunConsumerAsync( queue, itemsTakenHolder, itemsAddedTotal, consumerCancelSource ) ) )
40 | .ToArray();
41 |
42 | Task[] producerTasks = Enumerable.Range( 0, ProducerTasks )
43 | .Select( _ => Task.Run( () => RunProducer( queue ) ) )
44 | .ToArray();
45 |
46 | Task.WaitAll( producerTasks );
47 | Task.WaitAll( consumerTasks );
48 | }
49 |
50 | private static void RunProducer( IAsyncBatchCollection queue )
51 | {
52 | for ( int i = 0; i < _itemsAddedPerThread; i++ )
53 | {
54 | int item = 42;
55 | queue.Add( item );
56 | }
57 |
58 | queue.Flush();
59 | }
60 |
61 | private static async Task RunConsumerAsync( IAsyncBatchCollection queue, IntHolder itemsTakeHolder, int itemsAddedTotal, CancellationTokenSource cancelSource )
62 | {
63 | try
64 | {
65 | CancellationToken cancelToken = cancelSource.Token;
66 |
67 | while ( true )
68 | {
69 | IReadOnlyList items = await queue.TakeAsync( cancelToken ).ConfigureAwait( false );
70 | int itemsTakenLocal = Interlocked.Add( ref itemsTakeHolder.Value, items.Count );
71 |
72 | if ( itemsTakenLocal >= itemsAddedTotal )
73 | cancelSource.Cancel();
74 | }
75 | }
76 | catch ( OperationCanceledException )
77 | {
78 | }
79 | }
80 |
81 | private class IntHolder
82 | {
83 | public int Value;
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/AsyncCollections.Benchmark/AsyncCollections.Benchmark.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {28550F43-E353-44F0-B145-1688B7C85569}
8 | Exe
9 | Properties
10 | HellBrick.AsyncCollections.Benchmark
11 | AsyncCollections.Benchmark
12 | v4.5.1
13 | 512
14 |
15 |
16 |
17 |
18 |
19 | AnyCPU
20 | true
21 | full
22 | false
23 | bin\Debug\
24 | DEBUG;TRACE
25 | prompt
26 | 4
27 | false
28 |
29 |
30 | AnyCPU
31 | pdbonly
32 | true
33 | bin\Release\
34 | TRACE
35 | prompt
36 | 4
37 | false
38 |
39 |
40 |
41 | ..\packages\BenchmarkDotNet.0.9.6\lib\net45\BenchmarkDotNet.dll
42 | True
43 |
44 |
45 | ..\packages\BenchmarkDotNet.Diagnostics.Windows.0.9.6\lib\net40\BenchmarkDotNet.Diagnostics.Windows.dll
46 | True
47 |
48 |
49 |
50 |
51 |
52 | ..\packages\Microsoft.Diagnostics.Tracing.TraceEvent.1.0.41\lib\net40\Microsoft.Diagnostics.Tracing.TraceEvent.dll
53 | True
54 |
55 |
56 | ..\packages\Nito.AsyncEx.3.0.0\lib\net45\Nito.AsyncEx.dll
57 |
58 |
59 | ..\packages\Nito.AsyncEx.3.0.0\lib\net45\Nito.AsyncEx.Concurrent.dll
60 |
61 |
62 | ..\packages\Nito.AsyncEx.3.0.0\lib\net45\Nito.AsyncEx.Enlightenment.dll
63 |
64 |
65 |
66 |
67 |
68 | False
69 | ..\packages\Microsoft.Tpl.Dataflow.4.5.23\lib\portable-net45+win8+wpa81\System.Threading.Tasks.Dataflow.dll
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | {61be7a3d-ac66-434b-8a77-4f3466398cbc}
95 | AsyncCollections
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
106 |
107 |
108 |
109 |
116 |
--------------------------------------------------------------------------------
/AsyncCollections.Benchmark/AsyncQueueBenchmark.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Concurrent;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 | using BenchmarkDotNet;
9 | using BenchmarkDotNet.Attributes;
10 | using BenchmarkDotNet.Configs;
11 | using BenchmarkDotNet.Diagnostics.Windows;
12 | using BenchmarkDotNet.Jobs;
13 | using HellBrick.Collections;
14 |
15 | namespace HellBrick.AsyncCollections.Benchmark
16 | {
17 | [Config( typeof( BenchmarkConfig ) )]
18 | public class AsyncQueueBenchmark
19 | {
20 | [Params( 1, 3 )]
21 | public int ConsumerTasks { get; set; }
22 |
23 | [Params( 1, 3 )]
24 | public int ProducerTasks { get; set; }
25 |
26 | private const int _itemsAddedPerThread = 10000;
27 |
28 | [Benchmark( Description = "AsyncQueue" )]
29 | public void HellBrickAsyncQueue() => DdosQueue( new AsyncQueue() );
30 |
31 | [Benchmark( Description = "AsyncCollection( ConcurrentQueue )" )]
32 | public void HellBrickAsyncCollection() => DdosQueue( new AsyncCollection( new ConcurrentQueue() ) );
33 |
34 | [Benchmark( Description = "Nito.AsyncEx.AsyncCollection" )]
35 | public void NitoAsyncCollection() => DdosQueue( new NitoAsyncCollectionAdapter() );
36 |
37 | [Benchmark( Description = "System.Concurrent.BlockingCollection" )]
38 | public void SystemBlockingCollection() => DdosQueue( new BlockingCollectionAdapter() );
39 |
40 | [Benchmark( Description = "System.Threading.Tasks.Dataflow.BufferBlock" )]
41 | public void DataflowBufferBlock() => DdosQueue( new TplDataflowAdapter() );
42 |
43 | private void DdosQueue( IAsyncCollection queue )
44 | {
45 | int itemsAddedTotal = ProducerTasks * _itemsAddedPerThread;
46 | IntHolder itemsTakenHolder = new IntHolder() { Value = 0 };
47 | CancellationTokenSource consumerCancelSource = new CancellationTokenSource();
48 | Task[] consumerTasks = Enumerable.Range( 0, ConsumerTasks )
49 | .Select( _ => Task.Run( () => RunConsumerAsync( queue, itemsTakenHolder, itemsAddedTotal, consumerCancelSource ) ) )
50 | .ToArray();
51 |
52 | Task[] producerTasks = Enumerable.Range( 0, ProducerTasks )
53 | .Select( _ => Task.Run( () => RunProducer( queue ) ) )
54 | .ToArray();
55 |
56 | Task.WaitAll( producerTasks );
57 | Task.WaitAll( consumerTasks );
58 | }
59 |
60 | private static void RunProducer( IAsyncCollection queue )
61 | {
62 | for ( int i = 0; i < _itemsAddedPerThread; i++ )
63 | {
64 | int item = 42;
65 | queue.Add( item );
66 | }
67 | }
68 |
69 | private static async Task RunConsumerAsync( IAsyncCollection queue, IntHolder itemsTakeHolder, int itemsAddedTotal, CancellationTokenSource cancelSource )
70 | {
71 | try
72 | {
73 | CancellationToken cancelToken = cancelSource.Token;
74 |
75 | while ( true )
76 | {
77 | int item = await queue.TakeAsync( cancelToken ).ConfigureAwait( false );
78 | int itemsTakenLocal = Interlocked.Increment( ref itemsTakeHolder.Value );
79 |
80 | if ( itemsTakenLocal >= itemsAddedTotal )
81 | cancelSource.Cancel();
82 | }
83 | }
84 | catch ( OperationCanceledException )
85 | {
86 | }
87 | }
88 |
89 | private class IntHolder
90 | {
91 | public int Value;
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/AsyncCollections.Benchmark/BenchmarkConfig.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using BenchmarkDotNet.Configs;
7 | using BenchmarkDotNet.Diagnostics.Windows;
8 | using BenchmarkDotNet.Jobs;
9 |
10 | namespace HellBrick.AsyncCollections.Benchmark
11 | {
12 | internal class BenchmarkConfig : ManualConfig
13 | {
14 | public BenchmarkConfig()
15 | {
16 | Add( Job.RyuJitX64.WithLaunchCount( 1 ) );
17 | Add( new MemoryDiagnoser() );
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/AsyncCollections.Benchmark/BlockingCollectionAdapter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Concurrent;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 | using HellBrick.Collections;
8 |
9 | namespace HellBrick.AsyncCollections.Benchmark
10 | {
11 | class BlockingCollectionAdapter: IAsyncCollection
12 | {
13 | private readonly BlockingCollection _collection;
14 |
15 | public BlockingCollectionAdapter()
16 | {
17 | _collection = new BlockingCollection();
18 | }
19 |
20 | #region IAsyncCollection Members
21 |
22 | public int AwaiterCount
23 | {
24 | get { throw new NotImplementedException(); }
25 | }
26 |
27 | public void Add( T item )
28 | {
29 | _collection.Add( item );
30 | }
31 |
32 | public ValueTask TakeAsync( System.Threading.CancellationToken cancellationToken )
33 | {
34 | T item = _collection.Take( cancellationToken );
35 | return new ValueTask( item );
36 | }
37 |
38 | #endregion
39 |
40 | #region IEnumerable Members
41 |
42 | public IEnumerator GetEnumerator()
43 | {
44 | throw new NotImplementedException();
45 | }
46 |
47 | #endregion
48 |
49 | #region IEnumerable Members
50 |
51 | System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
52 | {
53 | throw new NotImplementedException();
54 | }
55 |
56 | #endregion
57 |
58 | #region IReadOnlyCollection Members
59 |
60 | public int Count
61 | {
62 | get { throw new NotImplementedException(); }
63 | }
64 |
65 | #endregion
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/AsyncCollections.Benchmark/NitoAsyncCollectionAdapter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using HellBrick.Collections;
7 |
8 | namespace HellBrick.AsyncCollections.Benchmark
9 | {
10 | class NitoAsyncCollectionAdapter: IAsyncCollection
11 | {
12 | private readonly Nito.AsyncEx.AsyncCollection _collection;
13 |
14 | public NitoAsyncCollectionAdapter()
15 | {
16 | _collection = new Nito.AsyncEx.AsyncCollection();
17 | }
18 |
19 | #region IAsyncCollection Members
20 |
21 | public int AwaiterCount
22 | {
23 | get { throw new NotImplementedException(); }
24 | }
25 |
26 | public void Add( T item )
27 | {
28 | _collection.Add( item );
29 | }
30 |
31 | public ValueTask TakeAsync( System.Threading.CancellationToken cancellationToken )
32 | {
33 | return new ValueTask( _collection.TakeAsync( cancellationToken ) );
34 | }
35 |
36 | #endregion
37 |
38 | #region IEnumerable Members
39 |
40 | public IEnumerator GetEnumerator()
41 | {
42 | throw new NotImplementedException();
43 | }
44 |
45 | #endregion
46 |
47 | #region IEnumerable Members
48 |
49 | System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
50 | {
51 | throw new NotImplementedException();
52 | }
53 |
54 | #endregion
55 |
56 | #region IReadOnlyCollection Members
57 |
58 | public int Count
59 | {
60 | get { throw new NotImplementedException(); }
61 | }
62 |
63 | #endregion
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/AsyncCollections.Benchmark/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using BenchmarkDotNet.Running;
7 |
8 | namespace HellBrick.AsyncCollections.Benchmark
9 | {
10 | internal class Program
11 | {
12 | private static void Main( string[] args ) => new BenchmarkSwitcher( EnumerateBenchmarks().ToArray() ).Run();
13 |
14 | private static IEnumerable EnumerateBenchmarks()
15 | {
16 | yield return typeof( AsyncQueueBenchmark );
17 | yield return typeof( AsyncBatchQueueBenchmark );
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/AsyncCollections.Benchmark/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle( "AsyncCollections.Benchmark" )]
9 | [assembly: AssemblyDescription( "" )]
10 | [assembly: AssemblyConfiguration( "" )]
11 | [assembly: AssemblyCompany( "" )]
12 | [assembly: AssemblyProduct( "AsyncCollections.Benchmark" )]
13 | [assembly: AssemblyCopyright( "Copyright © 2014" )]
14 | [assembly: AssemblyTrademark( "" )]
15 | [assembly: AssemblyCulture( "" )]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible( false )]
21 |
22 | // The following GUID is for the ID of the typelib if this project is exposed to COM
23 | [assembly: Guid( "2f4828ad-4bee-4575-900b-fd88ffc0ce9b" )]
24 |
25 | // Version information for an assembly consists of the following four values:
26 | //
27 | // Major Version
28 | // Minor Version
29 | // Build Number
30 | // Revision
31 | //
32 | // You can specify all the values or you can default the Build and Revision Numbers
33 | // by using the '*' as shown below:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion( "1.0.0.0" )]
36 | [assembly: AssemblyFileVersion( "1.0.0.0" )]
37 |
--------------------------------------------------------------------------------
/AsyncCollections.Benchmark/TplBatchBlockAdapter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 | using System.Threading.Tasks.Dataflow;
9 | using HellBrick.Collections;
10 |
11 | namespace HellBrick.AsyncCollections.Benchmark
12 | {
13 | public class TplBatchBlockAdapter : IAsyncBatchCollection
14 | {
15 | private readonly BatchBlock _batchBlock;
16 |
17 | public TplBatchBlockAdapter( int batchSize )
18 | {
19 | _batchBlock = new BatchBlock( batchSize );
20 | }
21 |
22 | public int BatchSize => _batchBlock.BatchSize;
23 | public int Count => _batchBlock.OutputCount;
24 |
25 | public void Add( T item ) => _batchBlock.Post( item );
26 | public ValueTask> TakeAsync( CancellationToken cancellationToken ) => new ValueTask>( RecieveAsync( cancellationToken ) );
27 | private async Task> RecieveAsync( CancellationToken cancellationToken ) => await _batchBlock.ReceiveAsync( cancellationToken ).ConfigureAwait( false );
28 | public void Flush() => _batchBlock.TriggerBatch();
29 |
30 | public IEnumerator> GetEnumerator()
31 | {
32 | throw new NotImplementedException();
33 | }
34 |
35 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/AsyncCollections.Benchmark/TplDataflowAdapter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using System.Threading.Tasks.Dataflow;
6 | using HellBrick.Collections;
7 |
8 | namespace HellBrick.AsyncCollections.Benchmark
9 | {
10 | class TplDataflowAdapter: IAsyncCollection
11 | {
12 | private readonly BufferBlock buffer = new BufferBlock();
13 |
14 | public int AwaiterCount
15 | {
16 | get { throw new NotSupportedException(); }
17 | }
18 |
19 | public void Add( T item )
20 | {
21 | buffer.Post( item );
22 | }
23 |
24 | public ValueTask TakeAsync( CancellationToken cancellationToken )
25 | {
26 | return new ValueTask( buffer.ReceiveAsync( cancellationToken ) );
27 | }
28 |
29 | public IEnumerator GetEnumerator()
30 | {
31 | throw new NotImplementedException();
32 | }
33 |
34 | System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
35 | {
36 | throw new NotImplementedException();
37 | }
38 |
39 | public int Count
40 | {
41 | get { throw new NotImplementedException(); }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/AsyncCollections.Benchmark/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/AsyncCollections.Test/.gitignore:
--------------------------------------------------------------------------------
1 | /bin/
2 | /obj/
3 |
--------------------------------------------------------------------------------
/AsyncCollections.Test/Assertions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using FluentAssertions;
7 | using FluentAssertions.Collections;
8 |
9 | namespace HellBrick.Collections.Test
10 | {
11 | public static class Assertions
12 | {
13 | ///
14 | /// Expects the current collection to contain all elements of in that exact order.
15 | ///
16 | public static AndConstraint> BeEqualTo( this GenericCollectionAssertions collectionShould, IReadOnlyList expected )
17 | {
18 | AndConstraint> andConstraint = collectionShould.HaveSameCount( expected );
19 |
20 | for ( int i = 0; i < expected.Count; i++ )
21 | andConstraint = collectionShould.HaveElementAt( i, expected[ i ] );
22 |
23 | return andConstraint;
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/AsyncCollections.Test/AsyncBatchQueueTest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Text;
4 | using System.Collections.Generic;
5 | using System.Threading.Tasks;
6 | using System.Threading;
7 | using FluentAssertions;
8 | using Xunit;
9 |
10 | namespace HellBrick.Collections.Test
11 | {
12 | public class AsyncBatchQueueTest
13 | {
14 | private AsyncBatchQueue _queue;
15 |
16 | [Fact]
17 | public void ThrowsOnIncorrectBatchSize()
18 | {
19 | Action act = () => _queue = new AsyncBatchQueue( 0 );
20 | act.ShouldThrow();
21 | }
22 |
23 | [Fact]
24 | public async Task FlushesWhenBatchSizeIsReached()
25 | {
26 | int[] array = { 0, 1, 42 };
27 | int index = 0;
28 |
29 | _queue = new AsyncBatchQueue( array.Length );
30 | for ( ; index < array.Length - 1; index++ )
31 | _queue.Add( array[ index ] );
32 |
33 | var takeTask = _queue.TakeAsync();
34 | takeTask.IsCompleted.Should().BeFalse();
35 |
36 | _queue.Add( array[ index ] );
37 | var batch = await takeTask.ConfigureAwait( true );
38 |
39 | batch.Should().BeEqualTo( array );
40 | }
41 |
42 | [Fact]
43 | public async Task ManualFlushWorks()
44 | {
45 | int[] array = { 0, 1, 42 };
46 |
47 | _queue = new AsyncBatchQueue( 50 );
48 | foreach ( var item in array )
49 | _queue.Add( item );
50 |
51 | _queue.Flush();
52 | var batch = await _queue.TakeAsync().ConfigureAwait( true );
53 |
54 | batch.Should().BeEqualTo( array );
55 | }
56 |
57 | [Fact]
58 | public async Task TimerFlushesPendingItems()
59 | {
60 | TimeSpan flushPeriod = TimeSpan.FromMilliseconds( 500 );
61 | var timerQueue = new AsyncBatchQueue( 9999 ).WithFlushEvery( flushPeriod );
62 | timerQueue.Add( 42 );
63 |
64 | await Task.Delay( flushPeriod + flushPeriod ).ConfigureAwait( true );
65 | var batch = await timerQueue.TakeAsync().ConfigureAwait( true );
66 | batch.Should().BeEqualTo( new[] { 42 } );
67 | }
68 |
69 | [Fact]
70 | public async Task MultithreadingInsertsDontCrash()
71 | {
72 | int insertThreads = 4;
73 | int itemsPerThread = 100;
74 |
75 | _queue = new AsyncBatchQueue( 11 );
76 |
77 | List insertTasks = Enumerable.Range( 1, insertThreads )
78 | .Select(
79 | _ => Task.Run(
80 | () =>
81 | {
82 | for ( int i = 0; i < itemsPerThread; i++ )
83 | _queue.Add( 42 );
84 | } ) )
85 | .ToList();
86 |
87 | await Task.WhenAll( insertTasks ).ConfigureAwait( true );
88 | _queue.Flush();
89 |
90 | int itemsTaken = 0;
91 | while ( _queue.Count > 0 )
92 | itemsTaken += ( await _queue.TakeAsync().ConfigureAwait( true ) ).Count;
93 |
94 | itemsTaken.Should().Be( insertThreads * itemsPerThread );
95 | }
96 |
97 | [Fact]
98 | public async Task NoRaceBetweenFlushOnAddAndOnDemand()
99 | {
100 | const int attempts = 100 * 1000;
101 | const int batchSize = 5;
102 | _queue = new AsyncBatchQueue( batchSize );
103 |
104 | for ( int attemptNumber = 0; attemptNumber < attempts; attemptNumber++ )
105 | {
106 | AddAllItemsButOne( batchSize );
107 |
108 | using ( ManualResetEvent trigger = new ManualResetEvent( initialState: false ) )
109 | {
110 | Task addTask = Task.Run
111 | (
112 | () =>
113 | {
114 | trigger.WaitOne();
115 | _queue.Add( 666 );
116 | }
117 | );
118 |
119 | Task flushTask = Task.Run
120 | (
121 | () =>
122 | {
123 | trigger.WaitOne();
124 | _queue.Flush();
125 | }
126 | );
127 |
128 | trigger.Set();
129 | await addTask.ConfigureAwait( true );
130 | await flushTask.ConfigureAwait( true );
131 |
132 | IReadOnlyList batch = await _queue.TakeAsync().ConfigureAwait( true );
133 | List allItems = batch.ToList();
134 |
135 | // This happens if Flush occurred before Add, which means there's another item from Add left unflushed.
136 | // Gotta flush once more to extract it.
137 | if ( batch.Count < batchSize )
138 | {
139 | _queue.Flush();
140 | IReadOnlyList secondBatch = await _queue.TakeAsync().ConfigureAwait( true );
141 | allItems.AddRange( secondBatch );
142 | }
143 |
144 | allItems.Count.Should().BeLessOrEqualTo( batchSize, $"Double flush detected at attempt #{attemptNumber}. Items: {String.Join( ", ", allItems )}" );
145 | }
146 | }
147 | }
148 |
149 | private void AddAllItemsButOne( int batchSize )
150 | {
151 | for ( int itemIndex = 0; itemIndex < batchSize - 1; itemIndex++ )
152 | _queue.Add( itemIndex );
153 | }
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/AsyncCollections.Test/AsyncBoundedPriorityQueueTest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using FluentAssertions;
7 | using HellBrick.Collections;
8 | using Xunit;
9 |
10 | namespace HellBrick.Collections.Test
11 | {
12 | public class AsyncBoundedPriorityQueueTest: AsyncCollectionTest>
13 | {
14 | protected override AsyncBoundedPriorityQueue CreateCollection()
15 | {
16 | return new AsyncBoundedPriorityQueue( 2 );
17 | }
18 |
19 | [Fact]
20 | public async Task ReturnsLowPriorityIfNoHighPriorityIsAvailable()
21 | {
22 | Collection.Add( 42, 1 );
23 | var result = await Collection.TakeAsync().ConfigureAwait( true );
24 | result.Should().Be( new PrioritizedItem( 42, 1 ) );
25 | }
26 |
27 | [Fact]
28 | public async Task RespectsPriority()
29 | {
30 | Collection.Add( 42, 0 );
31 | Collection.Add( 999, 1 );
32 |
33 | var result = await Collection.TakeAsync().ConfigureAwait( true );
34 | result.Should().Be( new PrioritizedItem( 42, 0 ) );
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/AsyncCollections.Test/AsyncCollectionTakeFromAnyTest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Concurrent;
3 | using System.Collections.Generic;
4 | using System.Diagnostics;
5 | using System.Linq;
6 | using System.Text;
7 | using System.Threading;
8 | using System.Threading.Tasks;
9 | using FluentAssertions;
10 | using HellBrick.Collections;
11 | using Xunit;
12 |
13 | namespace HellBrick.Collections.Test
14 | {
15 | public class AsyncCollectionTakeFromAnyTest
16 | {
17 | private readonly AsyncCollection[] _collections;
18 |
19 | public AsyncCollectionTakeFromAnyTest()
20 | {
21 | _collections = new AsyncCollection[ 2 ] { new AsyncCollection( new ConcurrentQueue() ), new AsyncCollection( new ConcurrentQueue() ) };
22 | }
23 |
24 | [Fact]
25 | public async Task ReturnsItemFromSecondIfFirstIsEmpty()
26 | {
27 | _collections[ 1 ].Add( 42 );
28 |
29 | var result = await AsyncCollection.TakeFromAnyAsync( _collections ).ConfigureAwait( true );
30 | result.Value.Should().Be( 42 );
31 | result.CollectionIndex.Should().Be( 1 );
32 | }
33 |
34 | [Fact]
35 | public async Task NoUnnecessaryAwaitersAreQueued()
36 | {
37 | _collections[ 1 ].Add( 42 );
38 |
39 | var result = await AsyncCollection.TakeFromAnyAsync( _collections ).ConfigureAwait( true );
40 | _collections[ 0 ].AwaiterCount.Should().Be( 0 );
41 | }
42 |
43 | [Fact]
44 | public async Task RespectsCollectionOrder()
45 | {
46 | _collections[ 0 ].Add( 42 );
47 | _collections[ 1 ].Add( 24 );
48 |
49 | var result = await AsyncCollection.TakeFromAnyAsync( _collections ).ConfigureAwait( true );
50 | result.Value.Should().Be( 42 );
51 | result.CollectionIndex.Should().Be( 0 );
52 | }
53 |
54 | [Fact]
55 | public async Task ReturnsItemIfItIsAddedLater()
56 | {
57 | var task = AsyncCollection.TakeFromAnyAsync( _collections );
58 | task.IsCompleted.Should().BeFalse();
59 |
60 | _collections[ 1 ].Add( 42 );
61 | var result = await task.ConfigureAwait( true );
62 | result.Value.Should().Be( 42 );
63 | result.CollectionIndex.Should().Be( 1 );
64 | }
65 |
66 | [Fact]
67 | public void CancelsTaskWhenTokenIsCanceled()
68 | {
69 | CancellationTokenSource cancelSource = new CancellationTokenSource();
70 | var task = AsyncCollection.TakeFromAnyAsync( _collections, cancelSource.Token );
71 |
72 | cancelSource.Cancel();
73 | Func asyncAct = async () => await task;
74 | asyncAct.ShouldThrow();
75 |
76 | _collections[ 0 ].Add( 42 );
77 | _collections[ 1 ].Add( 64 );
78 | _collections[ 0 ].Count.Should().Be( 1 );
79 | _collections[ 1 ].Count.Should().Be( 1 );
80 | }
81 |
82 | [Fact]
83 | public void DoesNothingIfTokenIsCanceledBeforeMethodCall()
84 | {
85 | CancellationTokenSource cancelSource = new CancellationTokenSource();
86 | cancelSource.Cancel();
87 | _collections[ 0 ].Add( 42 );
88 |
89 | var task = AsyncCollection.TakeFromAnyAsync( _collections, cancelSource.Token );
90 |
91 | task.IsCanceled.Should().BeTrue();
92 | _collections[ 0 ].Count.Should().Be( 1 );
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/AsyncCollections.Test/AsyncCollectionTest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.Linq;
5 | using System.Reflection;
6 | using System.Text;
7 | using System.Threading;
8 | using System.Threading.Tasks;
9 | using FluentAssertions;
10 | using HellBrick.Collections;
11 | using Xunit;
12 |
13 | namespace HellBrick.Collections.Test
14 | {
15 | public abstract class AsyncCollectionTest where TAsyncCollection : IAsyncCollection
16 | {
17 | protected TAsyncCollection Collection { get; }
18 |
19 | protected AsyncCollectionTest()
20 | {
21 | Collection = CreateCollection();
22 | }
23 |
24 | protected abstract TAsyncCollection CreateCollection();
25 |
26 | [Fact]
27 | public void TakingItemFromNonEmptyCollectionCompletesImmediately()
28 | {
29 | Collection.Add( 42 );
30 | var itemTask = Collection.TakeAsync( CancellationToken.None );
31 | itemTask.IsCompleted.Should().BeTrue();
32 | itemTask.Result.Should().Be( 42 );
33 | }
34 |
35 | [Fact]
36 | public async Task AddingItemCompletesPendingTask()
37 | {
38 | var itemTask = Collection.TakeAsync( CancellationToken.None );
39 | itemTask.IsCompleted.Should().BeFalse();
40 |
41 | Collection.Add( 42 );
42 | ( await itemTask.ConfigureAwait( true ) ).Should().Be( 42 );
43 | }
44 |
45 | [Fact]
46 | public void TakeWithCanceledTokenReturnsCanceledTask()
47 | {
48 | CancellationTokenSource cancelSource = new CancellationTokenSource();
49 | cancelSource.Cancel();
50 | Collection.Add( 42 );
51 |
52 | ValueTask itemTask = Collection.TakeAsync( cancelSource.Token );
53 | itemTask.IsCanceled.Should().BeTrue( "The task should have been canceled." );
54 | }
55 |
56 | [Fact]
57 | public void CancelledTakeCancelsTask()
58 | {
59 | CancellationTokenSource cancelSource = new CancellationTokenSource();
60 | var itemTask = Collection.TakeAsync( cancelSource.Token );
61 | cancelSource.Cancel();
62 |
63 | Func asyncAct = async () => await itemTask;
64 | asyncAct.ShouldThrow();
65 |
66 | Collection.Add( 42 );
67 | Collection.Count.Should().Be( 1 );
68 | Collection.AwaiterCount.Should().Be( 0 );
69 | }
70 |
71 | [Fact]
72 | public void InsertedItemsCanBeEnumerated()
73 | {
74 | int[] items = Enumerable.Range( 0, 1000 ).ToArray();
75 | foreach ( int item in items )
76 | Collection.Add( item );
77 |
78 | int[] enumeratedItems = Collection.ToArray();
79 | enumeratedItems.Should().BeEquivalentTo( items );
80 | }
81 |
82 | [Fact]
83 | public void ContinuationIsNotInlinedOnAddThread()
84 | {
85 | Task takeTask = TakeAndReturnContinuationThreadIdAsync();
86 | int addThreadID = Thread.CurrentThread.ManagedThreadId;
87 | Collection.Add( 42 );
88 | int continuationThreadID = takeTask.GetAwaiter().GetResult();
89 |
90 | addThreadID.Should().NotBe( continuationThreadID, "TakeAsync() continuation shouldn't have been inlined on the Add() thread." );
91 | }
92 |
93 | private async Task TakeAndReturnContinuationThreadIdAsync()
94 | {
95 | await Collection.TakeAsync().ConfigureAwait( false );
96 | return Thread.CurrentThread.ManagedThreadId;
97 | }
98 |
99 | [Fact]
100 | public async Task RandomMultithreadingOperationsDontCrash()
101 | {
102 | int itemsTaken = 0;
103 | int itemCount = 100;
104 | int producerThreads = 4;
105 | int totalItemCount = itemCount * producerThreads;
106 | int consumerThreads = 2;
107 | CancellationTokenSource cancelSource = new CancellationTokenSource();
108 |
109 | List consumerTasks = new List();
110 |
111 | for ( int i = 0; i < consumerThreads; i++ )
112 | {
113 | int consumerID = i;
114 | var consumerTask = Task.Run(
115 | async () =>
116 | {
117 | try
118 | {
119 | while ( itemsTaken < totalItemCount )
120 | {
121 | int item = await Collection.TakeAsync( cancelSource.Token ).ConfigureAwait( true );
122 | int itemsTakenLocal = Interlocked.Increment( ref itemsTaken );
123 | if ( itemsTakenLocal == totalItemCount )
124 | cancelSource.Cancel();
125 |
126 | Debug.WriteLine( "{0} ( - {1} by {2} )", Collection, item, consumerID );
127 | }
128 | }
129 | catch ( OperationCanceledException )
130 | {
131 | // This is expected
132 | }
133 | } );
134 |
135 | consumerTasks.Add( consumerTask );
136 | }
137 |
138 | List producerTasks = new List();
139 |
140 | for ( int i = 0; i < producerThreads; i++ )
141 | {
142 | int producerID = i;
143 |
144 | var producerTask = Task.Run(
145 | () =>
146 | {
147 | for ( int j = 0; j < itemCount; j++ )
148 | {
149 | int item = producerID * itemCount + j; // some kind of a unique item ID
150 | Collection.Add( item );
151 | Debug.WriteLine( Collection );
152 | }
153 | } );
154 |
155 | producerTasks.Add( producerTask );
156 | }
157 |
158 | await Task.WhenAll( producerTasks ).ConfigureAwait( true );
159 |
160 | await Task.WhenAll( consumerTasks ).ConfigureAwait( true );
161 | Collection.Count.Should().Be( 0 );
162 | }
163 | }
164 |
165 | public class AsyncStackTest : AsyncCollectionTest>
166 | {
167 | protected override AsyncStack CreateCollection() => new AsyncStack();
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/AsyncCollections.Test/AsyncCollections.Test.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Debug
5 | AnyCPU
6 | {EAA37D17-2AF6-447E-A568-97F061C9D1D7}
7 | Library
8 | Properties
9 | HellBrick.Collections.Test
10 | AsyncCollections.Test
11 | v4.5.1
12 | 512
13 | {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
14 | 10.0
15 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
16 | $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages
17 | False
18 | UnitTest
19 |
20 |
21 |
22 | true
23 | full
24 | false
25 | bin\Debug\
26 | DEBUG;TRACE
27 | prompt
28 | 4
29 |
30 |
31 | pdbonly
32 | true
33 | bin\Release\
34 | TRACE
35 | prompt
36 | 4
37 |
38 |
39 |
40 | ..\packages\FluentAssertions.4.5.0\lib\net45\FluentAssertions.dll
41 | True
42 |
43 |
44 | ..\packages\FluentAssertions.4.5.0\lib\net45\FluentAssertions.Core.dll
45 | True
46 |
47 |
48 |
49 |
50 |
51 | ..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll
52 | True
53 |
54 |
55 | ..\packages\xunit.assert.2.1.0\lib\dotnet\xunit.assert.dll
56 | True
57 |
58 |
59 | ..\packages\xunit.extensibility.core.2.1.0\lib\dotnet\xunit.core.dll
60 | True
61 |
62 |
63 | ..\packages\xunit.extensibility.execution.2.1.0\lib\net45\xunit.execution.desktop.dll
64 | True
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | {61be7a3d-ac66-434b-8a77-4f3466398cbc}
87 | AsyncCollections
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 | False
101 |
102 |
103 | False
104 |
105 |
106 | False
107 |
108 |
109 | False
110 |
111 |
112 |
113 |
114 |
115 |
116 |
123 |
--------------------------------------------------------------------------------
/AsyncCollections.Test/AsyncQueueTest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Threading.Tasks;
4 | using FluentAssertions;
5 | using Xunit;
6 |
7 | namespace HellBrick.Collections.Test
8 | {
9 | public class AsyncQueueTest : AsyncCollectionTest>
10 | {
11 | private const int _itemsToOverflowSegment = AsyncQueue.SegmentSize + 1;
12 |
13 | protected override AsyncQueue CreateCollection() => new AsyncQueue();
14 |
15 | [Theory]
16 | [InlineData( 0, 0 )]
17 | [InlineData( 5, 3 )]
18 | [InlineData( 3, 5 )]
19 | [InlineData( 7, 7 )]
20 | [InlineData( AsyncQueue.SegmentSize, 1 )]
21 | [InlineData( 1, AsyncQueue.SegmentSize )]
22 | [InlineData( _itemsToOverflowSegment, 1 )]
23 | [InlineData( 1, _itemsToOverflowSegment )]
24 | [InlineData( _itemsToOverflowSegment * 2, 1 )]
25 | [InlineData( 1, _itemsToOverflowSegment * 2 )]
26 | public void CountsAreCorrect( int itemsInserted, int awaitersInserted )
27 | {
28 | InsertItems( Enumerable.Range( 0, itemsInserted ).ToArray() );
29 | InsertAwaiters( awaitersInserted );
30 |
31 | int itemAwaiterBalance = itemsInserted - awaitersInserted;
32 |
33 | Collection.Count.Should().Be( Math.Max( 0, itemAwaiterBalance ) );
34 | Collection.AwaiterCount.Should().Be( Math.Max( 0, -1 * itemAwaiterBalance ) );
35 | }
36 |
37 | [Fact]
38 | public void CountsAreCorrectIfItemTailLagsBehind()
39 | {
40 | InsertAwaiters( _itemsToOverflowSegment );
41 |
42 | Collection.Count.Should().Be( 0 );
43 | Collection.AwaiterCount.Should().Be( _itemsToOverflowSegment );
44 |
45 | InsertItems( Enumerable.Range( 0, 1 ).ToArray() );
46 |
47 | Collection.Count.Should().Be( 0 );
48 | Collection.AwaiterCount.Should().Be( _itemsToOverflowSegment - 1 );
49 | }
50 |
51 | [Fact]
52 | public void CountsAreCorrectIfAwaiterTailLagsBehind()
53 | {
54 | InsertItems( Enumerable.Range( 0, _itemsToOverflowSegment ).ToArray() );
55 |
56 | Collection.AwaiterCount.Should().Be( 0 );
57 | Collection.Count.Should().Be( _itemsToOverflowSegment );
58 |
59 | InsertAwaiters( 1 );
60 |
61 | Collection.AwaiterCount.Should().Be( 0 );
62 | Collection.Count.Should().Be( _itemsToOverflowSegment - 1 );
63 | }
64 |
65 | [Fact]
66 | public void CountsAreCorrectIfTailsMatch()
67 | {
68 | InsertAwaiters( _itemsToOverflowSegment + 1 );
69 | InsertItems( Enumerable.Range( 0, _itemsToOverflowSegment ).ToArray() );
70 |
71 | Collection.Count.Should().Be( 0 );
72 | Collection.AwaiterCount.Should().Be( 1 );
73 |
74 | Collection.Add( 42 );
75 |
76 | Collection.Count.Should().Be( 0 );
77 | Collection.AwaiterCount.Should().Be( 0 );
78 |
79 | Collection.Add( 64 );
80 |
81 | Collection.Count.Should().Be( 1 );
82 | Collection.AwaiterCount.Should().Be( 0 );
83 | }
84 |
85 | [Theory]
86 | [InlineData( Order.ItemsFirst )]
87 | [InlineData( Order.AwaitersFirst )]
88 | public async Task EverythingWorksIfSegmentIsFilledByOneKindOfItems( Order insertionOrder )
89 | {
90 | int[] items = Enumerable.Range( 0, _itemsToOverflowSegment ).ToArray();
91 | ValueTask[] tasks = null;
92 |
93 | switch ( insertionOrder )
94 | {
95 | case Order.ItemsFirst:
96 | InsertItems( items );
97 | tasks = InsertAwaiters( items.Length );
98 | break;
99 |
100 | case Order.AwaitersFirst:
101 | tasks = InsertAwaiters( items.Length );
102 | InsertItems( items );
103 | break;
104 | }
105 |
106 | tasks.Should().OnlyContain( t => t.IsCompleted );
107 | int[] values = await Task.WhenAll( tasks.Select( t => t.AsTask() ) ).ConfigureAwait( true );
108 | values.Should().BeEquivalentTo( items ).And.BeInAscendingOrder();
109 | }
110 |
111 | [Fact]
112 | public void EnumeratorReturnsItemsInCorrectOrder()
113 | {
114 | int[] items = Enumerable.Range( 0, _itemsToOverflowSegment * 2 ).ToArray();
115 | InsertItems( items );
116 | Collection.Should().BeEquivalentTo( items ).And.BeInAscendingOrder();
117 | }
118 |
119 | [Fact]
120 | public void EnumeratorDoesNotReturnItemsThatHaveBeenRemovedBetweenMoveNextCalls()
121 | {
122 | InsertItems( 1, 2, 3 );
123 | using ( var enumerator = Collection.GetEnumerator() )
124 | {
125 | enumerator.MoveNext().Should().BeTrue();
126 | enumerator.Current.Should().Be( 1 );
127 |
128 | InsertAwaiters( 2 );
129 |
130 | enumerator.MoveNext().Should().BeTrue();
131 | enumerator.Current.Should().Be( 3 );
132 | }
133 | }
134 |
135 | private ValueTask[] InsertAwaiters( int awaiterCount ) => Enumerable.Repeat( 0, awaiterCount ).Select( _ => Collection.TakeAsync() ).ToArray();
136 |
137 | private void InsertItems( params int[] items )
138 | {
139 | foreach ( int item in items )
140 | Collection.Add( item );
141 | }
142 |
143 | public enum Order
144 | {
145 | ItemsFirst,
146 | AwaitersFirst
147 | }
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/AsyncCollections.Test/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle( "AsyncCollections.Test" )]
9 | [assembly: AssemblyDescription( "" )]
10 | [assembly: AssemblyConfiguration( "" )]
11 | [assembly: AssemblyCompany( "" )]
12 | [assembly: AssemblyProduct( "AsyncCollections.Test" )]
13 | [assembly: AssemblyCopyright( "Copyright © 2014" )]
14 | [assembly: AssemblyTrademark( "" )]
15 | [assembly: AssemblyCulture( "" )]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible( false )]
21 |
22 | // The following GUID is for the ID of the typelib if this project is exposed to COM
23 | [assembly: Guid( "1c03447a-1eeb-4dd7-848c-a16b6f29f3b2" )]
24 |
25 | // Version information for an assembly consists of the following four values:
26 | //
27 | // Major Version
28 | // Minor Version
29 | // Build Number
30 | // Revision
31 | //
32 | // You can specify all the values or you can default the Build and Revision Numbers
33 | // by using the '*' as shown below:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion( "1.0.0.0" )]
36 | [assembly: AssemblyFileVersion( "1.0.0.0" )]
37 |
--------------------------------------------------------------------------------
/AsyncCollections.Test/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/AsyncCollections.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 2013
4 | VisualStudioVersion = 12.0.21005.1
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AsyncCollections", "AsyncCollections\AsyncCollections.csproj", "{61BE7A3D-AC66-434B-8A77-4F3466398CBC}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AsyncCollections.Test", "AsyncCollections.Test\AsyncCollections.Test.csproj", "{EAA37D17-2AF6-447E-A568-97F061C9D1D7}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AsyncCollections.Benchmark", "AsyncCollections.Benchmark\AsyncCollections.Benchmark.csproj", "{28550F43-E353-44F0-B145-1688B7C85569}"
11 | EndProject
12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{45DBD4B2-E839-4D97-97A8-686E8E6B4645}"
13 | ProjectSection(SolutionItems) = preProject
14 | NuGet.Config = NuGet.Config
15 | EndProjectSection
16 | EndProject
17 | Global
18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
19 | Debug|Any CPU = Debug|Any CPU
20 | Release|Any CPU = Release|Any CPU
21 | EndGlobalSection
22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
23 | {61BE7A3D-AC66-434B-8A77-4F3466398CBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
24 | {61BE7A3D-AC66-434B-8A77-4F3466398CBC}.Debug|Any CPU.Build.0 = Debug|Any CPU
25 | {61BE7A3D-AC66-434B-8A77-4F3466398CBC}.Release|Any CPU.ActiveCfg = Release|Any CPU
26 | {61BE7A3D-AC66-434B-8A77-4F3466398CBC}.Release|Any CPU.Build.0 = Release|Any CPU
27 | {EAA37D17-2AF6-447E-A568-97F061C9D1D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
28 | {EAA37D17-2AF6-447E-A568-97F061C9D1D7}.Debug|Any CPU.Build.0 = Debug|Any CPU
29 | {EAA37D17-2AF6-447E-A568-97F061C9D1D7}.Release|Any CPU.ActiveCfg = Release|Any CPU
30 | {EAA37D17-2AF6-447E-A568-97F061C9D1D7}.Release|Any CPU.Build.0 = Release|Any CPU
31 | {28550F43-E353-44F0-B145-1688B7C85569}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
32 | {28550F43-E353-44F0-B145-1688B7C85569}.Debug|Any CPU.Build.0 = Debug|Any CPU
33 | {28550F43-E353-44F0-B145-1688B7C85569}.Release|Any CPU.ActiveCfg = Release|Any CPU
34 | {28550F43-E353-44F0-B145-1688B7C85569}.Release|Any CPU.Build.0 = Release|Any CPU
35 | EndGlobalSection
36 | GlobalSection(SolutionProperties) = preSolution
37 | HideSolutionNode = FALSE
38 | EndGlobalSection
39 | EndGlobal
40 |
--------------------------------------------------------------------------------
/AsyncCollections/.gitignore:
--------------------------------------------------------------------------------
1 | /bin/
2 | /obj/
3 |
--------------------------------------------------------------------------------
/AsyncCollections/AnyResult.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace HellBrick.Collections
8 | {
9 | ///
10 | /// Represents an item retrieved from one of the asynchronous collections.
11 | ///
12 | public struct AnyResult : IEquatable>
13 | {
14 | public AnyResult( T value, int collectionIndex )
15 | {
16 | Value = value;
17 | CollectionIndex = collectionIndex;
18 | }
19 |
20 | ///
21 | /// Gets the item retrieved from a collection.
22 | ///
23 | public T Value { get; }
24 |
25 | ///
26 | /// Gets the index of the collection the item was retrieved from.
27 | ///
28 | public int CollectionIndex { get; }
29 |
30 | public override int GetHashCode()
31 | {
32 | unchecked
33 | {
34 | const int prime = -1521134295;
35 | int hash = 12345701;
36 | hash = hash * prime + EqualityComparer.Default.GetHashCode( Value );
37 | hash = hash * prime + EqualityComparer.Default.GetHashCode( CollectionIndex );
38 | return hash;
39 | }
40 | }
41 |
42 | public bool Equals( AnyResult other ) => EqualityComparer.Default.Equals( Value, other.Value ) && EqualityComparer.Default.Equals( CollectionIndex, other.CollectionIndex );
43 | public override bool Equals( object obj ) => obj is AnyResult && Equals( (AnyResult) obj );
44 |
45 | public static bool operator ==( AnyResult x, AnyResult y ) => x.Equals( y );
46 | public static bool operator !=( AnyResult x, AnyResult y ) => !x.Equals( y );
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/AsyncCollections/AsyncBatchQueue.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 |
8 | namespace HellBrick.Collections
9 | {
10 | ///
11 | /// Represents a thread-safe collection that groups the items added to it into batches and allows consuming them asynchronously.
12 | ///
13 | /// The type of the items contained in the collection.
14 | public class AsyncBatchQueue : IAsyncBatchCollection
15 | {
16 | private volatile Batch _currentBatch;
17 | private readonly AsyncQueue> _batchQueue = new AsyncQueue>();
18 |
19 | ///
20 | /// Initializes a new instance of that produces batches of a specified size.
21 | ///
22 | /// Amount of the items contained in an output batch.
23 | public AsyncBatchQueue( int batchSize )
24 | {
25 | if ( batchSize <= 0 )
26 | throw new ArgumentOutOfRangeException( "batchSize", batchSize, "Batch size must be a positive integer." );
27 |
28 | BatchSize = batchSize;
29 | _currentBatch = new Batch( this );
30 | }
31 |
32 | ///
33 | /// Gets amount of items contained in an output batch.
34 | ///
35 | public int BatchSize { get; }
36 |
37 | ///
38 | /// Gets the number of flushed batches currently available for consuming.
39 | ///
40 | public int Count => _batchQueue.Count;
41 |
42 | ///
43 | /// Adds an item to the collection. Flushes the new batch to be available for consuming if amount of the pending items has reached .
44 | ///
45 | ///
46 | public void Add( T item )
47 | {
48 | SpinWait spin = new SpinWait();
49 |
50 | while ( !_currentBatch.TryAdd( item ) )
51 | spin.SpinOnce();
52 | }
53 |
54 | ///
55 | /// Removes and returns a batch from the collection in an asynchronous manner.
56 | ///
57 | public ValueTask> TakeAsync() => TakeAsync( CancellationToken.None );
58 |
59 | ///
60 | /// Removes and returns a batch from the collection in an asynchronous manner.
61 | ///
62 | public ValueTask> TakeAsync( CancellationToken cancellationToken ) => _batchQueue.TakeAsync( cancellationToken );
63 |
64 | ///
65 | /// Forces a new batch to be created and made available for consuming even if amount of the pending items has not reached yet.
66 | /// Does nothing if there are no pending items to flush.
67 | ///
68 | public void Flush()
69 | {
70 | SpinWait spin = new SpinWait();
71 | while ( !_currentBatch.TryFlush() )
72 | spin.SpinOnce();
73 | }
74 |
75 | public IEnumerator> GetEnumerator() => _batchQueue.GetEnumerator();
76 | System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
77 |
78 | private class Batch : IReadOnlyList
79 | {
80 | private readonly AsyncBatchQueue _queue;
81 | private readonly T[] _items;
82 | private readonly bool[] _finalizationFlags;
83 | private int _lastReservationIndex = -1;
84 | private int _count = -1;
85 |
86 | public Batch( AsyncBatchQueue queue )
87 | {
88 | _queue = queue;
89 | _items = new T[ _queue.BatchSize ];
90 | _finalizationFlags = new bool[ _queue.BatchSize ];
91 | }
92 |
93 | public bool TryAdd( T item )
94 | {
95 | int index = Interlocked.Increment( ref _lastReservationIndex );
96 |
97 | // The following is true if someone has beaten us to the last slot and we have to wait until the next batch comes along.
98 | if ( index >= _queue.BatchSize )
99 | return false;
100 |
101 | // The following is true if we've taken the last slot, which means we're obligated to flush the current batch and create a new one.
102 | if ( index == _queue.BatchSize - 1 )
103 | FlushInternal( _queue.BatchSize );
104 |
105 | // The full fence prevents setting finalization flag before the actual item value is written.
106 | _items[ index ] = item;
107 | Interlocked.MemoryBarrier();
108 | _finalizationFlags[ index ] = true;
109 |
110 | return true;
111 | }
112 |
113 | public bool TryFlush()
114 | {
115 | int expectedPreviousReservation = Volatile.Read( ref _lastReservationIndex );
116 |
117 | // We don't flush if the batch doesn't have any items or if another thread is about to flush it.
118 | // However, we report success to avoid unnecessary spinning.
119 | if ( expectedPreviousReservation < 0 || expectedPreviousReservation >= _queue.BatchSize - 1 )
120 | return true;
121 |
122 | int previousReservation = Interlocked.CompareExchange( ref _lastReservationIndex, _queue.BatchSize, expectedPreviousReservation );
123 |
124 | // Flush reservation has succeeded.
125 | if ( expectedPreviousReservation == previousReservation )
126 | {
127 | FlushInternal( previousReservation + 1 );
128 | return true;
129 | }
130 |
131 | // The following is true if someone has completed the batch by the time we tried to flush it.
132 | // Therefore the batch will be flushed anyway even if we don't do anything.
133 | // The opposite means someone has slipped in an update and we have to spin.
134 | return previousReservation >= _queue.BatchSize;
135 | }
136 |
137 | private void FlushInternal( int count )
138 | {
139 | _count = count;
140 | _queue._currentBatch = new Batch( _queue );
141 |
142 | // The full fence ensures that the current batch will never be added to the queue before _count is set.
143 | Interlocked.MemoryBarrier();
144 |
145 | _queue._batchQueue.Add( this );
146 | }
147 |
148 | private T GetItemWithoutValidation( int index )
149 | {
150 | SpinWait spin = new SpinWait();
151 | while ( !_finalizationFlags[ index ] )
152 | {
153 | spin.SpinOnce();
154 |
155 | // The full fence prevents caching any part of _finalizationFlags[ index ] expression.
156 | Interlocked.MemoryBarrier();
157 | }
158 |
159 | // The full fence prevents reading item value before finalization flag is set.
160 | Interlocked.MemoryBarrier();
161 | return _items[ index ];
162 | }
163 |
164 | public T this[ int index ]
165 | {
166 | get
167 | {
168 | if ( index >= Count )
169 | throw new IndexOutOfRangeException();
170 |
171 | return GetItemWithoutValidation( index );
172 | }
173 | }
174 |
175 | public int Count => _count;
176 |
177 | public IEnumerator GetEnumerator()
178 | {
179 | for ( int i = 0; i < Count; i++ )
180 | yield return GetItemWithoutValidation( i );
181 | }
182 |
183 | System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
184 | }
185 | }
186 | }
--------------------------------------------------------------------------------
/AsyncCollections/AsyncBoundedPriorityQueue.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Collections.Concurrent;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Text;
7 | using System.Threading;
8 | using System.Threading.Tasks;
9 |
10 | namespace HellBrick.Collections
11 | {
12 | ///
13 | /// Represents a thread-safe queue with a bounded number of priority levels.
14 | ///
15 | /// The type of the items contained in the queue.
16 | public class AsyncBoundedPriorityQueue : AsyncCollection>, IAsyncCollection
17 | {
18 | private readonly Func _priorityResolver;
19 |
20 | ///
21 | /// Initializes a new instance with a specified number of priority levels.
22 | /// The items will be inserted at the lowest priority by default.
23 | ///
24 | /// An amount of priority levels to support.
25 | public AsyncBoundedPriorityQueue( int priorityLevels )
26 | : this( priorityLevels, _ => priorityLevels - 1 )
27 | {
28 | }
29 |
30 | ///
31 | /// Initializes a new instance with a specified number of priority levels and a specified priority resolver.
32 | ///
33 | /// An amount of priority levels to support.
34 | /// The delegate to use to determine the default item priority.
35 | /// Must return an integer between 0 (top priority) and - 1 (low priority).
36 | ///
37 | public AsyncBoundedPriorityQueue( int priorityLevels, Func priorityResolver )
38 | : base( new ConcurrentBoundedPriorityQueue( priorityLevels ) )
39 | {
40 | PriorityLevels = priorityLevels;
41 | _priorityResolver = priorityResolver;
42 | }
43 |
44 | ///
45 | /// Gets the amount of priority levels the collection supports.
46 | ///
47 | public int PriorityLevels { get; }
48 |
49 | ///
50 | /// Adds an item to the collection at the highest priority.
51 | ///
52 | /// The item to add to the collection.
53 | public void AddTopPriority( T item ) => Add( item, 0 );
54 |
55 | ///
56 | /// Adds an item to the collection at the lowest priority.
57 | ///
58 | /// The item to add to the collection.
59 | public void AddLowPriority( T item ) => Add( item, PriorityLevels - 1 );
60 |
61 | ///
62 | /// Adds an item to the collection at a specified priority.
63 | ///
64 | /// The item to add to the collection.
65 | /// The priority of the item, with 0 being the top priority.
66 | public void Add( T item, int priority )
67 | {
68 | if ( priority < 0 || priority > PriorityLevels )
69 | throw new ArgumentOutOfRangeException( nameof( priority ), priority, $"Priority can't be less than 0 or bigger than {PriorityLevels - 1}." );
70 |
71 | Add( new PrioritizedItem( item, priority ) );
72 | }
73 |
74 | ///
75 | /// Adds an item to the collection at default priority.
76 | ///
77 | /// The item to add to the collection.
78 | public void Add( T item ) => Add( item, _priorityResolver( item ) );
79 |
80 | ///
81 | /// Removes and returns an item with the highest priority from the collection in an asynchronous manner.
82 | ///
83 | public ValueTask> TakeAsync() => TakeAsync( CancellationToken.None );
84 |
85 | ///
86 | /// Removes and returns an item with the highest priority from the collection in an asynchronous manner.
87 | ///
88 | ValueTask IAsyncCollection.TakeAsync( System.Threading.CancellationToken cancellationToken )
89 | {
90 | ValueTask> prioritizedItemTask = TakeAsync( cancellationToken );
91 | return prioritizedItemTask.IsCompletedSuccessfully ? new ValueTask( prioritizedItemTask.Result.Item ) : new ValueTask( UnwrapAsync( prioritizedItemTask ) );
92 | }
93 |
94 | private async Task UnwrapAsync( ValueTask> prioritizedItemTask )
95 | {
96 | PrioritizedItem result = await prioritizedItemTask.ConfigureAwait( false );
97 | return result.Item;
98 | }
99 |
100 | IEnumerator IEnumerable.GetEnumerator() => ( this as IEnumerable> ).Select( prioritizedItem => prioritizedItem.Item ).GetEnumerator();
101 |
102 | private class ConcurrentBoundedPriorityQueue : IProducerConsumerCollection>
103 | {
104 | private readonly ConcurrentQueue[] _itemQueues;
105 |
106 | public ConcurrentBoundedPriorityQueue( int priorityLevels )
107 | {
108 | if ( priorityLevels < 0 )
109 | throw new ArgumentOutOfRangeException( nameof( priorityLevels ), priorityLevels, "Amount of priority levels can't be less than 0." );
110 |
111 | _itemQueues = Enumerable.Range( 0, priorityLevels ).Select( _ => new ConcurrentQueue() ).ToArray();
112 | }
113 |
114 | public int Count => _itemQueues.Sum( q => q.Count );
115 |
116 | public bool TryAdd( PrioritizedItem item )
117 | {
118 | _itemQueues[ item.Priority ].Enqueue( item.Item );
119 | return true;
120 | }
121 |
122 | public bool TryTake( out PrioritizedItem item )
123 | {
124 | for ( int priority = 0; priority < _itemQueues.Length; priority++ )
125 | {
126 | T itemValue;
127 | if ( _itemQueues[ priority ].TryDequeue( out itemValue ) )
128 | {
129 | item = new PrioritizedItem( itemValue, priority );
130 | return true;
131 | }
132 | }
133 |
134 | item = default( PrioritizedItem );
135 | return false;
136 | }
137 |
138 | public IEnumerator> GetEnumerator()
139 | => _itemQueues
140 | .SelectMany( ( queue, index ) => queue.Select( item => new PrioritizedItem( item, index ) ) )
141 | .GetEnumerator();
142 |
143 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
144 | bool ICollection.IsSynchronized => false;
145 | object ICollection.SyncRoot => null;
146 |
147 | void ICollection.CopyTo( Array array, int index )
148 | {
149 | throw new NotSupportedException();
150 | }
151 |
152 | void IProducerConsumerCollection>.CopyTo( PrioritizedItem[] array, int index )
153 | {
154 | throw new NotSupportedException();
155 | }
156 |
157 | PrioritizedItem[] IProducerConsumerCollection>.ToArray()
158 | {
159 | throw new NotSupportedException();
160 | }
161 | }
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/AsyncCollections/AsyncCollection.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Concurrent;
3 | using System.Collections.Generic;
4 | using System.Diagnostics;
5 | using System.Linq;
6 | using System.Text;
7 | using System.Threading;
8 | using System.Threading.Tasks;
9 | using HellBrick.Collections.Internal;
10 |
11 | namespace HellBrick.Collections
12 | {
13 | ///
14 | /// Represents a thread-safe collection that allows asynchronous consuming.
15 | ///
16 | /// The type of the items contained in the collection.
17 | public class AsyncCollection : IAsyncCollection
18 | {
19 | private readonly IProducerConsumerCollection _itemQueue;
20 | private readonly ConcurrentQueue> _awaiterQueue = new ConcurrentQueue>();
21 |
22 | // _queueBalance < 0 means there are free awaiters and not enough items.
23 | // _queueBalance > 0 means the opposite is true.
24 | private long _queueBalance = 0;
25 |
26 | ///
27 | /// Initializes a new instance of with a specified as an underlying item storage.
28 | ///
29 | /// The collection to use as an underlying item storage. MUST NOT be accessed elsewhere.
30 | public AsyncCollection( IProducerConsumerCollection itemQueue )
31 | {
32 | _itemQueue = itemQueue;
33 | _queueBalance = _itemQueue.Count;
34 | }
35 |
36 | public int Count => _itemQueue.Count;
37 |
38 | ///
39 | /// Gets an amount of pending item requests.
40 | ///
41 | public int AwaiterCount => _awaiterQueue.Count;
42 |
43 | ///
44 | /// Adds an item to the collection.
45 | ///
46 | /// The item to add to the collection.
47 | public void Add( T item )
48 | {
49 | while ( !TryAdd( item ) ) ;
50 | }
51 |
52 | ///
53 | /// Tries to add an item to the collection.
54 | /// May fail if an awaiter that's supposed to receive the item is cancelled. If this is the case, the TryAdd() method must be called again.
55 | ///
56 | /// The item to add to the collection.
57 | /// True if the item was added to the collection; false if the awaiter was cancelled and the operation must be retried.
58 | private bool TryAdd( T item )
59 | {
60 | long balanceAfterCurrentItem = Interlocked.Increment( ref _queueBalance );
61 | SpinWait spin = new SpinWait();
62 |
63 | if ( balanceAfterCurrentItem > 0 )
64 | {
65 | // Items are dominating, so we can safely add a new item to the queue.
66 | while ( !_itemQueue.TryAdd( item ) )
67 | spin.SpinOnce();
68 |
69 | return true;
70 | }
71 | else
72 | {
73 | // There's at least one awaiter available or being added as we're speaking, so we're giving the item to it.
74 |
75 | IAwaiter awaiter;
76 |
77 | while ( !_awaiterQueue.TryDequeue( out awaiter ) )
78 | spin.SpinOnce();
79 |
80 | // Returns false if the cancellation occurred earlier.
81 | return awaiter.TrySetResult( item );
82 | }
83 | }
84 |
85 | ///
86 | /// Removes and returns an item from the collection in an asynchronous manner.
87 | ///
88 | public ValueTask TakeAsync( CancellationToken cancellationToken )
89 | => cancellationToken.IsCancellationRequested
90 | ? CanceledValueTask.Value
91 | : TakeAsync( new CompletionSourceAwaiterFactory( cancellationToken ) );
92 |
93 | private ValueTask TakeAsync( TAwaiterFactory awaiterFactory ) where TAwaiterFactory : IAwaiterFactory
94 | {
95 | long balanceAfterCurrentAwaiter = Interlocked.Decrement( ref _queueBalance );
96 |
97 | if ( balanceAfterCurrentAwaiter < 0 )
98 | {
99 | // Awaiters are dominating, so we can safely add a new awaiter to the queue.
100 | IAwaiter awaiter = awaiterFactory.CreateAwaiter();
101 | _awaiterQueue.Enqueue( awaiter );
102 | return awaiter.Task;
103 | }
104 | else
105 | {
106 | // There's at least one item available or being added, so we're returning it directly.
107 |
108 | T item;
109 | SpinWait spin = new SpinWait();
110 |
111 | while ( !_itemQueue.TryTake( out item ) )
112 | spin.SpinOnce();
113 |
114 | return new ValueTask( item );
115 | }
116 | }
117 |
118 | public override string ToString() => $"Count = {Count}, Awaiters = {AwaiterCount}";
119 |
120 | public IEnumerator GetEnumerator() => _itemQueue.GetEnumerator();
121 | System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => _itemQueue.GetEnumerator();
122 |
123 | #region Static
124 |
125 | internal const int TakeFromAnyMaxCollections = BitArray32.BitCapacity;
126 |
127 | ///
128 | /// Removes and returns an item from one of the specified collections in an asynchronous manner.
129 | ///
130 | public static ValueTask> TakeFromAnyAsync( AsyncCollection[] collections ) => TakeFromAnyAsync( collections, CancellationToken.None );
131 |
132 | ///
133 | /// Removes and returns an item from one of the specified collections in an asynchronous manner.
134 | ///
135 | public static ValueTask> TakeFromAnyAsync( AsyncCollection[] collections, CancellationToken cancellationToken )
136 | {
137 | if ( collections == null )
138 | throw new ArgumentNullException( "collections" );
139 |
140 | if ( collections.Length <= 0 || collections.Length > TakeFromAnyMaxCollections )
141 | throw new ArgumentException( String.Format( "The collection array can't contain less than 1 or more than {0} collections.", TakeFromAnyMaxCollections ), "collections" );
142 |
143 | if ( cancellationToken.IsCancellationRequested )
144 | return CanceledValueTask>.Value;
145 |
146 | ExclusiveCompletionSourceGroup exclusiveSources = new ExclusiveCompletionSourceGroup();
147 |
148 | // Fast route: we attempt to take from the top-priority queues that have any items.
149 | // If the fast route succeeds, we avoid allocating and queueing a bunch of awaiters.
150 | for ( int i = 0; i < collections.Length; i++ )
151 | {
152 | if ( collections[ i ].Count > 0 )
153 | {
154 | AnyResult? result = TryTakeFast( exclusiveSources, collections[ i ], i );
155 | if ( result.HasValue )
156 | return new ValueTask>( result.Value );
157 | }
158 | }
159 |
160 | // No luck during the fast route; just queue the rest of awaiters.
161 | for ( int i = 0; i < collections.Length; i++ )
162 | {
163 | AnyResult? result = TryTakeFast( exclusiveSources, collections[ i ], i );
164 | if ( result.HasValue )
165 | return new ValueTask>( result.Value );
166 | }
167 |
168 | // None of the collections had any items. The order doesn't matter anymore, it's time to start the competition.
169 | exclusiveSources.UnlockCompetition( cancellationToken );
170 | return new ValueTask>( exclusiveSources.Task );
171 | }
172 |
173 | private static AnyResult? TryTakeFast( ExclusiveCompletionSourceGroup exclusiveSources, AsyncCollection collection, int index )
174 | {
175 | // This can happen if the awaiter has already been created during the fast route.
176 | if ( exclusiveSources.IsAwaiterCreated( index ) )
177 | return null;
178 |
179 | ValueTask collectionTask = collection.TakeAsync( exclusiveSources.CreateAwaiterFactory( index ) );
180 |
181 | // One of the collections already had an item and returned it directly
182 | if ( collectionTask != null && collectionTask.IsCompleted )
183 | {
184 | exclusiveSources.MarkAsResolved();
185 | return new AnyResult( collectionTask.Result, index );
186 | }
187 | else
188 | return null;
189 | }
190 |
191 | #endregion
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/AsyncCollections/AsyncCollections.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Release
6 | AnyCPU
7 | {61BE7A3D-AC66-434B-8A77-4F3466398CBC}
8 | Library
9 | Properties
10 | HellBrick.Collections
11 | AsyncCollections
12 | v4.6
13 | 512
14 |
15 | {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
16 | Profile151
17 | 12.0
18 |
19 |
20 |
21 |
22 | 12.0
23 | publish\
24 | true
25 | Disk
26 | false
27 | Foreground
28 | 7
29 | Days
30 | false
31 | false
32 | true
33 | 0
34 | 1.0.0.%2a
35 | false
36 | false
37 | true
38 |
39 |
40 | true
41 | full
42 | false
43 | bin\Debug\
44 | DEBUG;TRACE
45 | prompt
46 | 4
47 |
48 |
49 | pdbonly
50 | true
51 | bin\Release\
52 | TRACE
53 | prompt
54 | 4
55 |
56 |
57 | OnBuildSuccess
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | False
98 | .NET Framework 3.5 SP1 Client Profile
99 | false
100 |
101 |
102 | False
103 | .NET Framework 3.5 SP1
104 | false
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
--------------------------------------------------------------------------------
/AsyncCollections/AsyncQueue.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Collections.Concurrent;
4 | using System.Collections.Generic;
5 | using System.Diagnostics;
6 | using System.Linq;
7 | using System.Text;
8 | using System.Threading;
9 | using System.Threading.Tasks;
10 | using HellBrick.Collections.Internal;
11 |
12 | namespace HellBrick.Collections
13 | {
14 | ///
15 | /// Represents a thread-safe queue that allows asynchronous consuming.
16 | ///
17 | /// The type of the items contained in the queue.
18 | public class AsyncQueue : IAsyncCollection
19 | {
20 | internal const int SegmentSize = 32;
21 |
22 | private Segment _itemTail;
23 | private Segment _awaiterTail;
24 |
25 | ///
26 | /// This actually points to either or , depending on which one lags behind.
27 | /// The only reason this field exists is to simplify enumerating segments for things like , or .
28 | ///
29 | private Segment _head;
30 |
31 | ///
32 | /// The value is positive if there are any active enumerators and negative if any segments are being transferred to the pool.
33 | ///
34 | private int _enumerationPoolingBalance = 0;
35 |
36 | private Segment _segmentPoolHead = null;
37 |
38 | ///
39 | /// Initializes a new empty instance of .
40 | ///
41 | public AsyncQueue()
42 | {
43 | Segment firstSegment = new Segment( this ) { SegmentID = 0 };
44 | _itemTail = firstSegment;
45 | _awaiterTail = firstSegment;
46 | _head = firstSegment;
47 | }
48 |
49 | ///
50 | /// Initializes a new instance of that contains elements copied from a specified collection.
51 | ///
52 | /// The collection whose elements are copied to the new queue.
53 | public AsyncQueue( IEnumerable collection )
54 | : this()
55 | {
56 | foreach ( T item in collection )
57 | Add( item );
58 | }
59 |
60 | public int AwaiterCount => ComputeCount( Volatile.Read( ref _awaiterTail ), Volatile.Read( ref _itemTail ), s => s.AwaiterCount );
61 | public int Count => ComputeCount( Volatile.Read( ref _itemTail ), Volatile.Read( ref _awaiterTail ), s => s.ItemCount );
62 |
63 | private int ComputeCount( Segment myTail, Segment otherTail, Func countExtractor )
64 | {
65 | if ( myTail.SegmentID < otherTail.SegmentID )
66 | return 0;
67 |
68 | if ( myTail.SegmentID == otherTail.SegmentID )
69 | return countExtractor( myTail );
70 |
71 | int count = countExtractor( myTail ) + countExtractor( otherTail );
72 | long fullMiddleSegmentCount = myTail.SegmentID - otherTail.SegmentID - 1;
73 | if ( fullMiddleSegmentCount > 0 )
74 | count += SegmentSize * (int) fullMiddleSegmentCount;
75 |
76 | return count;
77 | }
78 |
79 | public void Add( T item )
80 | {
81 | SpinWait spin = new SpinWait();
82 | while ( !Volatile.Read( ref _itemTail ).TryAdd( item ) )
83 | spin.SpinOnce();
84 | }
85 |
86 | public ValueTask TakeAsync( CancellationToken cancellationToken )
87 | => cancellationToken.IsCancellationRequested
88 | ? CanceledValueTask.Value
89 | : TakeWithoutValidationAsync( cancellationToken );
90 |
91 | private ValueTask TakeWithoutValidationAsync( CancellationToken cancellationToken )
92 | {
93 | SpinWait spin = new SpinWait();
94 |
95 | while ( true )
96 | {
97 | ValueTask? result = Volatile.Read( ref _awaiterTail ).TryTakeAsync( cancellationToken );
98 | if ( result != null )
99 | return result.Value;
100 |
101 | spin.SpinOnce();
102 | }
103 | }
104 |
105 | public Enumerator GetEnumerator()
106 | {
107 | SpinWait spin = new SpinWait();
108 |
109 | while ( true )
110 | {
111 | int oldBalance = Volatile.Read( ref _enumerationPoolingBalance );
112 | if ( oldBalance >= 0 && Interlocked.CompareExchange( ref _enumerationPoolingBalance, oldBalance + 1, oldBalance ) == oldBalance )
113 | break;
114 |
115 | spin.SpinOnce();
116 | }
117 |
118 | return new Enumerator( this );
119 | }
120 |
121 | IEnumerator IEnumerable.GetEnumerator() => new BoxedEnumerator( GetEnumerator() );
122 | IEnumerator IEnumerable.GetEnumerator() => ( this as IEnumerable ).GetEnumerator();
123 |
124 | public struct Enumerator : IEnumerator
125 | {
126 | private SelectManyStructEnumererator _innerEnumerator;
127 | private readonly AsyncQueue _queue;
128 |
129 | public Enumerator( AsyncQueue queue )
130 | {
131 | _queue = queue;
132 | _innerEnumerator = new SelectManyStructEnumererator( new SegmentEnumerator( queue ), segment => segment.GetEnumerator() );
133 | }
134 |
135 | public T Current => _innerEnumerator.Current;
136 | object IEnumerator.Current => Current;
137 |
138 | public bool MoveNext() => _innerEnumerator.MoveNext();
139 | public void Reset() => _innerEnumerator.Reset();
140 |
141 | public void Dispose()
142 | {
143 | _innerEnumerator.Dispose();
144 | Interlocked.Decrement( ref _queue._enumerationPoolingBalance );
145 | }
146 | }
147 |
148 | private struct SegmentEnumerator : IEnumerator
149 | {
150 | private readonly AsyncQueue _queue;
151 | private bool _readFirstSegment;
152 |
153 | public SegmentEnumerator( AsyncQueue queue )
154 | {
155 | _queue = queue;
156 | Current = default( Segment );
157 | _readFirstSegment = false;
158 | }
159 |
160 | public Segment Current { get; private set; }
161 | object IEnumerator.Current => Current;
162 |
163 | public bool MoveNext()
164 | {
165 | if ( !_readFirstSegment )
166 | {
167 | Current = Volatile.Read( ref _queue._head );
168 | _readFirstSegment = true;
169 | return true;
170 | }
171 |
172 | Current = Current.VolatileNext;
173 | return Current != null;
174 | }
175 |
176 | public void Dispose()
177 | {
178 | }
179 |
180 | public void Reset()
181 | {
182 | }
183 | }
184 |
185 | private class Segment : IEnumerable
186 | {
187 | private readonly T[] _items = new T[ SegmentSize ];
188 | private readonly IAwaiter[] _awaiters = new IAwaiter[ SegmentSize ];
189 | private readonly int[] _slotStates = new int[ SegmentSize ];
190 |
191 | private readonly AsyncQueue _queue;
192 | private long _segmentID;
193 |
194 | private int _awaiterIndex = -1;
195 | private int _itemIndex = -1;
196 | private Segment _next = null;
197 |
198 | private Segment _nextPooledSegment = null;
199 |
200 | public Segment( AsyncQueue queue )
201 | {
202 | _queue = queue;
203 | }
204 |
205 | public Segment VolatileNext => Volatile.Read( ref _next );
206 |
207 | public long SegmentID
208 | {
209 | get { return Volatile.Read( ref _segmentID ); }
210 | set { Volatile.Write( ref _segmentID, value ); }
211 | }
212 |
213 | public int ItemCount => Math.Max( 0, ItemAwaiterBalance );
214 | public int AwaiterCount => Math.Max( 0, -ItemAwaiterBalance );
215 |
216 | private int ItemAwaiterBalance => SlotReferenceToCount( ref _itemIndex ) - SlotReferenceToCount( ref _awaiterIndex );
217 | private int SlotReferenceToCount( ref int slotReference ) => Math.Min( SegmentSize, Volatile.Read( ref slotReference ) + 1 );
218 |
219 | public bool TryAdd( T item )
220 | => TryAdd( item, Interlocked.Increment( ref _itemIndex ) );
221 |
222 | private bool TryAdd( T item, int slot )
223 | => slot < SegmentSize
224 | && TryAddWithoutValidation( item, slot );
225 |
226 | private bool TryAddWithoutValidation( T item, int slot )
227 | {
228 | _items[ slot ] = item;
229 | bool wonSlot = Interlocked.CompareExchange( ref _slotStates[ slot ], SlotState.HasItem, SlotState.None ) == SlotState.None;
230 |
231 | /// 1. If we have won the slot, the item is considered successfully added.
232 | /// 2. Otherwise, it's up to the result of .
233 | /// Awaiter could have been canceled by now, and if it has, we should return false to insert item again into another slot.
234 | /// We also can't blindly read awaiter from the slot, because captures slot *before* filling in the awaiter.
235 | /// So we have to spin until it is available.
236 | /// And regardless of the awaiter state, we mark the slot as finished because both item and awaiter have visited it.
237 | bool success = wonSlot || TrySetAwaiterResultAndClearSlot( item, slot );
238 |
239 | HandleLastSlotCapture( slot, wonSlot, ref _queue._itemTail );
240 | return success;
241 | }
242 |
243 | private bool TrySetAwaiterResultAndClearSlot( T item, int slot )
244 | {
245 | bool success = SpinUntilAwaiterIsReady( slot ).TrySetResult( item );
246 | ClearSlot( slot );
247 | return success;
248 | }
249 |
250 | private IAwaiter SpinUntilAwaiterIsReady( int slot )
251 | {
252 | SpinWait spin = new SpinWait();
253 | while ( true )
254 | {
255 | IAwaiter awaiter = Volatile.Read( ref _awaiters[ slot ] );
256 | if ( awaiter != null )
257 | return awaiter;
258 |
259 | spin.SpinOnce();
260 | }
261 | }
262 |
263 | public ValueTask? TryTakeAsync( CancellationToken cancellationToken )
264 | => TryTakeAsync( cancellationToken, Interlocked.Increment( ref _awaiterIndex ) );
265 |
266 | private ValueTask? TryTakeAsync( CancellationToken cancellationToken, int slot )
267 | => slot < SegmentSize
268 | ? TryTakeWithoutValidationAsync( cancellationToken, slot )
269 | : (ValueTask?) null;
270 |
271 | private ValueTask TryTakeWithoutValidationAsync( CancellationToken cancellationToken, int slot )
272 | {
273 | ValueTask result;
274 |
275 | /// The order here differs from what does: we capture the slot *before* inserting an awaiter.
276 | /// We do it to avoid allocating an awaiter / registering the cancellation that we're not gonna need in case we lose.
277 | /// This means can see the default awaiter value, but it is easily solved by spinning until the awaiter is assigned.
278 | bool wonSlot = Interlocked.CompareExchange( ref _slotStates[ slot ], SlotState.HasAwaiter, SlotState.None ) == SlotState.None;
279 | if ( wonSlot )
280 | {
281 | IAwaiter awaiter = new CompletionSourceAwaiterFactory( cancellationToken ).CreateAwaiter();
282 | Volatile.Write( ref _awaiters[ slot ], awaiter );
283 | result = awaiter.Task;
284 | }
285 | else
286 | {
287 | result = new ValueTask( _items[ slot ] );
288 | ClearSlot( slot );
289 | }
290 |
291 | HandleLastSlotCapture( slot, wonSlot, ref _queue._awaiterTail );
292 | return result;
293 | }
294 |
295 | private void ClearSlot( int slot )
296 | {
297 | Volatile.Write( ref _slotStates[ slot ], SlotState.Cleared );
298 | Volatile.Write( ref _awaiters[ slot ], null );
299 | _items[ slot ] = default( T );
300 | }
301 |
302 | ///
303 | /// Here comes the tricky part.
304 | /// 0. We only care if we've captured exactly the last slot, so only one thread performs the segment maintenance.
305 | /// 1. Whether the slot is lost or won, the next time the same kind of item is inserted, there's no point in looking for a slot at the current segment.
306 | /// So we have to advance or , depending on the kind of item we're working on right now.
307 | /// 2. The last slot is captured twice: by an item and by an awaiter. We obviously should to grow a segment only once, so only the winner does it.
308 | /// 3. If we've lost the last slot, it's still possible the next segment is not grown yet, so we have to spin.
309 | /// 4. If we've lost the last slot, it means we're done with the current segment: all items and all awaiters have annihilated each other.
310 | /// and are 0 now, so the segment can't contribute to or .
311 | /// So we lose the reference to it by advancing .
312 | /// 5. If we've lost the last slot, we pool it to be reused later.
313 | ///
314 | /// Either or , whichever we're working on right now.
315 | private void HandleLastSlotCapture( int slot, bool wonSlot, ref Segment tailReference )
316 | {
317 | if ( !IsLastSlot( slot ) )
318 | return;
319 |
320 | Segment nextSegment = wonSlot ? GrowSegment() : SpinUntilNextSegmentIsReady();
321 | Volatile.Write( ref tailReference, nextSegment );
322 |
323 | if ( wonSlot )
324 | return;
325 |
326 | Volatile.Write( ref _queue._head, nextSegment );
327 | TryPoolSegment();
328 | }
329 |
330 | private void TryPoolSegment()
331 | {
332 | if ( !TryDecreaseBalance() )
333 | return;
334 |
335 | /// We reset so it could be GC-ed if it doesn't make it to the pool.
336 | /// It's safe to do so because:
337 | /// 1. guarantees that the whole queue is not being enumerated right now.
338 | /// 2. By this time is already rewritten so future enumerators can't possibly reference the current segment.
339 | /// The rest of the clean-up is *NOT* safe to do here, see for details.
340 | Volatile.Write( ref _next, null );
341 | PushToPool();
342 | Interlocked.Increment( ref _queue._enumerationPoolingBalance );
343 | }
344 |
345 | private bool TryDecreaseBalance()
346 | {
347 | SpinWait spin = new SpinWait();
348 | while ( true )
349 | {
350 | int enumeratorPoolBalance = Volatile.Read( ref _queue._enumerationPoolingBalance );
351 |
352 | // If the balance is positive, we have some active enumerators and it's dangerous to pool the segment right now.
353 | // We can't spin until the balance is restored either, because we have no guarantee that enumerators will be disposed soon (or will be disposed at all).
354 | // So we have no choice but to give up on pooling the segment.
355 | if ( enumeratorPoolBalance > 0 )
356 | return false;
357 |
358 | if ( Interlocked.CompareExchange( ref _queue._enumerationPoolingBalance, enumeratorPoolBalance - 1, enumeratorPoolBalance ) == enumeratorPoolBalance )
359 | return true;
360 |
361 | spin.SpinOnce();
362 | }
363 | }
364 |
365 | private void PushToPool()
366 | {
367 | SpinWait spin = new SpinWait();
368 | while ( true )
369 | {
370 | Segment oldHead = Volatile.Read( ref _queue._segmentPoolHead );
371 | Volatile.Write( ref _nextPooledSegment, oldHead );
372 |
373 | if ( Interlocked.CompareExchange( ref _queue._segmentPoolHead, this, oldHead ) == oldHead )
374 | break;
375 |
376 | spin.SpinOnce();
377 | }
378 | }
379 |
380 | private Segment TryPopSegmentFromPool()
381 | {
382 | SpinWait spin = new SpinWait();
383 | while ( true )
384 | {
385 | Segment oldHead = Volatile.Read( ref _queue._segmentPoolHead );
386 | if ( oldHead == null )
387 | return null;
388 |
389 | if ( Interlocked.CompareExchange( ref _queue._segmentPoolHead, oldHead._nextPooledSegment, oldHead ) == oldHead )
390 | {
391 | Volatile.Write( ref oldHead._nextPooledSegment, null );
392 | return oldHead;
393 | }
394 |
395 | spin.SpinOnce();
396 | }
397 | }
398 |
399 | ///
400 | /// It's possible for the appenders to read the tail reference before it's updated and to try appending to the segment while it's being pooled.
401 | /// There's no cheap way to prevent it, so we have to be prepared that an append can succeed as fast as we reset or .
402 | /// This means this method must *NOT* be called on putting the segment into the pool, because it could lead to items/awaiters being stored somewhere in the pool.
403 | /// Since there's no guarantee that the segment will ever be reused, this effectively means losing data (or, in the best-case scenario, completely screwing up the item order).
404 | /// We prevent this disaster by calling this method on taking segment from the pool, instead of putting it there.
405 | /// This way even if such append happens, we're about the reattach the segment to the queue and the data won't be lost.
406 | ///
407 | private Segment ResetAfterTakingFromPool()
408 | {
409 | /// We must reset before and .
410 | /// Otherwise appenders could successfully increment a pointer and mess with the slots before they are ready to be messed with.
411 | for ( int i = 0; i < SegmentSize; i++ )
412 | {
413 | /// We can't simply overwrite the state, because it's possible that the slot loser has not finished yet.
414 | SpinWait spin = new SpinWait();
415 | while ( Interlocked.CompareExchange( ref _slotStates[ i ], SlotState.None, SlotState.Cleared ) != SlotState.Cleared )
416 | spin.SpinOnce();
417 | }
418 |
419 | Volatile.Write( ref _awaiterIndex, -1 );
420 | Volatile.Write( ref _itemIndex, -1 );
421 |
422 | return this;
423 | }
424 |
425 | private static bool IsLastSlot( int slot ) => slot == SegmentSize - 1;
426 |
427 | private Segment SpinUntilNextSegmentIsReady()
428 | {
429 | SpinWait spin = new SpinWait();
430 | while ( VolatileNext == null )
431 | spin.SpinOnce();
432 |
433 | return VolatileNext;
434 | }
435 |
436 | private Segment GrowSegment()
437 | {
438 | Segment newTail = TryPopSegmentFromPool()?.ResetAfterTakingFromPool() ?? new Segment( _queue );
439 | newTail.SegmentID = _segmentID + 1;
440 | Volatile.Write( ref _next, newTail );
441 | return newTail;
442 | }
443 |
444 | private int SpinUntilStateIsResolvedAndReturnState( int slot )
445 | {
446 | SpinWait spin = new SpinWait();
447 | int slotState;
448 | while ( true )
449 | {
450 | slotState = Volatile.Read( ref _slotStates[ slot ] );
451 | if ( slotState != SlotState.None )
452 | break;
453 |
454 | spin.SpinOnce();
455 | }
456 |
457 | return slotState;
458 | }
459 |
460 | public Enumerator GetEnumerator() => new Enumerator( this );
461 | IEnumerator IEnumerable.GetEnumerator() => new BoxedEnumerator( GetEnumerator() );
462 | IEnumerator IEnumerable.GetEnumerator() => ( this as IEnumerable ).GetEnumerator();
463 |
464 | private static class SlotState
465 | {
466 | public const int None = 0;
467 | public const int HasItem = 1;
468 | public const int HasAwaiter = 2;
469 | public const int Cleared = 3;
470 | }
471 |
472 | public struct Enumerator : IEnumerator
473 | {
474 | private readonly Segment _segment;
475 | private int _currentSlot;
476 | private int _effectiveLength;
477 |
478 | public Enumerator( Segment segment )
479 | {
480 | _segment = segment;
481 | _currentSlot = Int32.MinValue;
482 | _effectiveLength = Int32.MinValue;
483 | Current = default( T );
484 | }
485 |
486 | public T Current { get; private set; }
487 | object IEnumerator.Current => Current;
488 |
489 | public bool MoveNext()
490 | {
491 | if ( _currentSlot == Int32.MinValue )
492 | {
493 | /// Items in slots 0 .. are taken by awaiters, so they are no longer considered stored in the queue.
494 | _currentSlot = _segment.SlotReferenceToCount( ref _segment._awaiterIndex );
495 |
496 | /// is the last slot an item actually exists at at the moment, so we shouldn't enumerate through the default values that are stored further.
497 | _effectiveLength = _segment.SlotReferenceToCount( ref _segment._itemIndex );
498 | }
499 |
500 | while ( _currentSlot < _effectiveLength )
501 | {
502 | int slotState = _segment.SpinUntilStateIsResolvedAndReturnState( _currentSlot );
503 | Current = _segment._items[ _currentSlot ];
504 | _currentSlot++;
505 |
506 | if ( slotState == SlotState.HasItem )
507 | return true;
508 | }
509 |
510 | return false;
511 | }
512 |
513 | public void Dispose()
514 | {
515 | }
516 |
517 | public void Reset()
518 | {
519 | }
520 | }
521 | }
522 | }
523 | }
524 |
--------------------------------------------------------------------------------
/AsyncCollections/AsyncStack.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Concurrent;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 |
8 | namespace HellBrick.Collections
9 | {
10 | ///
11 | /// Represents a thread-safe stack that allows asynchronous consuming.
12 | ///
13 | /// The type of the items contained in the stack.
14 | public class AsyncStack : AsyncCollection
15 | {
16 | ///
17 | /// Initializes a new empty instance of .
18 | ///
19 | public AsyncStack() : base( new ConcurrentStack() ) { }
20 |
21 | ///
22 | /// Initializes a new instance of that contains elements copied from a specified collection.
23 | ///
24 | /// The collection whose elements are copied to the new stack.
25 | public AsyncStack( IEnumerable collection ) : base( new ConcurrentStack( collection ) ) { }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/AsyncCollections/IAsyncBatchCollection.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 |
6 | namespace HellBrick.Collections
7 | {
8 | public interface IAsyncBatchCollection : IReadOnlyCollection>
9 | {
10 | int BatchSize { get; }
11 |
12 | void Add( T item );
13 | void Flush();
14 | ValueTask> TakeAsync( CancellationToken cancellationToken );
15 | }
16 |
17 | public static class AsyncBatchCollectionExtensions
18 | {
19 | public static ValueTask> TakeAsync( this IAsyncBatchCollection collection ) => collection.TakeAsync( CancellationToken.None );
20 | public static TimerAsyncBatchQueue WithFlushEvery( this IAsyncBatchCollection collection, TimeSpan flushPeriod ) => new TimerAsyncBatchQueue( collection, flushPeriod );
21 | }
22 | }
--------------------------------------------------------------------------------
/AsyncCollections/IAsyncCollection.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 |
6 | namespace HellBrick.Collections
7 | {
8 | ///
9 | /// Represents a thread-safe collection that allows asynchronous consuming.
10 | ///
11 | /// The type of the items contained in the collection.
12 | public interface IAsyncCollection : IReadOnlyCollection
13 | {
14 | ///
15 | /// Gets an amount of pending item requests.
16 | ///
17 | int AwaiterCount { get; }
18 |
19 | ///
20 | /// Adds an item to the collection.
21 | ///
22 | /// The item to add to the collection.
23 | void Add( T item );
24 |
25 | ///
26 | /// Removes and returns an item from the collection in an asynchronous manner.
27 | ///
28 | ValueTask TakeAsync( CancellationToken cancellationToken );
29 | }
30 |
31 | public static class AsyncCollectionExtensions
32 | {
33 | ///