在单独的线程中同步更新属性

Synchronizing updating of properties in a separate thread

我有一个 class 继承自 BackgroundService。它在应用程序的生命周期内运行一个任务,在那个任务中我有一个循环来做一些计算,其中一些使用可设置的属性,可以从另一个线程设置。对于循环的一次迭代,我必须确保 none 这些属性发生变化,因此我的所有计算都使用相同的值。

我最初处理这个问题的方法是让我的 require 属性实际上只是在队列中插入一个动作,然后这些动作在我的循环开始时执行。所以,我最终得到这样的结果:

private MyConfig Config { get; set; }
private readonly ConcurrentQueue<Action> queue = new ConcurrentQueue<Action>();

public void UpdateConfig(MyConfig config)
{
    queue.Enqueue(() => Config = config);
}

protected async override Task ExecuteAsync(CancellationToken stoppingToken)
{
    await Task.Run(async () =>
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            for (int i = queue.Count; i > 0; i--)
            {
                queue.TryDequeue(out Action element);
                element?.Invoke();
            }
        }
        //now do some stuff with the updated config and maybe other properties too
    });             
}

我觉得可能有更好的方法。我应该只锁定那些属性吗?在这一点上,我不确定最好的解决方案。

这个问题有很多解决方法。您的解决方案有效,但可能 运行 出现问题。例如:

protected async Task AnotherThread ()
{
    var config = new MyConfig ();
    config.Property1 = 50;
    config.Property2 = 75;
    UpdateConfig ( config );
   
    // This statement breaks your threading approach.
    config.Property1 = 47;
 }

防止复制。看看下面的配置 class。这 class 将允许我们以线程安全的方式创建副本。

public class MyConfig
{
    private readonly object _sync = new object();

    public Int32 _property1;
    public Int32 Property1
    {
        get
        {
            lock ( _sync )
            {
                return _property1;
            }
        }
        set
        {
            lock ( _sync )
            {
                _property1 = value;
            }
        }
    }

    public Int32 _property2;
    public Int32 Property2
    {
        get
        {
            lock ( _sync )
            {
                return _property2;
            }
        }
        set
        {
            lock ( _sync )
            {
                _property2 = value;
            }
        }
    }

    // this method will create a copy of the current configuration. 
    public MyConfig Copy ()
    {
        MyConfig copy = new MyConfig();

        // locking here will prevent changes to the properties of this instance
        lock ( _sync )
        {
            // copy all properties while changes from other threads are prevented.
            copy.Property1 = Property1;
            copy.Property2 = Property2;
        }
        // changes are allowed to properties of this instance again.
        return copy;
    }
}

然后,

protected async override Task ExecuteAsync ( CancellationToken stoppingToken )
{
    await Task.Run ( async () =>
    {
        while ( !stoppingToken.IsCancellationRequested )
        {       
            MyConfig copy = Config.Copy ();

            // nothing outside of this thread has a reference to this copy of the configuration...
            // so unless you do something silly in your Calculations it will be safe to use.
            // In addition, since the properties of MyConfig are thread-safe, 
            // Config can be edited from other threads.
            Calculations ( copy );
        }
            
    } );
}

最后,由于我们对配置的更改 class 我们的更新方法真的不需要担心线程安全。

public void UpdateConfig ( MyConfig config )
{
    // update reference to new object
    Config = config;
}

编辑:回应评论。

另一种选择是不可变配置 class。我不会详细介绍实现细节,但这使您可以自由共享引用,而不必担心对象发生变化。

最后,有许多不同的库可用于防止第一个解决方案中的代码膨胀和错误。可能值得检查一下 AutoMapper。

我倾向于将您的代码更改为:

private MyConfig Config { get; set; }
private readonly ConcurrentQueue<Action<MyConfig>> queue = new ConcurrentQueue<Action<MyConfig>>();

public void UpdateConfig(Action<MyConfig> configAction)
{
    queue.Enqueue(configAction);
}
protected async Task ExecuteAsync(CancellationToken stoppingToken)
{
    await Task.Run(() =>
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            for (int i = queue.Count; i > 0; i--)
            {
                queue.TryDequeue(out Action<MyConfig> element);
                element?.Invoke(this.Config);
            }
        }
    });
}

这确保更新配置的唯一方法是通过此方法。您只在 class.

中处理 private MyConfig Config { get; set; } 变量

我也会担心对 ExecuteAsync 的并发调用,所以我建议确保您一次只有 运行 很多出队。

以下是我认为的标准模式:使用 lock 对象来同步对共享字段的访问。

private readonly object _locker = new object();
private MyConfig _config;

public void UpdateConfig(MyConfig config)
{
    lock (_locker) _config = config;
}

protected async override Task ExecuteAsync(CancellationToken stoppingToken)
{
    await Task.Run(async () =>
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            MyConfig localConfig; lock (_locker) localConfig = _config;
            // Do stuff with the localConfig. Don't use the shared _config field.
        }
    });             
}

这应该是 thread-safe,前提是 MyConfig 类型是不可变的。它应该同样适用于 classstruct MyConfig.