Skip to content

Commit ce9e329

Browse files
committed
Add AsyncBuffer
1 parent 3563c62 commit ce9e329

File tree

7 files changed

+159
-0
lines changed

7 files changed

+159
-0
lines changed
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LockFreeRingBuffer", "LockFreeRingBuffer\LockFreeRingBuffer.csproj", "{F6BEC6D2-D968-4252-AFD9-1BE0F78E9D29}"
4+
EndProject
5+
Global
6+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
7+
Debug|Any CPU = Debug|Any CPU
8+
Release|Any CPU = Release|Any CPU
9+
EndGlobalSection
10+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
11+
{F6BEC6D2-D968-4252-AFD9-1BE0F78E9D29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
12+
{F6BEC6D2-D968-4252-AFD9-1BE0F78E9D29}.Debug|Any CPU.Build.0 = Debug|Any CPU
13+
{F6BEC6D2-D968-4252-AFD9-1BE0F78E9D29}.Release|Any CPU.ActiveCfg = Release|Any CPU
14+
{F6BEC6D2-D968-4252-AFD9-1BE0F78E9D29}.Release|Any CPU.Build.0 = Release|Any CPU
15+
EndGlobalSection
16+
EndGlobal
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
public sealed class AsyncLogger : IAsyncDisposable
2+
{
3+
private readonly LockFreeRingBuffer<string> _ringBuffer;
4+
private readonly CancellationTokenSource _cancellationTokenSource;
5+
private readonly ManualResetEvent _newMessageEvent;
6+
private readonly Task _logProcessorTask;
7+
private bool _disposed;
8+
9+
public AsyncLogger()
10+
{
11+
_ringBuffer = new LockFreeRingBuffer<string>(2);
12+
_cancellationTokenSource = new CancellationTokenSource();
13+
_newMessageEvent = new ManualResetEvent(false);
14+
_logProcessorTask = Task.Run(ProcessLogs);
15+
}
16+
17+
public void Log(string message)
18+
{
19+
ObjectDisposedException.ThrowIf(_disposed, this);
20+
21+
while (!_ringBuffer.TryWrite(message))
22+
{
23+
// Handle buffer being full, e.g., wait, retry, or drop the message.
24+
}
25+
26+
_newMessageEvent.Set();
27+
}
28+
29+
private void ProcessLogs()
30+
{
31+
while (!_cancellationTokenSource.IsCancellationRequested)
32+
{
33+
_newMessageEvent.WaitOne();
34+
ProcessAllAvailableMessages();
35+
_newMessageEvent.Reset();
36+
}
37+
38+
// Final flush of all messages before exiting
39+
ProcessAllAvailableMessages();
40+
}
41+
42+
private void ProcessAllAvailableMessages()
43+
{
44+
while (_ringBuffer.TryRead(out var logMessage))
45+
{
46+
// Process the log message
47+
Console.WriteLine(logMessage);
48+
}
49+
}
50+
51+
public async ValueTask DisposeAsync()
52+
{
53+
await _cancellationTokenSource.CancelAsync();
54+
_newMessageEvent.Set(); // Ensure the log processing task wakes up to process remaining messages
55+
await _logProcessorTask;
56+
_cancellationTokenSource.Dispose();
57+
_newMessageEvent.Close();
58+
59+
_disposed = true;
60+
}
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
public class LockFreeRingBuffer<T> where T : class
2+
{
3+
private readonly T[] _buffer;
4+
private readonly int _capacity;
5+
private int _head;
6+
private int _tail;
7+
8+
public LockFreeRingBuffer(int capacity)
9+
{
10+
_capacity = capacity;
11+
_buffer = new T[_capacity];
12+
_head = 0;
13+
_tail = 0;
14+
}
15+
16+
public bool TryWrite(T value)
17+
{
18+
do
19+
{
20+
var currentTail = _tail;
21+
var nextTail = (currentTail + 1) % _capacity;
22+
23+
// Check if the buffer is full
24+
if (nextTail == Volatile.Read(ref _head))
25+
{
26+
return false;
27+
}
28+
29+
// Attempt to update the _tail index atomically
30+
if (Interlocked.CompareExchange(ref _tail, nextTail, currentTail) == currentTail)
31+
{
32+
_buffer[currentTail] = value;
33+
return true;
34+
}
35+
}
36+
while (true);
37+
}
38+
39+
public bool TryRead(out T? value)
40+
{
41+
do
42+
{
43+
var currentHead = _head;
44+
if (currentHead == Volatile.Read(ref _tail))
45+
{
46+
value = default;
47+
return false;
48+
}
49+
50+
// Attempt to update the _head index atomically
51+
var item = _buffer[currentHead];
52+
if (Interlocked.CompareExchange(ref _head, (currentHead + 1) % _capacity, currentHead) == currentHead)
53+
{
54+
value = item;
55+
return true;
56+
}
57+
}
58+
while (true);
59+
}
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
10+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
await using var logger = new AsyncLogger();
2+
logger.Log("Info: Application started");
3+
logger.Log("Warning: Low memory");
4+
logger.Log("Error: Out of memory");
5+
logger.Log("Debug: Memory usage: 1.5 GB");
6+
logger.Log("Info: Application stopped");

LockFreeRingBuffer/README.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# An asynchronous lock free ring buffer for logging
2+
3+
In this blog post, I showcase a very simple lock-free ring buffer for logging. I'll show why it's useful and how it works.
4+
5+
Found [here](https://steven-giesel.com/blogPost/11f0ded8-7119-4cfc-b7cf-317ff73fb671)

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Contains all of my examples from various blog posts. You can find a comprehensiv
44

55
| BlogPost | Publish Date |
66
| -------------------------------------------------------------------------------------------------------------- | ------------ |
7+
| [An asynchronous lock free ring buffer for logging](LockFreeRingBuffer/) | 03.01.2024 |
78
| [Blazor .NET 8 - Enhanced Form Navigation](BlazorStaticForms/) | 22.11.2023 |
89
| [Redux Pattern in Blazor](ReduxBlazor/) | 04.11.2023 |
910
| [Blazor with TailwindCSS](BlazorWithTailwind/) | 18.10.2023 |

0 commit comments

Comments
 (0)