Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions CameraScanner.Maui/Platforms/Android/BarcodeAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,7 @@ public void Analyze(IImageProxy proxyImage)
{
Task.Run(async () =>
{
var run = await this.syncHelper.RunOnceAsync(() => this.cameraManager.PerformBarcodeDetectionAsync(proxyImage));
if (run == false)
{
// this.logger.LogDebug("Analyze -> frame skipped (already in progress)");
}
await this.syncHelper.RunOnceAsync(() => this.cameraManager.PerformBarcodeDetectionAsync(proxyImage));
}).Wait();
}

Expand Down Expand Up @@ -96,4 +92,4 @@ public void Analyze(IImageProxy proxyImage)
}
}
}
}
}
233 changes: 203 additions & 30 deletions CameraScanner.Maui/Utils/SyncHelper.cs
Original file line number Diff line number Diff line change
@@ -1,62 +1,235 @@
namespace CameraScanner.Maui.Utils
using System.Diagnostics;

namespace CameraScanner.Maui.Utils
{
internal class SyncHelper
{
private const int NotRunning = 0;
private const int Running = 1;
private int currentState;
private static readonly object NoResult = new();
private readonly object syncRoot = new();
private Task<object>? currentExecutionTask;
private bool currentExecutionHasResult;

/// <summary>
/// Indicates if the current instance of <seealso cref="SyncHelper"/> is currently running.
/// </summary>
public bool IsRunning
{
get
{
lock (this.syncRoot)
{
return this.currentExecutionTask != null;
}
}
}

/// <summary>
/// Runs the given <paramref name="action"/> only once at a time.
/// </summary>
/// <param name="action">The synchronous action.</param>
public void RunOnce(Action action)
{
if (!this.TryBeginExecution(hasResult: false, out var execution))
{
return;
}

var id = $"{Guid.NewGuid():N}".Substring(0, 5).ToUpperInvariant();
Debug.WriteLine($"RunOnce: Task {id} started");

try
{
action();
execution.SetResult(NoResult);
}
catch (Exception ex)
{
execution.SetException(ex);
throw;
}
finally
{
Debug.WriteLine($"RunOnce: Task {id} finished");
this.EndExecution(execution.Task);
}
}

/// <summary>
/// Runs the given <paramref name="task"/> only once at a time.
/// </summary>
/// <param name="task">The asynchronous task.</param>
public async Task RunOnceAsync(Func<Task> task)
{
if (!this.TryBeginExecution(hasResult: false, out var execution))
{
return;
}

var id = $"{Guid.NewGuid():N}".Substring(0, 5).ToUpperInvariant();
Debug.WriteLine($"RunOnceAsync: Task {id} started");

try
{
await task().ConfigureAwait(false);
execution.SetResult(NoResult);
}
catch (Exception ex)
{
execution.SetException(ex);
throw;
}
finally
{
Debug.WriteLine($"RunOnceAsync: Task {id} finished");
this.EndExecution(execution.Task);
}
}

public async Task<bool> RunOnceAsync(Func<Task> task)
/// <summary>
/// Runs the given <paramref name="function"/> only once at a time.
/// </summary>
/// <param name="function">The synchronous function which returns a result of type <typeparamref name="T"/>.</param>
public T RunOnce<T>(Func<T> function)
{
var run = Interlocked.CompareExchange(ref this.currentState, Running, NotRunning) == NotRunning;
if (run)
while (true)
{
// The given task is only executed if we pass this atomic CompareExchange call,
// which switches the current state flag from 'not running' to 'running'.
var joinMode = this.TryJoinOrBeginResultExecution(out var executionTask, out var execution);
if (joinMode == JoinMode.WaitForResult)
{
return (T)executionTask.GetAwaiter().GetResult();
}

if (joinMode == JoinMode.WaitForCompletion)
{
executionTask.GetAwaiter().GetResult();
continue;
}

var id = $"{Guid.NewGuid():N}".Substring(0, 5).ToUpperInvariant();
Debug.WriteLine($"RunOnce: Task {id} started");

try
{
await task();
var result = function();
execution.SetResult(result!);
return result;
}
catch (Exception ex)
{
execution.SetException(ex);
throw;
}
finally
{
Interlocked.Exchange(ref this.currentState, NotRunning);
Debug.WriteLine($"RunOnce: Task {id} finished");
this.EndExecution(execution.Task);
}
}
else
{
// All other method calls which can't make it into the critical section
// are just returned immediately.
}

return run;
}

public bool RunOnce(Action action)
/// <summary>
/// Runs the given <paramref name="task"/> only once at a time.
/// </summary>
/// <param name="task">The asynchronous task which returns a result of type <typeparamref name="T"/>.</param>
public async Task<T> RunOnceAsync<T>(Func<Task<T>> task)
{
var run = Interlocked.CompareExchange(ref this.currentState, Running, NotRunning) == NotRunning;
if (run)
while (true)
{
// The given task is only executed if we pass this atomic CompareExchange call,
// which switches the current state flag from 'not running' to 'running'.
var joinMode = this.TryJoinOrBeginResultExecution(out var executionTask, out var execution);
if (joinMode == JoinMode.WaitForResult)
{
return (T)await executionTask.ConfigureAwait(false);
}

if (joinMode == JoinMode.WaitForCompletion)
{
await executionTask.ConfigureAwait(false);
continue;
}

var id = $"{Guid.NewGuid():N}".Substring(0, 5).ToUpperInvariant();
Debug.WriteLine($"RunOnceAsync: Task {id} started");

try
{
action();
var result = await task().ConfigureAwait(false);
execution.SetResult(result!);
return result;
}
catch (Exception ex)
{
execution.SetException(ex);
throw;
}
finally
{
Interlocked.Exchange(ref this.currentState, NotRunning);
Debug.WriteLine($"RunOnceAsync: Task {id} finished");
this.EndExecution(execution.Task);
}
}
}

private bool TryBeginExecution(bool hasResult, out TaskCompletionSource<object> execution)
{
lock (this.syncRoot)
{
if (this.currentExecutionTask != null)
{
execution = null!;
return false;
}

execution = this.CreateExecutionSource();
this.currentExecutionTask = execution.Task;
this.currentExecutionHasResult = hasResult;
return true;
}
}

private JoinMode TryJoinOrBeginResultExecution(out Task<object> executionTask, out TaskCompletionSource<object> execution)
{
lock (this.syncRoot)
{
var currentExecutionTask = this.currentExecutionTask;
if (currentExecutionTask == null)
{
execution = this.CreateExecutionSource();
this.currentExecutionTask = execution.Task;
this.currentExecutionHasResult = true;
executionTask = execution.Task;
return JoinMode.BeganExecution;
}

executionTask = currentExecutionTask;
execution = null!;
return this.currentExecutionHasResult
? JoinMode.WaitForResult
: JoinMode.WaitForCompletion;
}
else
}

private void EndExecution(Task<object> executionTask)
{
lock (this.syncRoot)
{
// All other method calls which can't make it into the critical section
// are just returned immediately.
if (ReferenceEquals(this.currentExecutionTask, executionTask))
{
this.currentExecutionTask = null;
this.currentExecutionHasResult = false;
}
}
}

return run;
private TaskCompletionSource<object> CreateExecutionSource()
{
return new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
}

private enum JoinMode
{
BeganExecution,
WaitForCompletion,
WaitForResult,
}
}
}

2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2025 Thomas Galliker
Copyright (c) 2026 Thomas Galliker

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
Loading