Проблема синхронизации с использованием pthread_kill () для завершения потока, заблокированного для ввода-вывода - PullRequest
0 голосов
/ 13 ноября 2018

Ранее я задавал вопрос о том, как завершить поток, заблокированный для ввода-вывода. Я использовал pthread_kill() вместо pthread_cancel() или для записи в каналы, учитывая несколько преимуществ.

У меня есть код для отправки сигнала (SIGUSR2) целевому потоку с помощью pthread_kill(). Ниже приведен скелетный код для этого. В большинстве случаев getTimeRemainedForNextEvent() возвращает значение, которое блокирует poll () на несколько часов. Из-за этого большого значения тайм-аута, даже если Thread2 устанавливает terminateFlag (чтобы остановить Thread1), Thread2 блокируется до тех пор, пока не будет возвращено poll () Thread1 (что может произойти через несколько часов, если в сокетах нет событий). Поэтому я посылаю сигнал в Thread1, используя pthread_kill(), чтобы прервать системный вызов poll () (если он заблокирован).

static void signalHandler(int signum) {
    //Does nothing
}

// Thread 1 (Does I/O operations and handles scheduler events). 

void* Thread1(void* args) {
    terminateFlag = 0;
    while(!terminateFlag) {
        int millis = getTimeRemainedForNextEvent(); //calculate maximum number of milliseconds poll() can block.

        int ret = poll(fds,numOfFDs,millis);
        if(ret > 0) {
            //handle socket events.
        } else if (ret < 0) {
            if(errno == EINTR)
                perror("Poll Error");
            break;
        }

        handleEvent();  
    }
}

// Thread 2 (Terminates Thread 1 when Thread 1 needs to be terminated)

void* Thread2(void* args) {
    while(1) {

    /* Do other stuff */

    if(terminateThread1) {
            terminateFlag = 1;
            pthread_kill(ftid,SIGUSR2); //ftid is pthread_t variable of Thread1
            pthread_join( ftid, NULL );
        }
    }

    /* Do other stuff */
} 

Приведенный выше код работает нормально, если Thread2 устанавливает terminateFlag и отправляет сигнал в Thread1, когда он заблокирован в системном вызове poll (). Но если переключение контекста происходит после того, как функция getTimeRemainedForNextEvent() Thread1 и Thread2 установит terminateFlag и отправит сигнал, poll () Thread1 блокируется на несколько часов, поскольку он потерял сигнал, прерывающий системный вызов.

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

Ответы [ 3 ]

0 голосов
/ 13 ноября 2018

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

void *Thread1(void *args) {
    pthread_mutex_lock(&a_mutex);
    terminateFlag = 0;
    while(!terminateFlag) {
        pthread_mutex_unlock(&a_mutex);

        // ...

        pthread_mutex_lock(&a_mutex);
    }
    pthread_mutex_unlock(&a_mutex);
}

void* Thread2(void* args) {
    // ...

    if (terminateThread1) {
        pthread_mutex_lock(&a_mutex);
        terminateFlag = 1;
        pthread_mutex_unlock(&a_mutex);
        pthread_kill(ftid,SIGUSR2); //ftid is pthread_t variable of Thread1
        pthread_join( ftid, NULL );
    }

    // ...
} 

Но это не решает основную проблему , что сигнал, отправленный потоком 2, может быть доставлен потоку 1 после того, как он протестирует terminateFlag, но до того, как он вызовет poll(), хотя он и сужает окно, в котором это могло произойти.

Самое чистое решение - это то, что было предложено уже в ответе @PaulSanders: иметь поток 2, чтобы активировать поток 1 через файловый дескриптор, который поток 1 опрашивает (то есть с помощью канала). Поскольку у вас, кажется, есть веская причина для поиска альтернативного подхода, однако также должна быть возможность заставить ваш подход к работе с сигналами работать путем надлежащего использования маскирования сигналов. Расширяя комментарий @ Shawn, вот как это будет работать:

  1. Родительский поток блокирует SIGUSR2 перед запуском потока 1, так что последний, который наследует свою маску сигналов от своего родителя, начинает с того, что этот сигнал заблокирован.

  2. В потоке 1 используется ppoll() вместо poll(), чтобы можно было указать, что SIGUSR2 будет разблокировано на время этого вызова. ppoll() выполняет маску сигналов атомарно, так что нет никакой возможности потерять сигнал, если он заблокирован перед вызовом и разблокирован в течение.

  3. Поток 2 использует pthread_kill() для отправки SIGUSR2 в поток 1, чтобы остановить его. Поскольку этот сигнал разблокируется только для этого потока, когда он выполняет вызов ppoll(), он не будет потерян (заблокированные сигналы остаются в ожидании до разблокирования). Это именно тот сценарий использования, для которого предназначен ppoll().

  4. Вы даже должны иметь возможность покончить с переменной terminateThread и связанной с ней синхронизацией, потому что вы должны иметь возможность полагаться на сигнал, доставляемый во время вызова ppoll() и, следовательно, вызывая код EINTR путь должен быть осуществлен. Этот путь не использует terminateThread для остановки потока.

0 голосов
/ 14 ноября 2018

Как вы говорите сами, вы можете использовать аннулирование потока, чтобы решить эту проблему.За исключением отмены потока, я не думаю, что есть «правильный» способ решить эту проблему в POSIX (пробуждение вызова poll с помощью write не совсем универсальный метод, который будет работать для всех ситуаций, в которыхпоток может быть заблокирован), потому что парадигма POSIX для создания системных вызовов и обработки сигналов просто не позволяет вам сократить разрыв между проверкой флага и потенциально длинным блокирующим вызовом.

