C# 线程静态 + 易失性成员未按预期工作

本文关键字:工作 成员 线程 静态 易失性 | 更新日期: 2023-09-27 18:36:57

我正在阅读提示和技巧帖子,我想我会尝试一些我以前从未做过的 C# 东西。因此,下面的代码没有实际用途,而只是一个"测试函数",看看会发生什么。

无论如何,我有两个静态私有字段:

private static volatile string staticVolatileTestString = "";
[ThreadStatic]
private static int threadInt = 0;

如您所见,我正在测试ThreadStaticAttributevolatile 关键字。

无论如何,我有一个如下所示的测试方法:

private static string TestThreadStatic() {
    // Firstly I'm creating 10 threads (DEFAULT_TEST_SIZE is 10) and starting them all with an anonymous method
    List<Thread> startedThreads = new List<Thread>();
    for (int i = 0; i < DEFAULT_TEST_SIZE; ++i) {
        Thread t = new Thread(delegate(object o) {
            // The anon method sets a newValue for threadInt and prints the new value to the volatile test string, then waits between 1 and 10 seconds, then prints the value for threadInt to the volatile test string again to confirm that no other thread has changed it
            int newVal = randomNumberGenerator.Next(10, 100);
            staticVolatileTestString += Environment.NewLine + "'tthread " + ((int) o) + " setting threadInt to " + newVal;
            threadInt = newVal;
            Thread.Sleep(randomNumberGenerator.Next(1000, 10000));
            staticVolatileTestString += Environment.NewLine + "'tthread " + ((int) o) + " finished: " + threadInt;
        });
        t.Start(i);
        startedThreads.Add(t);
    }
    foreach (Thread th in startedThreads) th.Join();
    return staticVolatileTestString;
}

我希望看到从这个函数返回的是这样的输出:

thread 0 setting threadInt to 88
thread 1 setting threadInt to 97
thread 2 setting threadInt to 11
thread 3 setting threadInt to 84
thread 4 setting threadInt to 67
thread 5 setting threadInt to 46
thread 6 setting threadInt to 94
thread 7 setting threadInt to 60
thread 8 setting threadInt to 11
thread 9 setting threadInt to 81
thread 5 finished: 46
thread 2 finished: 11
thread 4 finished: 67
thread 3 finished: 84
thread 9 finished: 81
thread 6 finished: 94
thread 7 finished: 60
thread 1 finished: 97
thread 8 finished: 11
thread 0 finished: 88

但是,我得到的是:

thread 0 setting threadInt to 88
thread 4 setting threadInt to 67
thread 6 setting threadInt to 94
thread 7 setting threadInt to 60
thread 8 setting threadInt to 11
thread 9 setting threadInt to 81
thread 5 finished: 46
thread 2 finished: 11
thread 4 finished: 67
thread 3 finished: 84
thread 9 finished: 81
thread 6 finished: 94
thread 7 finished: 60
thread 1 finished: 97
thread 8 finished: 11
thread 0 finished: 88

输出的后"一半"符合预期(我想这意味着 ThreadStatic 字段的工作方式与我想象的一样),但似乎一些初始输出已从前"一半"中"跳过"。

此外,前"一半"中的线程是无序的,但我知道线程不会在调用 Start() 后立即运行;相反,内部操作系统控件将按照它认为合适的方式启动线程。编辑:不,他们不是,实际上,我只是认为他们是因为我的大脑错过了连续的数字


所以,我的问题是:是什么问题导致我在输出的前"一半"中丢失了几行?例如,"线程 3 将 threadInt 设置为 84"行在哪里?

C# 线程静态 + 易失性成员未按预期工作

线程同时执行。概念上发生的事情是这样的:

staticVolatileTestString += Environment.NewLine + "'tthread " + ((int) o) + " setting threadInt to " + newVal;
  1. 线程 1 读取静态易失性测试字符串
  2. 线程 2 读取静态易失性测试字符串
  3. 线程 3 读取静态易失性测试字符串
  4. 线程 1 附加内容并写回静态易失性测试字符串
  5. 线程 2 附加内容并写回静态 VolatileTestString
  6. 线程 3 附加内容并写回静态 VolatileTestString

这会导致您的线路丢失。挥发性在这里无济于事;连接字符串的整个过程不是原子的。您需要在这些操作周围使用锁:

private static object sync = new object();
lock (sync) {
    staticVolatileTestString += Environment.NewLine + "'tthread " + ((int) o) + " setting threadInt to " + newVal;
}

MSDN 在此处描述了关键字 volatile 的作用:

volatile 关键字指示字段可能由 多个并发执行的线程。声明的字段 易失性不受假定访问权限的编译器优化的约束 通过单个线程。这可确保最新的值是 始终出现在现场。

这意味着,在您的示例中,发生的情况与此有关(这可能会不时变化;取决于调度程序):

  • 线程 0 正在从staticVolatileTestString中读取字符串
  • 线程 0
  • 正在附加"线程 0 将线程输入设置为 88"
  • 线程 0 正在将字符串写回 staticVolatileTestString

到目前为止,这就像预期的一样,但是:

  • 线程 1-4 正在从staticVolatileTestString中读取字符串
  • 线程 1
  • 正在附加"线程 1 将线程 Int 附加到 97"
  • 线程 2
  • 正在附加"线程 2 将线程 Int 设置为 11"
  • 线程 2 正在将字符串写回staticVolatileTestString
  • 。线程 1、2、3 正在读取和追加以及写入
  • 线程 4 正在将字符串写回staticVolatileTestString
  • 等等...

看到这里发生了什么? 线程 4 读取字符串"线程 0 将 threadInt 设置为 88",附加其"线程 4..."并将其写回,覆盖线程 1、2 和 3 已经写入字符串的所有内容。