在单独的线程中同步更新属性
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
类型是不可变的。它应该同样适用于 class
或 struct
MyConfig
.
我有一个 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
类型是不可变的。它应该同样适用于 class
或 struct
MyConfig
.