C# ThreadStatic + изменчивые члены не работают должным образом
Я читал пост с советами и рекомендациями и думал, что попробую кое-что из C#, чего раньше никогда не делал. Следовательно, следующий код не служит никакой реальной цели, а представляет собой "тестовую функцию", чтобы увидеть, что происходит.
В любом случае, у меня есть два статических приватных поля:
private static volatile string staticVolatileTestString = "";
[ThreadStatic]
private static int threadInt = 0;
Как видите, я тестирую ThreadStaticAttribute и ключевое слово volatile.
Во всяком случае, у меня есть метод испытаний, который выглядит следующим образом:
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(); но вместо этого внутренние элементы управления ОС будут запускать потоки по своему усмотрению. РЕДАКТИРОВАТЬ: Нет, это не так, на самом деле, я просто думал, что они были, потому что мой мозг пропускает последовательные числа
Итак, мой вопрос: что происходит, если я теряю несколько строк в первой "половине" результата? Например, где находится строка ' thread 3, устанавливающая threadInt в 84 '?
2 ответа
Потоки выполняются одновременно. Что концептуально происходит так:
staticVolatileTestString += Environment.NewLine + "\tthread " + ((int) o) + " setting threadInt to " + newVal;
- Поток 1 читает staticVolatileTestString
- Поток 2 читает staticVolatileTestString
- Поток 3 читает staticVolatileTestString
- Поток 1 добавляет материал и записывает staticVolatileTestString назад
- Поток 2 добавляет материал и записывает staticVolatileTestString назад
- Поток 3 добавляет материал и записывает staticVolatileTestString назад
Это приводит к потере ваших строк. Волатильность здесь не помогает; весь процесс объединения строк не является атомарным. Вам нужно использовать блокировку этих операций:
private static object sync = new object();
lock (sync) {
staticVolatileTestString += Environment.NewLine + "\tthread " + ((int) o) + " setting threadInt to " + newVal;
}
MSDN описывает, что такое ключевое слово volatile
делает здесь:
Ключевое слово volatile указывает, что поле может быть изменено несколькими одновременно выполняющимися потоками. Поля, которые объявлены как volatile, не подлежат оптимизации компилятора, которая предполагает доступ из одного потока. Это гарантирует, что в поле всегда присутствует самое последнее значение.
В вашем примере это означает, что происходит по этому поводу (это может меняться время от времени; зависит от планировщика):
- нить 0 читает строку из
staticVolatileTestString
- нить 0 добавляет 'нить 0, устанавливающая нить на 88'
- поток 0 записывает строку обратно в
staticVolatileTestString
это как ожидалось до сих пор, но потом:
- поток 1-4 читает строку из
staticVolatileTestString
- поток 1 добавляет 'поток 1, устанавливающий threadInt к 97'
- поток 2 добавляет 'поток 2, устанавливающий threadInt к 11'
- поток 2 записывает строку обратно в
staticVolatileTestString
- ... темы 1, 2, 3 читают и добавляют и пишут
- поток 4 записывает строку обратно в
staticVolatileTestString
- и так далее...
видите, что здесь произошло? Поток 4 прочитал строку "поток 0, установив threadInt равным 88", добавил его "поток 4..." и записал его обратно, переписав все потоки 1, 2 и 3, уже записанные в строку.