Ну, тогда нужно сделать класс cancelable_function<...>, который будет знать, что выполнять, и как это остановить
Проблема в том, что все блокирующие функции, которые будут вызваны из cancellable_function, тоже должны быть представлены в виде cancellable_function, которые должны каким-то образом связываться с вызывающей cancellable_function, и т.д. В моём подходе аналог cancellable_function — это обычная функция, которая принимает ссылку на cancellation_token и передаёт её вызываемым блокирующим функциям. Мне кажется, что так гораздо лаконичнее.
Тут тоже надо знать, что безопасно для той или иной функции в том или ином контексте. В частности, если функция, которая запускается в потоке, не известна программисту, нужно будет от автора получить или cancellation_handler, который потом использовать при создании токена <...>
Я не вполне понял, что вы имеете в виду под "известна программисту", но от автора ничего получать не надо. Токены не требуют для своего создания cancellation_handler, токен создаётся и живёт вместе с потоком. Когда в этом потоке вызывается прерываемая функция — она использует токен для того, чтобы проверять статус прерывания. Если эта функция вызывает другую прерываемую функцию — она передаёт ей ссылку на этот токен.
Вот небольшой пример программы, которая обрабатывает задачи от пользователя в отдельном потоке:
void do_subtask(const subtask_data& data, const cancellation_token& token)
{
// ...
}
void do_task(const task_data& data, const cancellation_token& token)
{
for (int i = 0; i < Subtasks && token; ++i)
do_subtask(data.get_subtask(i), token);
}
void do_work(concurrent_queue<task_data>& tasks, const cancellation_token& token)
{
while(token)
{
auto data = tasks.pop(token);
if (data)
do_task(*data, token);
}
}
int main()
{
concurrent_queue<task_data> tasks;
rethread::thread t{ [&tasks] (const cancellation_token& token) { do_work(tasks, token); } };
for (auto i = read_input(); i; i = read_input())
tasks.push(*i);
return 0;
}
То есть токен создаётся вместе с потоком, а потом передаётся в каждую блокирующую (или просто длительную) функцию.
Проблема в том, что сам по себе join() ситуацию не улучшит (это обсуждается в разделе "Серьёзная проблема").
Больше всего информации о том, как прервать какую-нибудь блокирующую функцию есть у автора этой функции. В месте создания потока разработчик может и не знать о том, в каких именно вызовах этот поток может уснуть, и как его там прервать. Вот если поток ожидает на многопоточной очереди — как его правильно прервать? Если положить в очередь специальный объект, то его может вытащить другой поток, а не тот, который мы пытались разбудить.
Explicit оператору приведения к булеану нужен для того, чтобы не компилировался следующий код:
standalone_cancellation_token token;
int i = token + 5;
pthread_cleanup_push/pthread_cleanup_pop я упоминать не стал, потому что плюсах им есть отличная альтернатива: RAII.
Проблема с обработчиками прерывания в Си в том, что их не все пишут. Из-за этого минусы использования pthread_cancel могут проявиться не сразу, а с задержкой.
Реальная история: в проекте появляется ещё одна 3-party библиотека, написанная на Си (обёртка над специфичным железом). Без исходного кода и связи с разработчиками библиотеки. Через несколько месяцев обнаруживаются странные дэдлоки, которых быть не должно. После нескольких дней дебага выясняется, что если вызвать pthread_cancel для потока в тот момент, когда этот поток находится внутри этой библиотеки — то он просто тихо умирает, даже не разматывая свой стек.
Насчёт отдельных задач — я имел в виду отмену долгой задачи без прерывания всего потока. Например, в приложении-навигаторе может быть отдельный поток для того, чтобы рассчитывать маршрут. Рассчёт маршрута из точки А в точку Б — это отдельная задача. Если пользователь изменил начальную и конечную точки — расчёт можно отменить, но прерывать весь поток смысла нет.
Для того, чтобы прерывать блокирующие IO-операции, необходимо реализовать соответствующий cancellation_handler. Для POSIX'а можно использовать такую схему — вместо того, чтобы ждать данных в read, места во write, подключения в accept и т.д. — можно всё ожидание делать в вызове poll. Тогда для отмены блокирующиего IO достаточно использовать прерываемую версию poll, которая уже есть в rethread: https://github.com/bo-on-software/rethread/blob/master/rethread/poll.hpp
Проблема в том, что все блокирующие функции, которые будут вызваны из cancellable_function, тоже должны быть представлены в виде cancellable_function, которые должны каким-то образом связываться с вызывающей cancellable_function, и т.д. В моём подходе аналог cancellable_function — это обычная функция, которая принимает ссылку на cancellation_token и передаёт её вызываемым блокирующим функциям. Мне кажется, что так гораздо лаконичнее.
Я не вполне понял, что вы имеете в виду под "известна программисту", но от автора ничего получать не надо. Токены не требуют для своего создания cancellation_handler, токен создаётся и живёт вместе с потоком. Когда в этом потоке вызывается прерываемая функция — она использует токен для того, чтобы проверять статус прерывания. Если эта функция вызывает другую прерываемую функцию — она передаёт ей ссылку на этот токен.
Вот небольшой пример программы, которая обрабатывает задачи от пользователя в отдельном потоке:
То есть токен создаётся вместе с потоком, а потом передаётся в каждую блокирующую (или просто длительную) функцию.
Спасибо.
С февраля библиотека обросла тестами, документацией и переехала на атомики вместо мьютексов, что отлично сказалось на производительности.
Проблема в том, что сам по себе join() ситуацию не улучшит (это обсуждается в разделе "Серьёзная проблема").
Больше всего информации о том, как прервать какую-нибудь блокирующую функцию есть у автора этой функции. В месте создания потока разработчик может и не знать о том, в каких именно вызовах этот поток может уснуть, и как его там прервать. Вот если поток ожидает на многопоточной очереди — как его правильно прервать? Если положить в очередь специальный объект, то его может вытащить другой поток, а не тот, который мы пытались разбудить.
Explicit оператору приведения к булеану нужен для того, чтобы не компилировался следующий код:
Извиняюсь за орфографию.
Добрый день.
pthread_cleanup_push/pthread_cleanup_pop я упоминать не стал, потому что плюсах им есть отличная альтернатива: RAII.
Проблема с обработчиками прерывания в Си в том, что их не все пишут. Из-за этого минусы использования pthread_cancel могут проявиться не сразу, а с задержкой.
Реальная история: в проекте появляется ещё одна 3-party библиотека, написанная на Си (обёртка над специфичным железом). Без исходного кода и связи с разработчиками библиотеки. Через несколько месяцев обнаруживаются странные дэдлоки, которых быть не должно. После нескольких дней дебага выясняется, что если вызвать pthread_cancel для потока в тот момент, когда этот поток находится внутри этой библиотеки — то он просто тихо умирает, даже не разматывая свой стек.
Насчёт отдельных задач — я имел в виду отмену долгой задачи без прерывания всего потока. Например, в приложении-навигаторе может быть отдельный поток для того, чтобы рассчитывать маршрут. Рассчёт маршрута из точки А в точку Б — это отдельная задача. Если пользователь изменил начальную и конечную точки — расчёт можно отменить, но прерывать весь поток смысла нет.
Для того, чтобы прерывать блокирующие IO-операции, необходимо реализовать соответствующий cancellation_handler. Для POSIX'а можно использовать такую схему — вместо того, чтобы ждать данных в
read
, места воwrite
, подключения вaccept
и т.д. — можно всё ожидание делать в вызовеpoll
. Тогда для отмены блокирующиего IO достаточно использовать прерываемую версиюpoll
, которая уже есть в rethread: https://github.com/bo-on-software/rethread/blob/master/rethread/poll.hpp