gpt4 book ai didi

C# 更新对象引用和多线程

转载 作者:太空宇宙 更新时间:2023-11-03 15:36:30 24 4
gpt4 key购买 nike

在阅读了这么多关于如何做到这一点之后,我很困惑。

所以这就是我想要做的:
我有一个包含各种信息的数据结构/对象。我将数据结构视为不可变的。每当我需要更新信息时,我都会进行 DeepCopy 并对其进行更改。然后我交换旧的和新创建的对象。

现在我不知道如何正确地做每一件事。

让我们从读者/消费者线程的角度来看它。

MyObj temp = dataSource;
var a = temp.a;
... // many instructions
var b = temp.b;
....

据我了解,阅读引用文献是原子的。所以我不需要任何 volatile 或锁定来将 dataSource 的当前引用分配给 temp。但是垃圾收集呢。据我了解,GC 有某种引用计数器来知道何时释放内存。那么当另一个线程恰好在将 dataSource 分配给 temp 的那一刻更新 dataSource 时,GC 是否会增加正确内存块上的计数器?
另一件事是编译器/CLR 优化。我将 dataSource 分配给 temp 并使用 temp 访问数据成员。 CLR 是做什么的?它真的会复制 dataSource 还是优化器只是使用 dataSource 来访问 .a 和 .b?让我们假设 temp.a 和 temp.b 之间有很多指令,因此对 temp/dataSource 的引用不能保存在 CPU 寄存器中。 temp.b 真的是 temp.b 还是它优化为 dataSource.b 因为复制到 temp 可以被优化掉。如果另一个线程更新 dataSource 以指向一个新对象,这一点尤其重要。

我真的需要 volatile、lock、ReadWriterLockSlim、Thread.MemoryBarrier 或其他东西吗?
对我来说重要的是,即使另一个线程将 dataSource 更新为另一个新创建的数据结构,我也想确保 temp.a 和 temp.b 访问旧数据结构。我从不更改现有结构中的数据。更新总是通过创建副本、更新数据然后更新对数据结构的新副本的引用来完成。

也许还有一个问题。如果我不使用 volatile,那么所有 CPU 上的所有内核都需要多长时间才能看到更新的引用?

关于 volatile ,请看这里: When should the volatile keyword be used in C#?

我做了一个小测试程序:
namespace test1 {
public partial class Form1 : Form {
public Form1() { InitializeComponent(); }

Something sharedObj = new Something();

private void button1_Click(object sender, EventArgs e) {
Thread t = new Thread(Do); // Kick off a new thread
t.Start(); // running WriteY()

for (int i = 0; i < 1000; i++) {
Something reference = sharedObj;

int x = reference.x; // sharedObj.x;
System.Threading.Thread.Sleep(1);
int y = reference.y; // sharedObj.y;

if (x != y) {
button1.Text = x.ToString() + "/" + y.ToString();
Update();
}
}
}

private void Do() {
for (int i = 0; i < 1000000; i++) {
Something someNewObject = sharedObj.Clone(); // clone from immutable
someNewObject.Do();
sharedObj = someNewObject; // atomic
}
}
}

class Something {
public Something Clone() { return (Something)MemberwiseClone(); }
public void Do() { x++; System.Threading.Thread.Sleep(0); y++; }
public int x = 0;
public int y = 0;
}
}

在 Button1_click 中有一个 for 循环,在 for 循环内,我使用直接“shareObj”访问数据结构/对象一次,使用临时创建的“引用”访问一次。使用引用足以确保“var a”和“var b”使用来自同一对象的值进行初始化。

我唯一不明白的是,为什么“Something reference = sharedObj;”没有优化掉和“int x = reference.x;”没有被“int x = sharedObj.x;”取代?

编译器,jitter怎么知道不优化这个?还是临时对象从未在 C# 中优化?

但最重要的是:我的示例是否按预期运行是因为它是正确的,还是只是偶然按预期运行?

最佳答案

As I understand reading references is atomic.



正确的。这是一个非常有限的属性。这意味着阅读引用资料会起作用;您永远不会将一半旧引用的位与一半新引用的位混合在一起,从而导致引用不起作用。如果有一个并发更改,它不会 promise 您获得旧引用还是新引用(这样的 promise 甚至意味着什么?)

So I don't need any volatile or locking to assign the current reference of dataSource to temp.



也许吧,尽管在某些情况下这可能会出现问题。

But what about the Garbage Collection. As I understand the GC has some kind of reference counter to know when to free memory.



不正确。 .NET 垃圾收集中没有引用计数。

如果存在对对象的静态引用,则它不符合回收条件。

如果有对对象的事件本地引用,则它不符合回收条件。

如果在不符合回收条件的对象的字段中存在对对象的引用,那么它也不符合递归回收的条件。

这里不计较。要么有一个有效的强引用禁止回收,要么没有。

这有很多非常重要的含义。与此相关的是,永远不会有任何不正确的引用计数,因为没有引用计数。强有力的引用不会消失在你的脚下。

The other thing is compiler/CLR optimization. I assign dataSource to temp and use temp to access data members. What does the CLR do? Does it really make a copy of dataSource or does the optimizer just use dataSource to access .a and .b?



这取决于 dataSourcetemp至于它们是否是本地的,以及它们是如何使用的。

如果 dataSourcetemp都是本地的,那么编译器或抖动很可能会优化分配。如果它们都是本地的,那么它们都是同一个线程的本地,所以这不会影响多线程的使用。

如果 dataSource是一个字段(静态或实例),那么由于 temp在显示的代码中绝对是本地的(因为它在显示的代码片段中初始化)分配不能被优化掉。一方面,获取字段的本地副本本身就是一种可能的优化,在本地引用上执行多个操作比持续访问字段或静态更快。编译器或抖动“优化”只会让事情变慢并没有多大意义。

考虑一下如果你不使用 temp 实际会发生什么:
var a = dataSource.a;
... // many instructions
var b = dataSource.b;

访问 dataSource.a代码必须首先获得对 dataSource 的引用然后访问 a .之后它获得了对 dataSource 的引用。然后访问 b .

通过不使用本地进行优化是没有意义的,因为无论如何都会有一个隐式的本地。

一个简单的事实是,您的恐惧是经过考虑的:在 temp = dataSource 之后没有假设 temp == dataSource因为可能有其他线程在更改 dataSource ,因此基于 temp == dataSource 进行优化是无效的.*

实际上,您关心的优化要么不相关,要么无效,因此不会发生。

不过,有一个案例可能会给您带来问题。在一个内核上运行的线程几乎有可能看不到对 dataSource 的更改。由在另一个核心上更改的线程制成。因此,如果您有:
/* Thread A */
dataSource = null;

/* Some time has passed */

/* Thread B */
var isNull = dataSource == null;

那么不能保证仅仅因为线程A已经完成设置 dataSource为空,线程 B 会看到这个。曾经。

.NET 本身和 .NET 通常在(x86 和 x86-64)上运行的处理器中使用的内存模型可以防止这种情况发生,但就 future 可能的优化而言,这是可能的。你需要内存屏障来确保线程 A 的发布肯定会影响线程 B 的阅读。 lockvolatile都是确保这一点的两种方式。

*一个甚至不需要是多线程的就不会遵循这一点,尽管可以在特定情况下证明没有单线程更改会破坏该假设。但这并不重要,因为多线程情况仍然适用。

关于C# 更新对象引用和多线程,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/31719158/

24 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com