Всем привет, скажите пожалуйста почему у меня не правильно вычисляется число pi? То-есть есть примитивная формула и я её короче захотел просчитать, так вот, всё вроде-бы в один дополнительный поток работает хорошо, но я захотел чуть ускорить весь процесс (поэкспериментировать) и решил сделать просчёт суммы 6/n^2 в четырёх разных потоках, но к сожалению у меня вылетает не "3.141.... и т.д.", а какие нибудь совсем левые значения "4.76...", "2.34...", "1.13...". В чём может быть проблема ?
decimal sum = 0;
decimal n = 0;
void func()
{
while (true)
{
n++;
sum = sum + (6 / Convert.ToDecimal(n * n));
//Console.WriteLine("Поток " + Thread.CurrentThread.Name + " выводит " + n);
}
}
Thread myThread1;
Thread myThread2;
Thread myThread3;
Thread myThread4;
private void button_Click(object sender, RoutedEventArgs e)
{
myThread1 = new Thread(func); //Создаем новый объект потока (Thread)
myThread1.Name = "Thread1";
myThread2 = new Thread(func); //Создаем новый объект потока (Thread)
myThread2.Name = "Thread2";
myThread3 = new Thread(func); //Создаем новый объект потока (Thread)
myThread3.Name = "Thread3";
myThread4 = new Thread(func); //Создаем новый объект потока (Thread)
myThread4.Name = "Thread4";
myThread1.Start(); //запускаем поток
myThread2.Start(); //запускаем поток
myThread3.Start(); //запускаем поток
myThread4.Start(); //запускаем поток
}
private void button1_Click(object sender, RoutedEventArgs e)
{
label1.Content = (Math.Sqrt(Convert.ToDouble(sum))).ToString();
}
Ответ
Проблема в разделяемых данных. Дело с том, что ваши операции неатомарны, а выполнение потоков может пересекаться как угодно.
Представьте себе, что один поток стартовал и увеличил n, теперь n == 1. В этот момент другой поток стартовал, снова увеличил n (теперь n == 2), и прибавил к сумме (6 / Convert.ToDecimal(n * n));. Теперь первый поток продолжает выполнение, и прибавляет к сумме (6 / Convert.ToDecimal(n * n));. Но поскольку n уже успело увеличиться, будет прибавлено вовсе не то, что нужно!
Другая проблема, как подсказывает @Igor в комментарии — это то, что в коде sum = sum + (6 / Convert.ToDecimal(n * n)); значение sum может поменяться в другом потоке после вычисления правой части. Смотрите, пусть один поток вычислил значение sum + (6 / Convert.ToDecimal(n * n)) и хочет записать его в sum. В этот самый момент другой поток прибавляет к sum очередное слагаемое. Теперь возвращается к выполнению наш поток, и записывает в sum новое значение, теряя уже прибавленный другим потоком кусок!
Третья проблема — это неатомарность присваивания decimal. Дело в том, что decimal складывается не за один такт процессора, а значит, на середине сложения может вмешаться снова второй поток и всё испортить. Например, первое присвоение скопировало первое слово результата в переменную, тут прибежал второй поток и переписал новым значением все слова результата. Теперь первый поток продолжает запись, и записывает второе и дальнейшие слова — но первое слово получается вовсе от другого значения! Видите, сколько получается возможных проблем?
Есть несколько методов «бороться» с данными проблемами. Один из них — эксклюзивная блокировка данных на время работы с ними. Но ваш код состоит лишь из работы с разделяемыми данными, поэтому такая блокировка просто заставит все потоки ждать, пока один из них обрабатывает одну итерацию, тем самым задание будет выполнено ещё медленее, чем если бы его делал один поток!
Таким образом, имеет смысл изменить алгоритм, чтобы каждый поток работал лишь с локальными данными. Например, пусть один поток считает лишь каждое четвёртое слагаемое.
Получится примерно такой код:
decimal compute(int start, int step, int limit)
{
decimal sum = 0;
for (long n = start; n < limit; n += step)
sum += 6 / (decimal)(n * n);
return sum;
}
Task t0 = Task.Run(() => compute(0, 4, 100000));
Task t1 = Task.Run(() => compute(1, 4, 100000));
Task t2 = Task.Run(() => compute(2, 4, 100000));
Task t3 = Task.Run(() => compute(3, 4, 100000));
decimal result = await t0 + await t1 + await t2 + await t3;
double pi = Math.Sqrt((double)result);
Для того, чтобы разобраться с тем, как работать с многопоточным кодом, стоит прочитать online-главу из книги братьев Албахари, или её русский перевод: глава 1, глава 2, глава 3, глава 4, глава 5.1, глава 5.2