我正在设计一个类,我希望在主线程完成配置之后只读它,即“冻结”它。 Eric Lippert将此popsicle称为不变性。冻结后,可以同时访问多个线程进行读取。
我的问题是如何以线程安全的方式编写这个真实高效的方式,即不要试图不必要地聪明。
尝试1:
public class Foobar
{
private Boolean _isFrozen;
public void Freeze() { _isFrozen = true; }
// Only intended to be called by main thread, so checks if class is frozen. If it is the operation is invalid.
public void WriteValue(Object val)
{
if (_isFrozen)
throw new InvalidOperationException();
// write ...
}
public Object ReadSomething()
{
return it;
}
}
Eric Lippert似乎建议在this帖子中这可以。 我知道写入具有发布语义,但据我所知,这仅适用于排序,并不一定意味着所有线程都会在写入后立即看到该值。谁能证实这一点?这意味着这个解决方案不是线程安全的(当然这可能不是唯一的原因)。
尝试2:
上述内容,但使用Interlocked.Exchange
确保实际发布了该值:
public class Foobar
{
private Int32 _isFrozen;
public void Freeze() { Interlocked.Exchange(ref _isFrozen, 1); }
public void WriteValue(Object val)
{
if (_isFrozen == 1)
throw new InvalidOperationException();
// write ...
}
}
这里的优点是我们确保发布价值而不会在每次阅读时承受开销。如果在写入_isFrozen之前没有移动任何读取,因为Interlocked方法使用完整的内存屏障,我猜这是线程安全的。但是,谁知道编译器会做什么(并且根据C#规范的3.10节看起来非常多),所以我不知道这是否是线程安全的。
尝试3:
也可以使用Interlocked
进行阅读。
public class Foobar
{
private Int32 _isFrozen;
public void Freeze() { Interlocked.Exchange(ref _isFrozen, 1); }
public void WriteValue(Object val)
{
if (Interlocked.CompareExchange(ref _isFrozen, 0, 0) == 1)
throw new InvalidOperationException();
// write ...
}
}
绝对是线程安全的,但是每次读取都必须进行比较交换似乎有点浪费。我知道这个开销可能很小,但我正在寻找一个合理高效的方法(尽管可能就是这样)。
尝试4:
使用volatile
:
public class Foobar
{
private volatile Boolean _isFrozen;
public void Freeze() { _isFrozen = true; }
public void WriteValue(Object val)
{
if (_isFrozen)
throw new InvalidOperationException();
// write ...
}
}
但Joe Duffy宣称“sayonara volatile”,所以我不认为这是一个解决方案。
尝试5:
锁定一切,似乎有点矫枉过正:
public class Foobar
{
private readonly Object _syncRoot = new Object();
private Boolean _isFrozen;
public void Freeze() { lock(_syncRoot) _isFrozen = true; }
public void WriteValue(Object val)
{
lock(_syncRoot) // as above we could include an attempt that reads *without* this lock
if (_isFrozen)
throw new InvalidOperationException();
// write ...
}
}
同样看起来绝对是线程安全的,但是比使用上面的Interlocked方法有更多的开销,所以我倾向于尝试3而不是这个。
然后我可以提出至少一些(我相信还有更多):
尝试6:使用Thread.VolatileWrite
和Thread.VolatileRead
,但这些可能有点偏重。
尝试7:使用Thread.MemoryBarrier
,似乎有点内部。
尝试8:创建不可变副本 - 不想这样做
汇总:
编辑:
也许我的问题不明确,但我特别关注上述尝试为何好或坏的原因。请注意,我在这里谈论的是一个单独的编写器在任何并发读取之前写入然后冻结的情况。我相信尝试1是可以的,但我想确切地知道为什么(因为我想知道是否可以以某种方式优化读取,例如)。 我不太关心这是否是好的设计实践,而是更关心它的实际线程方面。
非常感谢收到回答的问题,但我选择将此标记为自己的答案,因为我觉得所给出的答案不会完全回答我的问题,我不想给给访问该网站的任何人的印象是,标记的答案是正确的,因为它因为赏金到期而自动标记为这样。 此外,我不认为投票数最多的答案是以压倒多数投票的,不足以自动将其作为答案。
我仍然倾向于尝试#1正确,但是,我会喜欢一些权威的答案。我理解x86有一个强大的模型,但我不想(也不应该)为特定的体系结构编写代码,毕竟这是关于.NET的好东西之一。
如果您对答案有疑问,请选择其中一种锁定方法,也许使用此处显示的优化方法,以避免对锁定进行大量争用。
答案 0 :(得分:25)
也许稍微偏离主题但只是出于好奇:)你为什么不使用“真正的”不变性?例如使Freeze()返回一个不可变的副本(没有“写入方法”或任何其他更改内部状态的可能性)并使用此副本而不是原始对象。您甚至可以在不更改状态的情况下进行操作,并在每次写入操作中返回一个新副本(具有已更改的状态)(afaik字符串类可以正常工作)。 “真正的不变性”本质上是线程安全的。
答案 1 :(得分:8)
我投票支持尝试5,使用lock(this)实现。
这是使这项工作最可靠的方法。可以使用读写器锁,但收益很少。只需使用普通锁即可。
如有必要,您可以先检查_isFrozen
然后再锁定:
void Freeze() { lock (this) _isFrozen = true; }
object ReadValue()
{
if (_isFrozen)
return Read();
else
lock (this) return Read();
}
void WriteValue(object value)
{
lock (this)
{
if (_isFrozen) throw new InvalidOperationException();
Write(value);
}
}
答案 2 :(得分:5)
如果您真正创建,填充并冻结对象,然后再将其显示给其他线程,那么您不需要任何特殊的处理线程安全性(.NET的强大内存模型已经是您的保证),所以解决方案1有效。
但是,如果您将未冻结的对象提供给另一个线程(或者如果您在不知道用户将如何使用它的情况下简单地创建您的类)那么使用该版本,返回新的完全不可变实例的解决方案可能会更好。在这种情况下,Mutable实例类似于StringBuilder,而不可变实例就像字符串一样。如果您需要额外的保证,可变实例可以检查其创建者线程并在从任何其他线程使用时抛出异常(在所有方法中......以避免可能的部分读取)。
答案 3 :(得分:2)
尝试2在x86和其他具有强大内存模型的处理器上是线程安全的,但我怎么做是为了让线程安全成为消费者的问题,因为你没有办法在消费的代码中有效地做到这一点。考虑:
if(!foo.frozen)
{
foo.apropery = "avalue";
}
frozen
属性的线程安全性和apropery
的setter中的守卫代码并不重要,因为即使它们完全是线程安全的,你仍然有竞争条件。相反,我会写它像
lock(foo)
{
if(!foo.frozen)
{
foo.apropery = "avalue";
}
}
并且没有任何属性本身就是线程安全的。
答案 4 :(得分:2)
#1 - 读者不是线程安全 - 我认为问题在于读者方面,而不是作者(代码未显示)
#2 - 读者不是线程安全 - 与#1相同
#3 - 有希望的,读取检查可以在大多数情况下(当CPU缓存同步时)进行优化
尝试3:
也可以使用Interlocked进行读取。
public class Foobar {
private object _syncRoot = new object();
private int _isFrozen = 0; // perf compiler warning, but training code, so show defaults
// Why Exchange to 1 then throw away result. Best to just increment.
//public void Freeze() { Interlocked.Exchange(ref _isFrozen, 1); }
public void Freeze() { Interlocked.Increment(ref _isFrozen); }
public void WriteValue(Object val) {
// if this core can see _isFrozen then no special lock or sync needed
if (_isFrozen != 0)
throw new InvalidOperationException();
lock(_syncRoot) {
if (_isFrozen != 0)
throw new InvalidOperationException(); // the 'throw' is 100x-1000x more costly than the lock, just eat it
_val = val;
}
}
public object Read() {
// frozen is one-way, if one-way state has been published
// to my local CPU cache then just read _val.
// There are very strange corner cases when _isFrozen and _val fields are in
// different cache lines, but should be nearly impossible to hit unless
// dealing with very large structs (make it more likely to cross
// 4k cache line).
if (_isFrozen != 0)
return _val;
// else
lock(_syncRoot) { // _isFrozen is 0 here
if (_isFrozen != 0) // if _isFrozen is 1 here we just collided with writer using lock on other thread, or our CPU cache was out of sync and lock() forced the dirty cache line to be read from main memory
return _val;
throw new InvalidOperationException(); // throw is 100x-1000x more expensive than lock, eat the cost of lock
}
}
}
Joe Duffy关于'volatile is dead'的帖子,我认为是在他的下一代CLR / OS架构和ARM上的CLR的背景下。我们这些做多核x64 / x86的人认为volatile很好。如果perf是主要问题,我建议您测量上面的代码并将其与volatile进行比较。
与其他人发布答案不同,如果您有很多读者(3个或更多线程可能同时读取同一个对象),我不会直接跳到lock()。但是在你的样本中,你会在发生碰撞时将性能敏感问题与异常混合起来,这没有多大意义。如果您正在使用异常,那么您还可以使用其他更高级别的构造。
如果您想要完全安全但需要针对大量并发读者进行优化,请将lock()/ Monitor更改为ReaderWriterLockSlim。
.NET有新的原语来处理发布值。看看Rx吧。对于某些情况,它可以非常快速和无锁(我认为它们使用类似于上面的优化)。
如果多次写入但只保留一个值 - 在Rx中为“new ReplaySubject(bufferSize:1)”。如果你尝试它,你可能会感到惊讶它有多快。与此同时,我赞赏你尝试学习这一详细程度。
如果你想要无锁,可以克服你对Thread.MemoryBarrier()的厌恶。这非常重要。但它与Joe Duffy所描述的具有相同的易失性 - 它被设计为对编译器和放大器的暗示。 CPU,以防止重新排序内存读取(在CPU方面需要很长时间,因此当没有提示时,它们会被积极地重新排序)。当这种重新排序与CLR结构(如函数的自动内联)相结合时,您可以在内存和放大器中看到非常令人惊讶的行为。注册级别。 MemoryBarrier()只是禁用CPU和CLR大部分时间都使用的那些单线程内存访问假设。
答案 5 :(得分:2)
也许我的问题不清楚,但我特别关注为什么上述尝试的好坏。请注意,我在这里谈论的是一个单独的编写器在任何并发读取之前写入然后冻结的情况。我相信尝试1是可以的,但我想确切地知道为什么(因为我想知道是否可以以某种方式优化读取,例如)。我不太关心这是否是好的设计实践,而是更关心它的实际线程方面。
好的,现在我更好地了解你在做什么,并在回复中寻找。请允许我详细说明我之前的答案,通过首先解决每次尝试来促进锁的使用。
尝试1:
使用没有任何形式的同步原语的简单类的方法在您的示例中完全可行。自从'创作'' thread是唯一可以访问此类的线程,在它变异状态期间这应该是安全的。如果只有另一个线程有可能在课程被冻结之前访问?你需要提供同步吗?从本质上讲,线程不可能拥有从未见过的东西的缓存。
除了具有此列表内部状态的缓存副本的线程之外,还有一个您应该关注的并发问题。您应该考虑通过创作线程进行写重新排序。你的示例解决方案没有足够的代码让我来解决这个问题,但是处理这个问题的过程已经冻结了。列表到另一个线程是问题的核心。您使用Interlocked.Exchange还是写入易失性状态?
我仍然主张不是最好的方法,因为无法保证另一个线程在变异时没有看到该实例。
尝试2:
虽然不应使用尝试2。如果您对成员使用原子写入,则还应使用原子读取。我永远不会推荐一个没有另一个,因为没有两个读取和写入是原子的你没有获得任何东西。原子读写的正确应用是您的尝试3'
尝试3:
如果线程试图改变冻结列表,这将保证抛出异常。但是,它没有声明只有在冻结的实例上才能接受读取。恕我直言,这与使用原子和非原子访问器访问我们的_isFrozen
变量一样糟糕。如果您要说保护写入很重要,那么您应该始终保护读取。一个没有另一个只是奇怪的#39;。
忽略了我自己编写gaurds编写的代码的感觉,但是根据您的具体用途,这是一种可接受的方法。我有一个作家,我写,我冻结,然后我把它提供给读者。在这种情况下,您的代码正常工作。在将类交给另一个线程之前,您依赖_isFrozen
集上的原子操作来提供所需的内存屏障。
简而言之,这种方法有效,但如果一个线程有一个未冻结的实例,它将再次破解。
尝试4:
虽然这与尝试3(给定一位作家)几乎相同,但存在一个很大的不同。在此示例中,如果您在阅读器中选中_isFrozen
,则每次访问都需要内存屏障。一旦列表被冻结,这是不必要的开销。
仍然存在与尝试3相同的问题,因为在读取期间没有关于_isFrozen
状态的断言,因此在您的示例用法中性能应该相同。
尝试5:
正如我所说,这是我的偏好,因为我的其他答案中出现了修改阅读。
尝试6:
与#4基本相同。
尝试7:
您可以使用Thread.MemoryBarrier
解决您的特定需求。基本上使用来自尝试1的代码,您可以创建实例,调用Freeze()
,添加Thread.MemoryBarrier
,然后共享实例(或在锁中共享它)。这应该很有效,再次仅限于您的有限用例。
尝试8:
如果不了解更多相关信息,我无法就复制费用提出建议。
<强>摘要强>
我更喜欢使用具有某种线程保证的类或者根本不使用类。创建一个仅部分&#39;线程安全,IMO,危险。
用着名的绝地大师的话来说:
要么做不要尝试。
线程安全也是如此。该类应该是线程安全的或不是。采用这种方法,您可以使用我的尝试5的增强,或使用尝试7.如果选择,我绝不会推荐#7。
因此,我的建议坚定地支持完全线程安全的版本。两者之间的性能成本非常小,几乎不存在。读者线程将永远不会点击锁定只是因为你有一个单独的编写器的使用场景。然而,如果他们这样做,适当的行为仍然是确定的。因此,随着您的代码随着时间的推移而变化,并且在被冻结之前突然您的实例被共享,您不会因为竞争条件而崩溃,这会使您的程序崩溃。线程是否安全,或者不是,不会半途而废,或者有一天你会遇到令人讨厌的惊喜。
我的偏好是由多个线程共享的所有类都是以下两种类型之一:
由于冰棒列表不是设计不可变的,因此它不适合#1。因此,如果您要跨线程共享对象,它应该适合#2。
希望所有这些咆哮进一步解释了我的推理:)
<强> _syncRoot 强>
很多人都注意到我在锁定实现上跳过了_syncRoot
的使用。虽然使用_syncRoot的原因是有效的,但并不总是必要的。在您拥有单个编写器的示例用法中,lock(this)
的使用应该足够好,而无需为_syncRoot
添加另一个堆分配。
答案 6 :(得分:1)
是构造和写入的东西,然后永久冻结并多次读取?
或者你冻结和解冻并多次重新冷冻?
如果它是前者,那么也许“冻结”检查应该是读者方法而不是编写器方法(以防止它在被冻结之前阅读)。
或者,如果是后者,那么您需要注意的用例是:
在后一种情况下,Google会为multiple reader single writer显示很多您可能感兴趣的结果。
答案 7 :(得分:1)
一般来说,每个可变对象应该只有一个明确定义的“所有者”;共享对象应该是不可变的。多个线程在冻结之前不应该访问冰棒。
就个人而言,我不喜欢暴露的“冻结”方法的冰棒免疫形式。我认为更简洁的方法是使用AsMutable
和AsImmutable
方法(每种方法都会在适当时简单地返回未修改的对象)。这种方法可以允许对不可变性做出更强有力的承诺。例如,如果在调用其AsImmutable
成员时正在变异“非共享可变对象”(与“非共享”对象相反的行为),则副本中的数据状态可能是不确定的,但无论返回什么都是不可改变的。相比之下,如果一个线程冻结一个对象,然后假设它是不可变的,而另一个线程正在写入它,那么“不可变”对象可能会在它被冻结并且读取其值后最终发生变化。
修改
基于进一步的描述,我建议在监视器锁中使用写入对象的代码,并使冻结例程看起来像:
public Thingie Freeze(void) // Returns the object in question { if (isFrozen) // Private field return this; else return DoFreeze(); } Thingie DoFreeze(void) { if (Monitor.TryEnter(whatever)) { isFrozen = true; return this; } else if (isFrozen) return this; else throw new InvalidOperationException("Object in use by writer"); }
Freeze
方法可以被任意数量的线程调用任意次数;它应该足够短以便内联(虽然我没有对其进行分析),因此应该几乎没有时间执行。如果任何线程中对象的第一次访问是通过Freeze
方法进行的,那么应该保证在任何合理的内存模型下都能正常可见(即使线程没有看到由创建的线程执行的对象的更新并且最初冻结它,它将执行TryEnter
,这将保证内存屏障,并且在失败之后它将注意到该对象被冻结并将其返回。
如果要写入对象的代码首先获取锁,则尝试写入冻结对象可能会死锁。如果一个人宁愿让这样的代码抛出一个异常,那么一个人使用TryEnter
并在它无法获得锁定时抛出异常。
用于锁定的对象应该是由要冻结的对象专门保存的对象。如果要冻结的对象不包含对任何内容的纯私有引用,则可以锁定this
或创建私有对象纯粹用于锁定目的。请注意,放弃“输入”监视器锁定而不进行清理是安全的; GC只会忘记它们,因为如果锁定没有引用,那么任何人都不会关心(或者甚至可以问)锁是否在它被放弃时进入。
答案 8 :(得分:1)
我不确定以下方法的成本如何,但它有点不同。只有在最初如果有多个线程试图同时写入值时,它们才会遇到锁定。一旦冻结,所有后来的调用都将直接获得异常。
尝试9:
public class Foobar
{
private readonly Object _syncRoot = new Object();
private object _val;
private Boolean _isFrozen;
private Action<object> WriteValInternal;
public void Freeze() { _isFrozen = true; }
public Foobar()
{
WriteValInternal = BeforeFreeze;
}
private void BeforeFreeze(object val)
{
lock (_syncRoot)
{
if (_isFrozen == false)
{
//Write the values....
_val = val;
//...
//...
//...
//and then modify the write value function
WriteValInternal = AfterFreeze;
Freeze();
}
else
{
throw new InvalidOperationException();
}
}
}
private void AfterFreeze(object val)
{
throw new InvalidOperationException();
}
public void WriteValue(Object val)
{
WriteValInternal(val);
}
public Object ReadSomething()
{
return _val;
}
}
答案 9 :(得分:0)
你有没有检查过Lazy
http://msdn.microsoft.com/en-us/library/dd642331.aspx
使用ThreadLocal
http://msdn.microsoft.com/en-us/library/dd642243.aspx
实际上还有一个Freezable类......
http://msdn.microsoft.com/en-us/library/vstudio/ms602734(v=vs.100).aspx
答案 10 :(得分:0)
您可以使用POST Sharp
来实现此目的采取一个界面
public interface IPseudoImmutable
{
bool IsFrozen { get; }
bool Freeze();
}
然后从InstanceLevelAspect派生你的属性,就像这个
/// <summary>
/// implement by divyang
/// </summary>
[Serializable]
[IntroduceInterface(typeof(IPseudoImmutable),
AncestorOverrideAction = InterfaceOverrideAction.Ignore, OverrideAction = InterfaceOverrideAction.Fail)]
public class PseudoImmutableAttribute : InstanceLevelAspect, IPseudoImmutable
{
private volatile bool isFrozen;
#region "IPseudoImmutable"
[IntroduceMember]
public bool IsFrozen
{
get
{
return this.isFrozen;
}
}
[IntroduceMember(IsVirtual = true, OverrideAction = MemberOverrideAction.Fail)]
public bool Freeze()
{
if (!this.isFrozen)
{
this.isFrozen = true;
}
return this.IsFrozen;
}
#endregion
[OnLocationSetValueAdvice]
[MulticastPointcut(Targets = MulticastTargets.Property | MulticastTargets.Field)]
public void OnValueChange(LocationInterceptionArgs args)
{
if (!this.IsFrozen)
{
args.ProceedSetValue();
}
}
}
public class ImmutableException : Exception
{
/// <summary>
/// The location name.
/// </summary>
private readonly string locationName;
/// <summary>
/// Initializes a new instance of the <see cref="ImmutableException"/> class.
/// </summary>
/// <param name="message">
/// The message.
/// </param>
public ImmutableException(string message)
: base(message)
{
}
public ImmutableException(string message, string locationName)
: base(message)
{
this.locationName = locationName;
}
public string LocationName
{
get
{
return this.locationName;
}
}
}
然后在你的班级中申请
[PseudoImmutableAttribute]
public class TestClass
{
public string MyString { get; set; }
public int MyInitval { get; set; }
}
然后在多线程
中运行它 /// <summary>
/// The program.
/// </summary>
public class Program
{
/// <summary>
/// The main.
/// </summary>
/// <param name="args">
/// The args.
/// </param>
public static void Main(string[] args)
{
Console.Title = "Divyang Demo ";
var w = new Worker();
w.Run();
Console.ReadLine();
}
}
internal class Worker
{
private object SyncObject = new object();
public Worker()
{
var r = new Random();
this.ObjectOfMyTestClass = new MyTestClass { MyInitval = r.Next(500) };
}
public MyTestClass ObjectOfMyTestClass { get; set; }
public void Run()
{
Task readWork;
readWork = Task.Factory.StartNew(
action: () =>
{
for (;;)
{
Task.Delay(1000);
try
{
this.DoReadWork();
}
catch (Exception exception)
{
// Console.SetCursorPosition(80,80);
// Console.SetBufferSize(100,100);
Console.WriteLine("Read Exception : {0}", exception.Message);
}
}
// ReSharper disable FunctionNeverReturns
});
Task writeWork;
writeWork = Task.Factory.StartNew(
action: () =>
{
for (int i = 0; i < int.MaxValue; i++)
{
Task.Delay(1000);
try
{
this.DoWriteWork();
}
catch (Exception exception)
{
Console.SetCursorPosition(80, 80);
Console.SetBufferSize(100, 100);
Console.WriteLine("write Exception : {0}", exception.Message);
}
if (i == 5000)
{
((IPseudoImmutable)this.ObjectOfMyTestClass).Freeze();
}
}
});
Task.WaitAll();
}
/// <summary>
/// The do read work.
/// </summary>
public void DoReadWork()
{
// ThreadId where reading is done
var threadId = System.Threading.Thread.CurrentThread.ManagedThreadId;
// printing on screen
lock (this.SyncObject)
{
Console.SetCursorPosition(0, 0);
Console.SetBufferSize(290, 290);
Console.WriteLine("\n");
Console.WriteLine("Read Start");
Console.WriteLine("Read => Thread Id: {0} ", threadId);
Console.WriteLine("Read => this.objectOfMyTestClass.MyInitval: {0} ", this.ObjectOfMyTestClass.MyInitval);
Console.WriteLine("Read => this.objectOfMyTestClass.MyString: {0} ", this.ObjectOfMyTestClass.MyString);
Console.WriteLine("Read End");
Console.WriteLine("\n");
}
}
/// <summary>
/// The do write work.
/// </summary>
public void DoWriteWork()
{
// ThreadId where reading is done
var threadId = System.Threading.Thread.CurrentThread.ManagedThreadId;
// random number generator
var r = new Random();
var count = r.Next(15);
// new value for Int property
var tempInt = r.Next(5000);
this.ObjectOfMyTestClass.MyInitval = tempInt;
// new value for string Property
var tempString = "Randome" + r.Next(500).ToString(CultureInfo.InvariantCulture);
this.ObjectOfMyTestClass.MyString = tempString;
// printing on screen
lock (this.SyncObject)
{
Console.SetBufferSize(290, 290);
Console.SetCursorPosition(125, 25);
Console.WriteLine("\n");
Console.WriteLine("Write Start");
Console.WriteLine("Write => Thread Id: {0} ", threadId);
Console.WriteLine("Write => this.objectOfMyTestClass.MyInitval: {0} and New Value :{1} ", this.ObjectOfMyTestClass.MyInitval, tempInt);
Console.WriteLine("Write => this.objectOfMyTestClass.MyString: {0} and New Value :{1} ", this.ObjectOfMyTestClass.MyString, tempString);
Console.WriteLine("Write End");
Console.WriteLine("\n");
}
}
}
but still it will allow you to change property like array ,list . but if you apply more login in that then it may work for all type of property and field
答案 11 :(得分:0)
我会做这样的事情,受C ++移动类型的启发。请记住在冻结/解冻后不要访问该对象。
当然,如果您希望在解冻/冻结后进行访问时明确用户获取NRE的原因,您可以添加_data != null
支票/投票。
public class Data
{
public string _foo;
public int _bar;
}
public class Mutable
{
private Data _data = new Data();
public Mutable() {}
public string Foo { get => _data._foo; set => _data._foo = value; }
public int Bar { get => _data._bar; set => _data._bar = value; }
public Frozen Freeze()
{
var f = new Frozen(_data);
_data = null;
return f;
}
}
public class Frozen
{
private Data _data;
public Frozen(Data data) => _data = data;
public string Foo => _data._foo;
public int Bar => _data._bar;
public Mutable Thaw()
{
var m = new Mutable(_data);
_data = null;
return m;
}
}