С приходом в нашу жизнь реализации Linux тредов NPTL, стало доступно досрочное завершение тредов. Представьте себе ситуацию, когда у Вас есть треды с блокирующими системными вызовами или просто треды, использующие sleep() (что ещё хуже) и Вам вдруг понадобилось их завершить. В случае, если Вы программируете на С, скорей всего проблем не будет. Грабли начинают когда Вы решаете сделать тоже самое в C++.

Что может случиться плохого? Такой подход соврешенно не совместим с RAII.
Рассмотрим пример, который гарантированно падает.

  1. #include <unistd.h>
  2. #include <pthread.h>
  3. #include <iostream>
  4. #include <exception>
  5.  
  6. class Object
  7. {
  8. public:
  9.   ~Object() {
  10.   std::cout << "Inside ~Object()" << std::endl;
  11.   sleep ( 999 );
  12.   }
  13. };
  14.  
  15. void newThread()
  16. {
  17.   try {  
  18.     Object regular_object_on_stack;
  19.     std::cout << "Inside the new thread" << std::endl;
  20.     // ... billion strings of code ...
  21.     throw std::runtime_error ( "Random exception" );
  22.   } catch ( const std::runtime_error& e ) {
  23.    std::cout << "You will never see this exception: " << e.what() << std::endl;
  24.   }
  25. }
  26.  
  27. int main()
  28. {
  29.    pthread_t thread;
  30.    pthread_create ( &thread, nullptr, &newThread, nullptr );
  31.    sleep ( 1 ); // time for new thread for correct init order
  32.    std::cout << "Let's try to cancel our thread ..." << std::endl;
  33.    pthread_cancel ( thread );
  34.    pthread_join ( thread, nullptr );
  35.    std::cout << "You have a good chance not to see this message" << std::endl;
  36.    return 0;
  37. }
Вот результат выполнения на Ubuntu 15.10:

  1. Inside the new thread
  2. Inside ~Object()
  3. Let's try to cancel our thread ...
  4. terminate called without an active exception
  5. Aborted (core dumped)
Что же происходит в этом коде такого, что необходимо завершить выполнение? К счастью, в Ubuntu 15.10 хороший вывод ошибок и ясно, что виновен не пойманый exception. Но где? При вызове любого системного вызова отмеченного как "Cancellation point".

Давайте разберём ещё раз, с самого начала. Мы создаём объект класса Object, внутри треда, который спустя секунду, будет отдан на растерзание pthread_cancel(). Внуртри этого же треда, мы кидаем exception, что приводит к stack unwinding и вызову деструкторов объектов на стеке и функция sleep(), переходя в kernel mode и являесь при этом cancellation point, благодаря C++ -- кидает exception, во время раскрутки стека, что и приводит к гарантированному падению нашей программы.

Не верите? :-) Попробуйте такой код:

  1. #include <unistd.h>
  2. #include <pthread.h>
  3. #include <iostream>
  4. #include <exception>
  5.  
  6. class Object
  7. {
  8. public:
  9.   ~Object() {
  10.    std::cout << "Inside ~Object()" << std::endl;
  11.    try {
  12.     sleep ( 999 );
  13.    } catch ( ... ) {
  14.     std::cout << "We got something strange here ..." << std::endl;
  15.    }
  16.  }
  17. };
  18.  
  19. void* newThread ( void* )
  20. {
  21.   try {  
  22.    Object regular_object_on_stack;
  23.    std::cout << "Inside the new thread" << std::endl;
  24.    // ... billion strings of code ...
  25.    throw std::runtime_error ( "Random exception" );
  26.  } catch ( std::runtime_error& e ) {
  27.    std::cout << "You will never see this exception: " << e.what() << std::endl;
  28.  }
  29. }
  30.  
  31. int main()
  32. {
  33.   pthread_t thread;
  34.   pthread_create ( &thread, nullptr, &newThread, nullptr );
  35.   sleep ( 1 ); // time for new thread for correct init order
  36.   std::cout << "Let's try to cancel our thread ..." << std::endl;
  37.   pthread_cancel ( thread );
  38.   pthread_join ( thread, nullptr );
  39.   std::cout << "You have a good chance not to see this message" << std::endl;
  40.   return 0;
  41. }
