Я пытался реализовать программы, которые вычисляют своего рода интеграл. И чтобы ускорить вычисления, один создает несколько процессов, а другой использует несколько потоков. В моей программе каждый процесс добавляет двойное значение в разделяемую память, а каждый поток добавляет двойное значение через указатель.
Вот мой вопрос. Очевидно, что операция добавления загружает значение из памяти, добавляет к нему значение и сохраняет результат в памяти. Таким образом, кажется, что мой код весьма подвержен проблеме производителя-потребителя, поскольку многие процессы/потоки обращаются к одной и той же области памяти. Однако я не смог найти случая, когда кто-то использовал семафоры или мьютексы для реализации простого аккумулятора.
// creating processes
while (whatever)
{
pid = fork();
if (pid == 0)
{
res = integralproc(clist, m, tmpcnt, tmpleft, tmpright);
*(createshm(shm_key)) += res;
exit(1);
}
}
// creating or retrieving shared memory
long double* createshm(int key)
{
int shm_id = -1;
void* shm_ptr = (void*)-1;
while (shm_id == -1)
{
shm_id = shmget((key_t)key, sizeof(long double), IPC_CREAT | 0777);
}
while (shm_ptr == (void*)-1)
{
shm_ptr = shmat(shm_id, (void*)0, 0);
}
return (long double*)shm_ptr;
}
// creating threads
while (whatever)
{
threadres = pthread_create(&(targs[i]->thread_handle), NULL, integral_thread, (void*)targs[i]);
}
// thread function. targ->resptr is pointer that we add the result to.
void *integral_thread(void *arg)
{
threadarg *targ = (threadarg*)arg;
long double res = integralproc(targ->clist, targ->m, targ->n, targ->left, targ->right);
*(targ->resptr) += res;
//printf("thread %ld calculated %Lf\n", targ->i, res);
pthread_exit(NULL);
}
Поэтому я реализовал это таким образом, и до сих пор, сколько бы процессов/потоков я не делал, результат был таким, как будто этого никогда не было. Я обеспокоен тем, что мои коды все еще могут быть потенциально опасными, просто вне поля моего зрения. Действительно ли этот код защищен от любой из этих проблем? Или я что-то упускаю из виду, и код должен быть исправлен?
Я рекомендую написать простую программу для целые числа. Несколько потоков увеличивают целое число (volatile
) фиксированное количество раз параллельно, без синхронизации. Сравните результат с фиксированным числом приращений, умноженным на количество потоков. Почитайте об атомарности и неопределенном поведении.
Почему бы не иметь отдельный счетчик для каждого потока и не объединить их в конце?
Кроме того, будьте очень осторожны, чтобы не использовать volatile в качестве атома бедняка, это просто неправильное использование volatile. C получил четко определенную стандартизованную модель многопоточной памяти, в основном она взяла стандартную модель параллелизма/атомарности C++ оптом.
Если все ваши потоки соревнуются в обновлении одного и того же объекта (т. е. targ->resptr
для каждого потока указывает на одно и то же), то да — у вас действительно есть состязание данных, и вы можете увидеть неправильные результаты (вероятно, «потерянные обновления», где два потоки, которые завершаются одновременно, пытаются обновить сумму, и только один из них эффективен).
Вы, вероятно, этого не видели, потому что время выполнения вашей функции integralproc()
велико, поэтому вероятность того, что несколько потоков одновременно достигнут точки обновления *targ->resptr
, невелика.
Тем не менее, вы все равно должны решить проблему. Вы можете либо добавить блокировку/разблокировку мьютекса вокруг обновления суммы:
pthread_mutex_lock(&result_lock);
*(targ->resptr) += res;
pthread_mutex_unlock(&result_lock);
(Это не должно влиять на эффективность решения, поскольку вы блокируете и разблокируете только один раз за время существования каждого потока).
В качестве альтернативы вы можете сделать так, чтобы каждый поток записывал свой собственный частичный результат в своей собственной структуре аргументов потока:
targ->result = res;
Затем, как только все рабочие потоки будут pthread_join()
отредактированы, создавший их родительский поток может просто просмотреть все структуры аргументов потока и сложить частичные результаты.
Здесь не требуется дополнительная блокировка, потому что рабочие потоки не имеют доступа к переменной результата друг друга, а pthread_join()
обеспечивает необходимую синхронизацию между рабочим, устанавливающим результат, и родительским потоком, читающим его.
Я думаю, что тогда необходимо и безопасно реализовать мьютекс или семафор. Спасибо за добрый ответ.
Технически он не определен, если только вы не используете какую-либо блокировку или, что предпочтительнее, не делаете счетчик атомарным и не используете с ним атомарные операции.