// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Threading;
using System.Threading.Tasks;
namespace QtVsTools
{
///
/// Base class of objects requiring thread-safety features
///
///
[DataContract]
public abstract class Concurrent
where TSubClass : Concurrent
{
// Shared static critical section used by non-resource methods (like ThreadSafeInit).
protected static object StaticCriticalSection { get; } = new();
// Per-instance critical section to lock per instance rather than global.
protected object CriticalSection { get; } = new();
protected static ConcurrentDictionary Resources { get; } = new();
protected sealed class Resource
{
public object SyncRoot { get; } = new();
public bool IsLocked { get; set; }
}
// Optionally remove the resource from the dictionary entirely.
protected static void Free(string resourceName)
{
Resources.TryRemove(resourceName, out _);
}
protected static bool Get(string resourceName, int timeout = -1)
{
var resource = Resources.GetOrAdd(resourceName, _ => new Resource());
lock (resource.SyncRoot) {
if (timeout < 0) {
// Infinite wait
while (resource.IsLocked)
Monitor.Wait(resource.SyncRoot);
resource.IsLocked = true;
return true;
}
// Timed wait
var startTime = Environment.TickCount;
while (resource.IsLocked) {
var elapsed = Environment.TickCount - startTime;
// Guard against overflow
if (elapsed < 0)
elapsed = int.MaxValue;
var remaining = timeout - elapsed;
if (remaining <= 0 || !Monitor.Wait(resource.SyncRoot, remaining))
return false; // Timed out
}
resource.IsLocked = true;
return true;
}
}
protected static async Task GetAsync(string resourceName, int timeout = -1)
{
return await Task.Run(() => Get(resourceName, timeout));
}
protected static void Release(string resourceName)
{
if (!Resources.TryGetValue(resourceName, out var resource))
return;
lock (resource.SyncRoot) {
if (!resource.IsLocked)
return;
resource.IsLocked = false;
Monitor.Pulse(resource.SyncRoot);
}
}
protected T ThreadSafeInit(Func getValue, Action init)
where T : class
{
return StaticThreadSafeInit(getValue, init, this);
}
protected static T StaticThreadSafeInit(
Func getValue,
Action init,
Concurrent instance = null)
where T : class
{
// prevent global lock at every call
var value = getValue();
if (value != null)
return value;
lock (instance?.CriticalSection ?? StaticCriticalSection) {
// prevent race conditions
value = getValue();
if (value != null)
return value;
init();
value = getValue();
return value;
}
}
protected void EnterCriticalSection()
{
EnterStaticCriticalSection(this);
}
protected bool TryEnterCriticalSection()
{
return TryEnterStaticCriticalSection(this);
}
protected static void EnterStaticCriticalSection(Concurrent instance = null)
{
Monitor.Enter(instance?.CriticalSection ?? StaticCriticalSection);
}
protected static bool TryEnterStaticCriticalSection(Concurrent instance = null)
{
return Monitor.TryEnter(instance?.CriticalSection ?? StaticCriticalSection);
}
protected void LeaveCriticalSection()
{
LeaveStaticCriticalSection(this);
}
protected static void LeaveStaticCriticalSection(Concurrent instance = null)
{
if (Monitor.IsEntered(instance?.CriticalSection ?? StaticCriticalSection))
Monitor.Exit(instance?.CriticalSection ?? StaticCriticalSection);
}
protected void AbortCriticalSection()
{
AbortStaticCriticalSection(this);
}
protected static void AbortStaticCriticalSection(Concurrent instance = null)
{
while (Monitor.IsEntered(instance?.CriticalSection ?? StaticCriticalSection))
Monitor.Exit(instance?.CriticalSection ?? StaticCriticalSection);
}
protected void ThreadSafe(Action action)
{
StaticThreadSafe(action, this);
}
protected static void StaticThreadSafe(Action action, Concurrent instance = null)
{
lock (instance?.CriticalSection ?? StaticCriticalSection) {
action();
}
}
protected T ThreadSafe(Func func)
{
return StaticThreadSafe(func, this);
}
protected static T StaticThreadSafe(Func func, Concurrent instance = null)
{
lock (instance?.CriticalSection ?? StaticCriticalSection) {
return func();
}
}
protected bool Atomic(Func test, Action action)
{
return StaticAtomic(test, action, instance: this);
}
protected bool Atomic(Func test, Action action, Action actionElse)
{
return StaticAtomic(test, action, actionElse, this);
}
protected static bool StaticAtomic(
Func test,
Action action,
Action actionElse = null,
Concurrent instance = null)
{
bool success;
lock (instance?.CriticalSection ?? StaticCriticalSection) {
success = test();
if (success)
action();
else
actionElse?.Invoke();
}
return success;
}
}
///
/// Base class of objects requiring thread-safety features
/// Sub-classes will share the same static critical section
///
///
[DataContract]
public class Concurrent : Concurrent
{
}
///
/// Simplify use of synchronization features in classes that are not Concurrent-based.
///
///
public sealed class Synchronized : Concurrent
{
private Synchronized() { }
public static new bool Atomic(Func test, Action action)
{
return StaticAtomic(test, action);
}
public static new bool Atomic(Func test, Action action, Action actionElse)
{
return StaticAtomic(test, action, actionElse);
}
public static new void ThreadSafe(Action action)
{
StaticThreadSafe(action);
}
public static new T ThreadSafe(Func func)
{
return StaticThreadSafe(func);
}
public static new void Free(string resourceName)
{
Concurrent.Free(resourceName);
}
public static new bool Get(string resourceName, int timeout = -1)
{
return Concurrent.Get(resourceName, timeout);
}
public static new Task GetAsync(string resourceName, int timeout = -1)
{
return Concurrent.GetAsync(resourceName, timeout);
}
public static new void Release(string resourceName)
{
Concurrent.Release(resourceName);
}
// Expose the base critical section if needed
public static new object StaticCriticalSection => Concurrent.StaticCriticalSection;
}
///
/// Allows exclusive access to a wrapped variable. Reading access is always allowed. Concurrent
/// write requests are protected by instance-based "critical sections." Once a thread sets a
/// non-default value, it effectively 'holds' it until it sets it back to default.
///
/// Type of wrapped variable
///
[DataContract]
public class Exclusive : Concurrent
{
private T value;
public void Set(T newValue)
{
EnterCriticalSection();
if (IsNull(value) && !IsNull(newValue)) {
// Acquiring
value = newValue;
} else if (!IsNull(value) && !IsNull(newValue)) {
// Already held, update in place
value = newValue;
LeaveCriticalSection();
} else if (!IsNull(value) && IsNull(newValue)) {
// Releasing
value = default;
LeaveCriticalSection();
// This class uses nested calls, so we call LeaveCriticalSection() once more
LeaveCriticalSection();
} else {
// Edge case: was null, setting null => no change
LeaveCriticalSection();
}
}
private static readonly EqualityComparer EqualityComparer = EqualityComparer.Default;
private static bool IsNull(T val) => EqualityComparer.Equals(val, default);
// Sets value to default => releases one level of lock
// plus the additional "extra" lock if it was currently held.
public void Release()
{
Set(default);
}
public static implicit operator T(Exclusive instance)
{
return instance.value;
}
}
}