void handler() { dont_enter_a_long_blocking_call_flg=1; }
int main()
{  //...
    if(dont_enter_a_long_blocking_call_flg)
        //THE GAP; what if the signal arrives here ?
        potentially_long_blocking_call();
    //....
}

muslБиблиотека libc использует сигналы для отмены потока (поскольку сигналы могут прервать вызовы с длительной блокировкой в ​​режиме ядра) и использует их вместе с глобальными метками сборки, чтобы из обработчика установки флага SIGCANCEL это можно было сделать (концептуально,Я не вставляю их фактический код):

void sigcancel_handler(int Sig, siginfo_t *Info, void *Uctx)
{
    thread_local_cancellation_flag=1;
    if_interrupted_the_gap_move_Program_Counter_to_start_cancellation(Uctx);
}

Теперь, если вы изменили if_interrupted_the_gap_move_Program_Counter_to_start_cancellation(Uctx); на if_interrupted_the_gap_move_Program_Counter_to_make_the_syscall_fail(Uctx); и экспортировали функцию if_interrupted_the_gap_move_Program_Counter_to_make_the_syscall_fail вместе с thread_local_cancellation_flag.

затем вы можете использовать его, чтобы *:

  • решить вашу проблему, надежно внедрив надежное подавление сигнала с любым сигналом без необходимостипоместить любой из этих pthread_cleanup_{push,pop} элементов в ваш уже работающий поточно-ориентированный однопоточный код
  • , чтобы обеспечить гарантированную реакцию в нормальном контексте на доставку сигнала в целевой поток, даже если сигнал обрабатывается.

По существу без такого расширения libc, если вы однажды kill()/pthread_kill() обработаете процесс / поток с сигналом, который он обрабатывает, или если поместите функцию в таймер отправки сигнала, вы не сможете быть уверены в гарантированной реакции надоставка сигнала, так как цель вполне может получить сигнал в промежутке, как указано выше, и зависать бесконечно, вместо того чтобы отвечать на него.

Я реализовал такое расширение libc поверх musl libc и опубликовал его сейчас https://github.com/pskocik/musl. В каталоге SIGNAL_EXAMPLES также приведены некоторые примеры kill(), pthread_kill и setitimer(), которые в условиях продемонстрированной гонки зависают с классическими libcs, но не имеют моего расширенного musl.Вы можете использовать этот расширенный мусл для четкого решения вашей проблемы, и я также использую его в своем личном проекте для надежного отмены потока без необходимости засорять мой код с помощью pthread_cleanup_{push,pop}

Очевидным недостатком этого подхода является то, что оннепереносимый, и у меня только это реализовано для мусульманин x86_64.Я опубликовал его сегодня в надежде, что кто-то (Cygwin, MacOSX?) Скопирует его, потому что я думаю, что это правильный способ сделать отмену в C.

В C ++ и с glibc, вы могли бы использовать тот факт,что glibc использует исключения для реализации отмены потока и просто использует pthread_cancel (который использует сигнал (SIGCANCEL) внизу), но перехватывает его вместо того, чтобы позволить ему убить поток.


Примечание:

Я действительно использую два локальных флага потока - флаг прерывателя, который разрывает следующий системный вызов с помощью ECANCELED, если он установлен до ввода системного вызова (EINTR, возвращаемый из потенциально длинного блокирующего системного вызова, превращается в ECANCELED в измененном libc-обеспечивает упаковку syscall, если установлен флаг прерывания) и сохраненный флаг прерывания - в тот момент, когда был использован флаг прерывания, он сохраняется в сохраненном флаге прерывания и обнуляется, чтобы флаг прерывания не прерывался при потенциально длительных блокирующих системных вызовах.

Идея состоит в том, что отменяющие сигналы обрабатываются по одному (обработчик сигналов можно оставить с заблокированными всеми / большинством сигналов; затем код обработчика (если таковой имеется) может разблокировать их), и что правильная проверка кода начинает разматываться, т.е. , убирая при возврате ошибки, в тот момент, когда он видит ECANCELED. Тогда следующий потенциально длинный системный вызов блокировки может быть в коде очистки (например, код, который записывает </html> в сокет), и этот системный вызов должен быть доступным (если флаг прерывания остается, он не будет). Конечно, с кодом очистки, содержащим, например, write(1,"</html>",...), он также может блокироваться неопределенно долго, но вы могли бы написать код очистки так, чтобы потенциально длинный системный вызов там выполнялся под таймером, когда очистка вызвана ошибкой (ECANCELED это ошибка). Как я уже упоминал, это расширение позволяет работать с надежными таймерами, не зависящими от состояния гонки, с сигналами.

Преобразование EINTR => ECANCELED происходит так, что зацикливание кода на EINTR знает, когда прекратить зацикливание (многие EINTR (= сигнал прервал системный вызов) не могут быть предотвращены, и код должен просто обработать их, повторив системный вызов. Я использую ECANCELED как «EINTR, после которого вы не должны повторять попытку».

0 голосов
/ 13 ноября 2018

Подумайте о наличии дополнительного файлового дескриптора в наборе fds, переданного poll, единственной задачей которого является возврат poll, когда вы хотите прекратить поток.

Таким образом, в потоке 2 у нас будет что-то вроде:

if (terminateThread1) {
        terminateFlag = 1;
        send (terminate_fd, " ", 1, 0);
        pthread_join (ftid, NULL);
    }
}

И terminate_fd будет в наборе fds, переданных в poll потоком 1.

- ИЛИ -

Если накладные расходы на наличие дополнительного fd на поток слишком велики (как обсуждалось в комментариях), отправьте что-то одному из существующих fd, который поток 1 игнорирует. Это приведет к возврату опроса, а затем завершится поток 1. Вы даже можете использовать это «специальное» значение в качестве флага завершения, что делает логику немного более аккуратной.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...