在 C# 中跨多个线程的方法调用中仅执行一次函数

问题描述 投票:0回答:1

我有一个来自在运行时加载的程序集的类,该类是使用跨多个线程(使用通道)的反射(因此使用无参数构造函数)实例化的。每个线程实例化该类的一个实例,然后使用一堆参数调用

Method()

其中一个参数包含插件可能需要的许可证密钥才能使用its插件。它应该只应用此密钥一次。

我当前的实现使用双重检查锁定:

private static bool _IsLicenceApplied;
private readonly static object _IsLicenceAppliedInitializationLock = new object();

public void Method(Keys keys, object data)
{
    if(!_IsLicenceApplied)
    {
        lock(_IsLicenceAppliedInitializationLock)
        {
            if(!_IsLicenceApplied)
            {
                Apply(keys);
                _IsLicenceApplied = true;
            }
        }
    }
    //do the rest of the method using data
}

我读过无数的答案、博客文章和文档,其中包含关于是否:

的相互冲突的信息和意见
  • _IsLicenceApplied
    应该是
    volatile
  • _IsLicenceApplied
    应使用
    Volatile
  • 上的方法进行读取和写入
  • 同样,但使用
    Interlocked
  • 我应该通过调用
    Thread.MemoryBarrier()
  • 来包围该块

其中很多都讨论了单例模式并用

Lazy<T>
替换此类代码,但我需要将密钥传递到方法中并让第一次执行处理它们。我认为
Lazy<T>
不可能做到这一点。也许还有另一个内置类可以使这成为可能?

编辑:在调用

_IsLicenceApplied = true;
后添加
Apply()

编辑2:阅读LazyInitializer的源代码后可能的解决方案(我可以使用它来代替?):

private static bool _IsLicenceApplied;
private static object _IsLicenceAppliedInitializationLock = new object();

public void Method(Keys keys, object data)
{
    if(!Volatile.Read(_IsLicenceApplied))
    {
        lock(_IsLicenceAppliedInitializationLock)
        {
            if(!Volatile.Read(_IsLicenceApplied))
            {
                Apply(keys);
                Volatile.Write(ref _IsLicenceApplied, true);
            }
        }
    }
    //do the rest of the method using data
}
c# .net multithreading volatile interlocked
1个回答
0
投票

您的第一个实现,没有使用

Volatile
,是不正确的。它允许进行编译器优化,从而导致线程在
Apply(keys)
调用完成之前继续运行。这种可能性是遥远的,主要是理论上的,但尽管如此,任何认为绝对正确性是不可协商的要求的审阅者都会认为这种实现不正确。

使用

Volatile
的第二个实现是正确的。尽管如此,它具有
LazyInitializer.EnsureInitialized
API 的行为,我不太喜欢它。如果
Apply(keys)
重复失败,就会出现可疑的行为,在这种情况下,所有等待线程将一一重试执行,从而可能导致累积的长时间延迟。我的偏好是将执行失败的错误传播到当前正在等待的所有线程。这样,任何线程等待的时间都不会超过单次调用的持续时间。下面是具有此行为的
SingleExecution
类:

/// <summary>
/// Represents a synchronous operation that is invoked on demand only once, unless
/// it fails, in which case it is retried as many times as needed until it succeeds.
/// </summary>
/// <remarks>
/// In case of failure the error is propagated to the invoking thread,
/// as well as to all other threads that are currently waiting for the execution.
/// </remarks>
public class SingleExecution
{
    private volatile Task _task;

    public void EnsureInvoked<TArg>(Action<TArg> action, TArg arg)
    {
        ArgumentNullException.ThrowIfNull(action);
        Task capturedTask = _task;
        if (capturedTask is null)
        {
            Task newTask = new(() =>
            {
                try
                {
                    action(arg);
                    _task = Task.CompletedTask;
                }
                catch
                {
                    _task = null;
                    throw;
                }
            });
            capturedTask = Interlocked
                .CompareExchange(ref _task, newTask, null) ?? newTask;
            if (ReferenceEquals(capturedTask, newTask))
                newTask.RunSynchronously(TaskScheduler.Default);
        }
        capturedTask.GetAwaiter().GetResult();
    }
}

此实现是我在

此处
发布的AsyncLazy类的修改版本。

关于处理异常的相同原则,尽管具有不同的实现,已在我发布的

无异常缓存">此处
© www.soinside.com 2019 - 2024. All rights reserved.