В результате будет такой вывод:

  1. Inside the new thread
  2. Inside ~Object()
  3. Let's try to cancel our thread ...
  4. We got something strange here ...
  5. FATAL: exception not rethrown
  6. Aborted (core dumped)
Поражены? И это в деструкторе объекта! Как Вы понимаете, о RAII с таким подходом можно забыть. Вы наверняка захотите использовать такие системные вызовы как fclose(), close(), dlclose() в деструкторе Вашего объекта. Список cancellation points достаточно обширен!

Всё, что вы можете -- попытаться контролировать процесс вхожения в cancellation point, используя pthread_testcancel(), которая создаст cancellation point до входа в опасную область видимости. При этом, я сам не уверен, что эта тактика оправданна и вообще имеет право на жизнь.

Интересно, что сам этот "exception" добавлеяет ИМЕННО C++ и его тип определён в хедере cxxabi.h, что значит, что его вполне реально поймать.

  1. #include <unistd.h>
  2. #include <pthread.h>
  3. #include <iostream>
  4. #include <exception>
  5. #include <cxxabi.h>
  6.  
  7. void* newThread ( void* )
  8. {
  9.   try {
  10.     sleep ( 999 );
  11.     std::cout << "Inside the new thread" << std::endl;
  12.     // ... billion strings of code ...
  13.     throw std::runtime_error ( "Random exception" );
  14.   } catch ( abi::__forced_unwind& ) {
  15.     std::cout << "abi::__forced_unwind is catched" << std::endl;
  16.   }
  17. }
  18.  
  19. int main()
  20. {
  21.   pthread_t thread;
  22.   pthread_create ( &thread, nullptr, &newThread, nullptr );
  23.   sleep ( 1 ); // time for new thread for correct init order
  24.   std::cout << "Let's try to cancel our thread ..." << std::endl;
  25.   pthread_cancel ( thread );
  26.   pthread_join ( thread, nullptr );
  27.   std::cout << "You have a good chance not to see this message" << std::endl;
  28.   return 0;
  29. }
Вывод:

  1. Let's try to cancel our thread ...
  2. abi::__forced_unwind is catched
  3. FATAL: exception not rethrown
  4. Aborted (core dumped)
На самом деле, это не столько "exception", сколько механизм для stack unwinding, о чём можно сделать вывод из названия типа "__forced_unwind". Следовательно, раз мы можем его поймать, мы можем его и кинуть далее и даже обработать!

  1. #include <unistd.h>
  2. #include <pthread.h>
  3. #include <iostream>
  4. #include <exception>
  5. #include <cxxabi.h>
  6.  
  7. void* newThread ( void* )
  8. {
  9.   try {
  10.     sleep ( 999 );
  11.     std::cout << "Inside the new thread" << std::endl;
  12.     // ... billion strings of code ...
  13.     throw std::runtime_error ( "Random exception" );
  14.   } catch ( abi::__forced_unwind& ) {
  15.     std::cout << "rethrowing exception ..." << std::endl;
  16.     throw;
  17.   } catch ( ... ) {
  18.     std::cout << "Catch any exception" << std::endl;
  19.   }
  20. }
  21.  
  22. int main()
  23. {
  24.   pthread_t thread;
  25.   pthread_create ( &thread, nullptr, &newThread, nullptr );
  26.   sleep ( 1 ); // time for new thread for correct init order
  27.   std::cout << "Let's try to cancel our thread ..." << std::endl;
  28.   pthread_cancel ( thread );
  29.   pthread_join ( thread, nullptr );
  30.   std::cout << "You have a good chance not to see this message" << std::endl;
  31.   return 0;
  32. }
Покрайней мере, программа уже не падает :-)

  1. Let's try to cancel our thread ...
  2. rethrowing exception ...
  3. You have a good chance not to see this message
Что и требовалось доказать. Использование pthread_cancelation -- весьма сомнительная возможноcть NPTL. Возможно, использование её в языке C оправданно, но в C++, в котором наличествует огромное количество уже готовых типов, которые используют различные системные вызовы, ничего не подозревая о новшествах реализации -- лучше воспользоваться функционалом предоставляемым libstdc++ ну или вообще, BOOST.

Статьи по теме



Статью пока никто не комментировал. Ваш комментарий может стать первым.

Войдите или зарегистрируйтесь, чтобы написать комментарий.