Вопрос: Родитель не дожидается завершения рабочих потоков


В моей программе основной поток создает 4 (или более) рабочих потока. В какой-то момент родительский (основной поток) должен ждать рабочих, чтобы выполнить некоторые вычисления. Потоки работают на бесконечном цикле, поэтому я не могу использовать pthread_join(.., ..) POSIX, чтобы ждать, пока рабочие не будут выполнены. Поэтому я использую глобальный счетчик и переменную условия.

Общий раздел

unsigned jobs = 0; // global variable

// global mutex and cv. They get initialised in my main.
pthread_mutex_t counter_mutex;
pthread_cond_t  counter_cv;

static void process(..){

    jobs = myArray.size(); 

    // I am using a function here that broadcasts a cv in order to
    // wake up the workers

    pthread_mutex_lock(&counter_mutex); // lock counter
        while (jobs > 0){
            pthread_cond_wait(&counter_cv, &counter_mutex); // PARENT SHOULD GET STUCK HERE TILL WORKERS ARE DONE
        }
        // cout << "Workers are done" << endl;
    pthread_mutex_unlock(&counter_mutex);   // unlock counter
}

Рабочий код

 extern unsigned jobs;
 extern pthread_mutex_t counter_mutex;
 extern pthread_cond_t  counter_cv;

 void *run() {

    for (int i = 0;; i++) {

        // do some calculations here

        pthread_mutex_lock(&counter_mutex); // lock counter
            jobs--;
            if (jobs == 0){
                pthread_cond_signal(&counter_cv);
                cout << "All jobs are done" << endl;
            }
        pthread_mutex_unlock(&counter_mutex);   // unlock counter

    }
}    

Проблема в том, что иногда мой основной поток не застревает в переменной условия, чтобы ждать рабочих и иногда вызывает ошибку сегментации. Есть ли какое-либо состояние гонки, которое я не вижу, и что вызывает эту проблему?


4


источник


Ответы:


В вашем коде есть очевидная проблема параллелизма. Ради простоты представьте, что у нас есть 2 рабочих, и осталось 2 задания. Может произойти следующий сценарий:

  • Количество заданий равно 2
  • Родитель застрял в ожидании переменной условия
  • Оба рабочих запускают итерацию обработки в своем бесконечном цикле
  • Первый рабочий быстрее выполняет свою задачу по любой причине и выполняет обновление числа заблокированных заданий. Количество заданий уменьшается до 1, поэтому родитель остается в ожидании и блокировка освобождается. Затем рабочий начинает свою следующую итерацию. К сожалению, в действительности нет работы (1 было сделано, а 1 в настоящее время выполняется вторым работником). Но он начинает гипотетическую работу (возможно, обращаясь к элементу массива, который тем временем был удален или вышел из диапазона?)
  • Второй рабочий заканчивает свою работу, а количество заданий уменьшается до 0, заставляя родителя пробуждаться, удерживая замок, выходить из цикла и разблокировать.
  • Вы отметили, что, хотя родитель думает, что все закончилось, первый рабочий все еще обрабатывает гипотетическую задачу, возможно, пытается получить доступ myArray тогда как он очищается, или что-то еще может пойти не так.

Таким образом, у вас есть две возможности для гонок и / или segfalts: у родителя и у все еще активного работника, занятого задачей, которая не существует.

Я думаю, что рабочий цикл будет намного более безопасным, если вы запустите цикл, получая блокировку, проверяя, остались ли еще задания, и уменьшите счетчик заданий, чтобы работник-сверстник знал, что действительно осталось:

void *run() {

    for (int i = 0;; i++) {
        pthread_mutex_lock(&counter_mutex); // lock counter
            if (jobs == 0){
                pthread_cond_signal(&counter_cv);
                cout << "All jobs are done" << endl;
            }
            else jobs--;
        pthread_mutex_unlock(&counter_mutex);   // unlock counter

        // do some calculations here

    }
}   

Преимущество состоит в том, что рабочий работает только в том случае, если на самом деле осталась работа. Единственная проблема заключается в том, что родитель пробуждается первым работником без работы. Тем не менее, все еще могут работать другие рабочие.

Если это проблема, вы можете, например, сохранить также счетчик все еще активных заданий и сделать родительский цикл включенным (jobs>0 || active_jobs>0)


0



Единственная проблема, которую я вижу с вашими битами кода, заключается в том, что каждый рабочий будет работать как сумасшедший (возможно, на 100% CPU, если ваша обработка не связана с таймерами / IO), и не будет завершена, если больше нет заданий.

В реальном случае ваш рабочий должен получить запрос на задание из очереди или что-то подобное, и блокировать (ожидая доступного задания или команды завершения), когда нет доступных заданий для обработки.

Я не вижу здесь никаких условий гонки. Единственная ошибка, которую я вижу, это систематическое уменьшение количества заданий. Это скоро станет отрицательным, и это может вызвать всевозможные проблемы, если вы предполагаете, что это невозможно. Чтобы этого избежать, просто проверьте счетчик на ноль перед его уменьшением.


0



Родитель не дожидается завершения рабочих потоков | Programmerz.ru