Ваш код, безусловно, является первым шагом в мир многопоточности, и вы только что испытали первую (из многих) головную боль!
Для начала, static
может позволить вам передавать переменную между потоками, но это не делает поточно-безопасным способом. Это означает, что ваше count < max
выражение и count++
не гарантированно будут в актуальном состоянии или эффективной защитой между потоками. Посмотрите на вывод вашей программы, когда max
равен только 10 (t установлен на 4, на моей 8-процессорной рабочей станции):
T0 - 0
T0 - 1
T0 - 2
T0 - 3
T1 - 0 // wait T1 got count = 0 too!
T2 - 1 // and T2 got count = 1 too!
T2 - 6
T2 - 7
T2 - 8
T2 - 9
T0 - 4
T3 - 1 // and T3 got count = 1 too!
T1 - 5
На ваш вопрос о печати каждой нити одна за другой я предполагаю, что вы пытаетесь координировать доступ к count
. Вы можете сделать это с помощью примитивов синхронизации (таких как оператор lock
в C #). Вот наивное изменение вашего кода, которое обеспечит выполнение только max
приращений:
static object countLock = new object();
public static void printWithLock()
{
// loop forever
while(true)
{
// protect access to count using a static object
// now only 1 thread can use 'count' at a time
lock (countLock)
{
if (count >= max) return;
Console.WriteLine(Thread.CurrentThread.Name + " - " + count.ToString());
count++;
}
}
}
Эта простая модификация делает вашу программу логически правильной, но также медленной . Образец теперь демонстрирует новую проблему: блокировка конкуренции . Каждый поток теперь претендует на доступ к countLock
. Мы сделали поток нашей программы безопасным, но без каких-либо преимуществ параллелизма!
Потоки и параллелизм не так-то просто получить правильно, но, к счастью, последние версии .Net поставляются с Task Parallel Library (TPL) и Parallel LINQ (PLINQ) .
Прелесть библиотеки в том, насколько легко было бы преобразовать ваш текущий код:
var sw = new Stopwatch();
sw.Start();
Enumerable.Range(0, max)
.AsParallel()
.ForAll(number =>
Console.WriteLine("T{0}: {1}",
Thread.CurrentThread.ManagedThreadId,
number));
Console.WriteLine("{0} ms elapsed", sw.ElapsedMilliseconds);
// Sample output from max = 10
//
// T9: 3
// T9: 4
// T9: 5
// T9: 6
// T9: 7
// T9: 8
// T9: 9
// T8: 1
// T7: 2
// T1: 0
// 30 ms elapsed
Вывод выше является интересной иллюстрацией того, почему многопоточность дает «неожиданные результаты» для новых пользователей. Когда потоки выполняются параллельно, они могут завершать фрагменты кода в разные моменты времени или один поток может быть быстрее другого. Вы никогда не знаете с многопоточностью!