如何使整个 POD 对象读取和更新操作无锁?
How to make whole POD object read and update operations lockfree?
假设我们有一个 POD 类型:
private class Messages {
public byte[] last;
public byte[] next;
}
及其实例messages
.
当用户(调用者)请求实例时,我们想给他消息对象的深层副本(可以不是最新的)。当用户设置自己的版本时,我们希望将其尽可能地提供给其他人,但又不中断读取请求(应删除旧版本,尽快不中断读取)。
如何使用 System.Collections.Concurrent 进行此类对象版本控制?
我尝试了什么:
internal class CuncurrentMessagesHelper {
private readonly ConcurrentStack<Messages> _stack = new ConcurrentStack<Messages>();
public CuncurrentMessagesHelper() {
}
public void SetLatest(Messages m) {
var length = _stack.Count;
_stack.Push(m);
var range = new Messages[length];
_stack.TryPopRange(range, 0, length);
}
public bool ReadLatest(out Messages result) {
return _stack.TryPeek(out result);
}
}
然而,这种帮助方法似乎很丑陋。
- 因为即使我们知道结果是有保证的,我们也使用 try 和 return bool 而不是 object;
- 它的 TryPopRange 使我们创建具有所有先前版本大小的附加数组。
这不是 POD。这是一个 POCO。我建议您仔细阅读 .NET 的值类型和引用类型之间的区别,因为它们的语义在编写安全的并发代码时至关重要。
由于 C# 引用保证是原子的,因此解决方案很简单(并且不需要任何特殊的并发容器)。
假设您的 Messages
对象在传入后是不可变的:
internal class ConcurrentMessagesHelper {
private volatile Messages _current;
public void SetLatest(Messages m) {
_current = m;
}
public Messages ReadLatest() {
return _current;
}
}
请注意,它是对正在此处复制的对象的引用(原子地),而不是对象的 byte[]
字段。 volatile
是必需的,因为引用由多个线程访问(它确保正确的行为,特别是关于内存排序和限制 JIT 只能执行线程安全的优化)。
如果传递给 SetLatest
的 Messages
对象可以更改,而它是最新的,那么您所要做的就是先制作一个副本。 SetLatest
变为:
public void SetLatest(Messages m) {
_current = DeepClone(m);
}
如果允许读者更改返回的 Messages
对象,那么您必须在让他们也拥有之前复制它。 ReadLatest
变为:
public Messages ReadLatest() {
return DeepClone(_current);
}
请注意,如果 Messages
的 byte[]
字段中包含的值在每条消息的生命周期内都是不可变的,那么您只需要一个浅拷贝,而不是深拷贝。
您可以将界面包装在一个简单的 属性:
中,从而使界面更加美观
internal class ConcurrentMessagesHelper {
private volatile Messages _current;
public Messages Current {
get { return DeepClone(_current); }
set { _current = DeepClone(value); }
}
private static Messages DeepClone(Messages m)
{
if (m == null)
return null;
return new Messages {
last = m.last == null ? null : (byte[])m.last.Clone(),
next = m.next == null ? null : (byte[])m.next.Clone()
};
}
}
如果您 实际上 确实有 POD 类型(例如 struct Messages
),那么我建议最简单的解决方案是将其包装在 class 中所以你可以有一个原子引用它的副本,这将允许你使用上面的解决方案。 StrongBox<T>
想到了。
这种情况下的代码变得更加简单,因为不需要显式复制:
private struct Messages {
public byte[] last;
public byte[] next;
}
internal class ConcurrentMessagesHelper {
private volatile StrongBox<Messages> _current;
public Messages Current {
get { return _current.Value; }
set { _current = new StrongBox<Messages>(value); }
}
}
如果 Messages
中的字节数组可以在对象的生命周期内发生变化,那么我们仍然需要深度克隆,但是:
internal class ConcurrentMessagesHelper {
private volatile StrongBox<Messages> _current;
public Messages Current {
get { return DeepClone(_current.Value); }
set { _current = new StrongBox<Messages>(DeepClone(value)); }
}
private static Messages DeepClone(Messages m)
{
return new Messages {
last = m.last == null ? null : (byte[])m.last.Clone(),
next = m.next == null ? null : (byte[])m.next.Clone()
};
}
}
假设我们有一个 POD 类型:
private class Messages {
public byte[] last;
public byte[] next;
}
及其实例messages
.
当用户(调用者)请求实例时,我们想给他消息对象的深层副本(可以不是最新的)。当用户设置自己的版本时,我们希望将其尽可能地提供给其他人,但又不中断读取请求(应删除旧版本,尽快不中断读取)。
如何使用 System.Collections.Concurrent 进行此类对象版本控制?
我尝试了什么:
internal class CuncurrentMessagesHelper {
private readonly ConcurrentStack<Messages> _stack = new ConcurrentStack<Messages>();
public CuncurrentMessagesHelper() {
}
public void SetLatest(Messages m) {
var length = _stack.Count;
_stack.Push(m);
var range = new Messages[length];
_stack.TryPopRange(range, 0, length);
}
public bool ReadLatest(out Messages result) {
return _stack.TryPeek(out result);
}
}
然而,这种帮助方法似乎很丑陋。
- 因为即使我们知道结果是有保证的,我们也使用 try 和 return bool 而不是 object;
- 它的 TryPopRange 使我们创建具有所有先前版本大小的附加数组。
这不是 POD。这是一个 POCO。我建议您仔细阅读 .NET 的值类型和引用类型之间的区别,因为它们的语义在编写安全的并发代码时至关重要。
由于 C# 引用保证是原子的,因此解决方案很简单(并且不需要任何特殊的并发容器)。
假设您的 Messages
对象在传入后是不可变的:
internal class ConcurrentMessagesHelper {
private volatile Messages _current;
public void SetLatest(Messages m) {
_current = m;
}
public Messages ReadLatest() {
return _current;
}
}
请注意,它是对正在此处复制的对象的引用(原子地),而不是对象的 byte[]
字段。 volatile
是必需的,因为引用由多个线程访问(它确保正确的行为,特别是关于内存排序和限制 JIT 只能执行线程安全的优化)。
如果传递给 SetLatest
的 Messages
对象可以更改,而它是最新的,那么您所要做的就是先制作一个副本。 SetLatest
变为:
public void SetLatest(Messages m) {
_current = DeepClone(m);
}
如果允许读者更改返回的 Messages
对象,那么您必须在让他们也拥有之前复制它。 ReadLatest
变为:
public Messages ReadLatest() {
return DeepClone(_current);
}
请注意,如果 Messages
的 byte[]
字段中包含的值在每条消息的生命周期内都是不可变的,那么您只需要一个浅拷贝,而不是深拷贝。
您可以将界面包装在一个简单的 属性:
中,从而使界面更加美观internal class ConcurrentMessagesHelper {
private volatile Messages _current;
public Messages Current {
get { return DeepClone(_current); }
set { _current = DeepClone(value); }
}
private static Messages DeepClone(Messages m)
{
if (m == null)
return null;
return new Messages {
last = m.last == null ? null : (byte[])m.last.Clone(),
next = m.next == null ? null : (byte[])m.next.Clone()
};
}
}
如果您 实际上 确实有 POD 类型(例如 struct Messages
),那么我建议最简单的解决方案是将其包装在 class 中所以你可以有一个原子引用它的副本,这将允许你使用上面的解决方案。 StrongBox<T>
想到了。
这种情况下的代码变得更加简单,因为不需要显式复制:
private struct Messages {
public byte[] last;
public byte[] next;
}
internal class ConcurrentMessagesHelper {
private volatile StrongBox<Messages> _current;
public Messages Current {
get { return _current.Value; }
set { _current = new StrongBox<Messages>(value); }
}
}
如果 Messages
中的字节数组可以在对象的生命周期内发生变化,那么我们仍然需要深度克隆,但是:
internal class ConcurrentMessagesHelper {
private volatile StrongBox<Messages> _current;
public Messages Current {
get { return DeepClone(_current.Value); }
set { _current = new StrongBox<Messages>(DeepClone(value)); }
}
private static Messages DeepClone(Messages m)
{
return new Messages {
last = m.last == null ? null : (byte[])m.last.Clone(),
next = m.next == null ? null : (byte[])m.next.Clone()
};
}
}