Журнал LinuxFormat - перейти на главную

LXF108:Дырки в паутине

Материал из Linuxformat
(Различия между версиями)
Перейти к: навигация, поиск
(Новая: == Дырки в паутине == : ''Утечка ресурсов – в первую очередь, памяти – одна из проблем современных сложны...)
 
м (Акроним переименовал страницу LХF100-101:Зaпycк пo ceти в LXF108:Дырки в паутине поверх перенаправления)
 
(не показаны 5 промежуточных версий 2 участников)
Строка 1: Строка 1:
 
== Дырки в паутине ==
 
== Дырки в паутине ==
: ''Утечка ресурсов в первую очередь, памяти одна из проблем современных сложных приложений. '''Андрей Кузьменко''' покажет, как избежать ее в ваших программах.''
+
: ''Утечка ресурсов в первую очередь, памяти одна из проблем современных сложных приложений. '''Андрей Кузьменко''' покажет, как избежать ее в ваших программах.''
  
 
Можно смело считать 2007 год переломным моментом
 
Можно смело считать 2007 год переломным моментом
Строка 9: Строка 9:
 
текущем году производители чипов уже делают упор на четырехъядерные процессоры. Очевидно, что производители ПО должны адекватно реагировать на эти изменения, выпуская продукты, задействующие все преимущества новых технологий. А на чем они пишутся?
 
текущем году производители чипов уже делают упор на четырехъядерные процессоры. Очевидно, что производители ПО должны адекватно реагировать на эти изменения, выпуская продукты, задействующие все преимущества новых технологий. А на чем они пишутся?
  
 +
{{Врезка
 +
|Заголовок=Мы про это уже писали
 +
|Содержание=Если вы хотите
 +
узнать больше
 +
о программировании с использованием Pthreads,
 +
обратитесь к
 +
нашим учебникам,
 +
опубликованным
 +
в [[LXF86:Unix API|LXF86]]-[[LXF87-88:Unix API|87/88]].
 +
|Ширина=100px}}
 
В языке C++, в отличие, например, от Java, отсутствует встроенная поддержка многопоточного программирования. Иными словами, с помощью «голого» C++ нельзя создавать соответствующие
 
В языке C++, в отличие, например, от Java, отсутствует встроенная поддержка многопоточного программирования. Иными словами, с помощью «голого» C++ нельзя создавать соответствующие
 
стандарту языка многопоточные приложения. Разумеется, данная
 
стандарту языка многопоточные приложения. Разумеется, данная
Строка 24: Строка 34:
 
=== И снова классы… ===
 
=== И снова классы… ===
 
Одной из серьезных проблем языка C++ (настолько серьезной,
 
Одной из серьезных проблем языка C++ (настолько серьезной,
что иные компании создают целые платформы со сборкой мусо-
+
что иные компании создают целые платформы со сборкой мусора, лишь бы с нею не сталкиваться) является утечка ресурсов. Это
ра, лишь бы с нею не сталкиваться) является утечка ресурсов. Это
+
может быть память, выделенная с помощью оператора new, файловые дескрипторы, сетевые сокеты, мьютексы. Для освобождения
может быть память, выделенная с помощью оператора new, фай-
+
ресурсов, используемых объектом, в C++ предусмотрены специальные методы деструкторы. Задача программиста правильно
ловые дескрипторы, сетевые сокеты, мьютексы. Для освобождения
+
ресурсов, используемых объектом, в C++ предусмотрены специ-
+
альные методы деструкторы. Задача программиста правильно
+
 
написать деструктор и убедиться в том, что в программе происходит
 
написать деструктор и убедиться в том, что в программе происходит
его вызов. Только так можно гарантировать, что ресурсы будут воз-
+
его вызов. Только так можно гарантировать, что ресурсы будут возвращены системе.
вращены системе.
+
  
 
Во всех тестах, описываемых в этой статье, будет использоваться класс checker, объявленный в файле checker.hpp. Вот он:
 
Во всех тестах, описываемых в этой статье, будет использоваться класс checker, объявленный в файле checker.hpp. Вот он:
Строка 58: Строка 64:
 
В документе The Open Group Base Specifications Issue 6 IEEE Std
 
В документе The Open Group Base Specifications Issue 6 IEEE Std
 
1003.1, 2004 Edition в разделе, посвященном функции pthread_exit(),
 
1003.1, 2004 Edition в разделе, посвященном функции pthread_exit(),
по этому поводу говорится следующее [здесь и далее перевод авто-
+
по этому поводу говорится следующее [здесь и далее перевод автора]: «Функция void pthread_exit(void *value_ptr) завершает вызывающий поток и делает значение value_ptr доступным для успешного присоединения к завершающему потоку. Любые обработчики
ра]: «Функция void pthread_exit(void *value_ptr) завершает вызы-
+
вающий поток и делает значение value_ptr доступным для успеш-
+
ного присоединения к завершающему потоку. Любые обработчики
+
 
отмены, помещенные в стек, но еще не извлеченные из него, будут
 
отмены, помещенные в стек, но еще не извлеченные из него, будут
извлечены в порядке, обратном по отношению к порядку помеще-
+
извлечены в порядке, обратном по отношению к порядку помещения в стек, а после выполнены. Если потоку принадлежат данные, то
ния в стек, а после выполнены. Если потоку принадлежат данные, то
+
после выполнения всех обработчиков отмены будут вызваны соответствующие функции деструкторов, при этом порядок их вызова
после выполнения всех обработчиков отмены будут вызваны соот-
+
ветствующие функции деструкторов, при этом порядок их вызова
+
 
не определен. При завершении потока ресурсы процесса, включая
 
не определен. При завершении потока ресурсы процесса, включая
мьютексы и дескрипторы файлов, не освобождаются, и не выполня-
+
мьютексы и дескрипторы файлов, не освобождаются, и не выполняются никакие восстановительные действия уровня процесса, включая всевозможные вызовы любых функций atexit()». То есть, если
ются никакие восстановительные действия уровня процесса, вклю-
+
объект в потоковой функции представляет собой локальную переменную с классом памяти auto, то после вызова pthread_exit() для
чая всевозможные вызовы любых функций atexit()». То есть, если
+
объект в потоковой функции представляет собой локальную пере-
+
менную с классом памяти auto, то после вызова pthread_exit() для
+
 
него должен быть автоматически выполнен деструктор. Однако, как
 
него должен быть автоматически выполнен деструктор. Однако, как
 
показывает практика, бывают случаи, когда этого не происходит.
 
показывает практика, бывают случаи, когда этого не происходит.
  
 
Рассмотрим следующую программу:
 
Рассмотрим следующую программу:
<sorce lang="cpp">void* task1(void *X) {
+
<source lang="cpp">void* task1(void *X) {
std::cout<<" Start task_1!"<<std::endl;
+
std::cout<<" Start task_1!"<<std::endl;
checker P("First"); P.calc(5);
+
checker P("First"); P.calc(5);
pthread_exit(NULL);
+
pthread_exit(NULL);
return 0;
+
return 0;
 
}
 
}
 
void* task2(void *X) {
 
void* task2(void *X) {
std::cout<<" Start task_2!"<<std::endl;
+
std::cout<<" Start task_2!"<<std::endl;
checker Q("Second"); Q.calc(8);
+
checker Q("Second"); Q.calc(8);
return 0;
+
return 0;
 
}
 
}
 
int main(void){
 
int main(void){
std::cout<<" Start test #1!"<<std::endl;
+
std::cout<<" Start test #1!"<<std::endl;
pthread_t threadA, threadB;
+
pthread_t threadA, threadB;
pthread_create(&threadA, NULL, task1, NULL);
+
pthread_create(&threadA, NULL, task1, NULL);
pthread_detach(threadA);
+
pthread_detach(threadA);
pthread_create(&threadB, NULL, task2, NULL);
+
pthread_create(&threadB, NULL, task2, NULL);
pthread_detach(threadB);
+
pthread_detach(threadB);
pthread_exit(NULL);
+
pthread_exit(NULL);
return 0;
+
return 0;
 
}</source>
 
}</source>
 
Здесь объявлены две потоковых функции: task1() и task2(). В
 
Здесь объявлены две потоковых функции: task1() и task2(). В
 
качестве элемента данных в каждой из них используется объект
 
качестве элемента данных в каждой из них используется объект
 
класса checker, выделенный в стеке. Функция task1() завершается
 
класса checker, выделенный в стеке. Функция task1() завершается
принудительно с помощью pthread_exit(), а task2() выходит «есте-
+
принудительно с помощью pthread_exit(), а task2() выходит «естественным образом» через return 0. При этом потоки создаются
ственным образом» через return 0. При этом потоки создаются
+
как открепленные, то есть при их уничтожении ресурсы, которые они
как открепленные, т.е. при их уничтожении ресурсы, которые они
+
 
использовали, сразу же возвращаются системе.
 
использовали, сразу же возвращаются системе.
Рассмотрим результат работы программы в различных дистрибу-
+
Рассмотрим результат работы программы в различных дистрибутивах Linux. Например, в SLAX 6.0.3 вывод на консоль будет таким:
тивах Linux. Например, в SLAX 6.0.3 вывод на консоль будет таким:
+
 
<pre>Start test #1!
 
<pre>Start test #1!
 
Start task_1!
 
Start task_1!
Строка 122: Строка 118:
 
Destructor done! Name:Second</pre>
 
Destructor done! Name:Second</pre>
 
Видите? Деструктор объекта из потоковой функции task1()
 
Видите? Деструктор объекта из потоковой функции task1()
вызван не был! Конечно, 3.0.4 не самая актуальная версия данно-
+
вызван не был! Конечно, 3.0.4 не самая актуальная версия данного дистрибутива, но, как мы увидим далее, аналогичные проблемы
го дистрибутива, но, как мы увидим далее, аналогичные проблемы
+
 
имеют место и в более современных ОС.
 
имеют место и в более современных ОС.
  
=== Поел убери за собой ===
+
=== Поел убери за собой ===
Очень часто при завершении потока бывает необходимо выпол-
+
Очень часто при завершении потока бывает необходимо выполнить некоторые заключительные операции: освободить память,
нить некоторые заключительные операции: освободить память,
+
 
закрыть файлы, снять блокировки с разделяемых переменных и
 
закрыть файлы, снять блокировки с разделяемых переменных и
 
т.п. Желательно, чтобы эти действия выполнялись единообразно,
 
т.п. Желательно, чтобы эти действия выполнялись единообразно,
как для стандартного завершения потоковой функции операто-
+
как для стандартного завершения потоковой функции оператором return, так и при аннулировании другим потоком. Библиотека
ром return, так и при аннулировании другим потоком. Библиотека
+
Pthreads предоставляет для этого возможность, называемую «стеком очистительно-восстановительных операций». Как она работает? С каждым потоком, имеющимся в программе, связывается
Pthreads предоставляет для этого возможность, называемую «сте-
+
стек очистительно-восстановительных операций, который содержит указатели на функции, вызываемые во время аннулирования
ком очистительно-восстановительных операций». Как она рабо-
+
тает? С каждым потоком, имеющимся в программе, связывается
+
стек очистительно-восстановительных операций, который содер-
+
жит указатели на функции, вызываемые во время аннулирования
+
 
(завершения) потока. Для работы с данным стеком используются
 
(завершения) потока. Для работы с данным стеком используются
 
две функции (или макроса):
 
две функции (или макроса):
pthread_cleanup_push() Принимает в качестве параметров указа-
+
* pthread_cleanup_push() Принимает в качестве параметров указатель на помещаемую в стек функцию и передаваемый ей аргумент;
тель на помещаемую в стек функцию и передаваемый ей аргумент;
+
* pthread_cleanup_pop() Принимает в качестве параметра целочисленное значение и извлекает завершающую функцию с вершины стека. Если аргумент отличен от нуля, завершающая функция выполняется.
pthread_cleanup_pop() Принимает в качестве параметра цело-
+
 
численное значение и извлекает завершающую функцию с верши-
+
ны стека. Если аргумент отличен от нуля, завершающая функция
+
выполняется.
+
 
Давайте рассмотрим еще один пример. Здесь мы определяем
 
Давайте рассмотрим еще один пример. Здесь мы определяем
 
потоковую функцию, которая в зависимости от значения параметра,
 
потоковую функцию, которая в зависимости от значения параметра,
 
заданного пользователем, завершается либо «обычным образом»,
 
заданного пользователем, завершается либо «обычным образом»,
 
либо вызовом pthread_exit():
 
либо вызовом pthread_exit():
void* task1(void *X){
+
<source lang="cpp">void* task1(void *X){
std::cout<<" Start test thread!"<<std::endl;
+
std::cout<<" Start test thread!"<<std::endl;
checker *Z = new checker("agent");
+
checker *Z = new checker("agent");
pthread_cleanup_push(del_ptr_checker, Z);
+
pthread_cleanup_push(del_ptr_checker, Z);
int *counter = static_cast<int*>(X);
+
int *counter = static_cast<int*>(X);
for(int i=0; i<(*counter); ++i)
+
for(int i=0; i<(*counter); ++i)
{
+
{
if(i==1000) pthread_exit(NULL);
+
  if(i==1000) pthread_exit(NULL);
}
+
}
pthread_cleanup_pop(1);
+
pthread_cleanup_pop(1);
std::cout<<" Thread go boom!"<<std::endl;
+
std::cout<<" Thread go boom!"<<std::endl;
return 0;
+
return 0;
}
+
}</source>
 
Обратите внимание, что в качестве элемента данных теперь
 
Обратите внимание, что в качестве элемента данных теперь
используется экземпляр класса checker, расположенный в дина-
+
используется экземпляр класса checker, расположенный в динамической памяти. Для ее освобождения была написана функция del_ptr_checker(), указатель на которую помещается в стек
мической памяти. Для ее освобождения была написана функ-
+
очистительно-восстановительных операций. Мы вызываем деструктор объекта при помощи pthread_cleanup_pop(1) кажется, это
ция del_ptr_checker(), указатель на которую помещается в стек
+
очистительно-восстановительных операций. Мы вызываем деструк-
+
тор объекта при помощи pthread_cleanup_pop(1) кажется, это
+
 
должно гарантировать выполнение очистительных действий вне
 
должно гарантировать выполнение очистительных действий вне
 
зависимости от способа завершения потока.
 
зависимости от способа завершения потока.
 
Функция del_ptr_checker() сама по себе довольно проста:
 
Функция del_ptr_checker() сама по себе довольно проста:
void del_ptr_checker(void *X){
+
<source lang="cpp">void del_ptr_checker(void *X){
 
checker *del = static_cast<checker*>(X);
 
checker *del = static_cast<checker*>(X);
 
std::cout<<" #-> START del_ptr_checker!"<<std::endl;
 
std::cout<<" #-> START del_ptr_checker!"<<std::endl;
Строка 178: Строка 162:
 
del = 0;
 
del = 0;
 
std::cout<<" #-> END del_ptr_checker!"<<std::endl;
 
std::cout<<" #-> END del_ptr_checker!"<<std::endl;
}
+
}</source>
Мы приводим переданный указатель к типу checker * и вызы-
+
Мы приводим переданный указатель к типу checker * и вызываем оператор delete для освобождения памяти. del_ptr_checker()
ваем оператор delete для освобождения памяти. del_ptr_checker()
+
 
определена в файле helper.hpp. Функция main() для нашего примера
 
определена в файле helper.hpp. Функция main() для нашего примера
 
выглядит так:
 
выглядит так:
int main(void)
+
<source lang="cpp">int main(void)
 
{
 
{
 
std::cout<<" START TEST #2!"<<std::endl;
 
std::cout<<" START TEST #2!"<<std::endl;
Строка 193: Строка 176:
 
std::cout<<" End MAIN"<<std::endl;
 
std::cout<<" End MAIN"<<std::endl;
 
return 0;
 
return 0;
}
+
}</source>
  
 
Приведу результаты выполнения двух тестов в SLAX 6.0.3:
 
Приведу результаты выполнения двух тестов в SLAX 6.0.3:
START TEST #2!
+
<pre>START TEST #2!
 
Enter N:222
 
Enter N:222
 
Start test thread!
 
Start test thread!
Строка 212: Строка 195:
 
Destructor done! Name:agent
 
Destructor done! Name:agent
 
#-> END del_ptr_checker!
 
#-> END del_ptr_checker!
End MAIN
+
End MAIN</pre>
Как мы видим, функция del_ptr_checker() вызывается независи-
+
Как мы видим, функция del_ptr_checker() вызывается независимо от значения параметра N, задаваемого пользователем, и динамическая память, занимаемая объектом класса checker, всегда корректно освобождается.
мо от значения параметра N, задаваемого пользователем, и дина-
+
мическая память, занимаемая объектом класса checker, всегда кор-
+
ректно освобождается.
+
 
А вот что происходит в OpenSUSE 10.1:
 
А вот что происходит в OpenSUSE 10.1:
linux@linux:~/super> ./test_2
+
<pre>linux@linux:~/super> ./test_2
 
Enter N:654
 
Enter N:654
 
End MAIN
 
End MAIN
Строка 224: Строка 204:
 
Enter N:8888
 
Enter N:8888
 
End MAIN
 
End MAIN
linux@linux:~/super>
+
linux@linux:~/super></pre>
 
Любопытно, но судя по выводу на консоль, поток, использующий
 
Любопытно, но судя по выводу на консоль, поток, использующий
динамическую переменную класса checker, даже не создается, не гово-
+
динамическую переменную класса checker, даже не создается, не говоря уже о вызове деструктора для объекта. Очень интересный результат!
ря уже о вызове деструктора для объекта. Очень интересный результат!
+
 
Кстати, аналогичное поведение наблюдается и в Mpentoo Linux 2006.1.
 
Кстати, аналогичное поведение наблюдается и в Mpentoo Linux 2006.1.
  
=== Я тебя породил... ===
+
=== Я тебя породил… ===
 
Бывают ситуации, когда одному потоку нужно завершить другой:
 
Бывают ситуации, когда одному потоку нужно завершить другой:
 
это может делаться при организации управления программой
 
это может делаться при организации управления программой
 
или для экономии ограниченных ресурсов. В библиотеке Pthreads
 
или для экономии ограниченных ресурсов. В библиотеке Pthreads
для этих целей предназначена функция pthread_cancel(). В каче-
+
для этих целей предназначена функция pthread_cancel(). В качестве параметра она принимает идентификатор потоковой функции, которую надо завершить, и возвращает 0 в случае успешного выполнения. В уже упоминавшемся документе The Open Group
стве параметра она принимает идентификатор потоковой функ-
+
ции, которую надо завершить, и возвращает 0 в случае успешно-
+
го выполнения. В уже упоминавшемся документе The Open Group
+
 
Base Specifications Issue 6 IEEE Std 1003.1, 2004 Edition в разделе,
 
Base Specifications Issue 6 IEEE Std 1003.1, 2004 Edition в разделе,
 
описывающем функцию pthread_cancel(), говорится: «Функция
 
описывающем функцию pthread_cancel(), говорится: «Функция
 
pthread_cancel() создает запрос на отмену потока. Когда он будет
 
pthread_cancel() создает запрос на отмену потока. Когда он будет
 
реализован, зависит от текущего состояния потока и его типа. При
 
реализован, зависит от текущего состояния потока и его типа. При
отмене потока должны быть вызваны обработчики, которые выпол-
+
отмене потока должны быть вызваны обработчики, которые выполнят связанные с отменой подготовительные действия. По завершению последнего обработчика должны быть вызваны деструкторы
нят связанные с отменой подготовительные действия. По заверше-
+
нию последнего обработчика должны быть вызваны деструкторы
+
 
данных, используемых потоком».
 
данных, используемых потоком».
  
Посмотрим, что же действительно происходит в данной ситуа-
+
Посмотрим, что же действительно происходит в данной ситуации, с помощью следующей простой программы:
ции, с помощью следующей простой программы:
+
 
<source lang="cpp">void *task1(void *X){
 
<source lang="cpp">void *task1(void *X){
 
  std::cout<<" Start thread!"<<std::endl;
 
  std::cout<<" Start thread!"<<std::endl;
Строка 283: Строка 256:
 
второй имеет класс auto. Для уничтожения первого экземпляра мы
 
второй имеет класс auto. Для уничтожения первого экземпляра мы
 
опять используем стек очистительно-восстановительных операций.
 
опять используем стек очистительно-восстановительных операций.
Вызов pthread_setcancelstate() разрешает аннулирование нашего пото-
+
Вызов pthread_setcancelstate() разрешает аннулирование нашего потока другим. Функция pthread_testcancel() проверяет наличие необработанных запросов на уничтожение. Если они есть, процесс аннулирования активизируется в точке вызова pthread_testcancel(). Обратите внимание, что в потоковой функции не используются разделяемые переменные и не происходит вызова системных функций (кроме вывода
ка другим. Функция pthread_testcancel() проверяет наличие необрабо-
+
танных запросов на уничтожение. Если они есть, процесс аннулирова-
+
ния активизируется в точке вызова pthread_testcancel(). Обратите вни-
+
мание, что в потоковой функции не используются разделяемые пере-
+
менные и не происходит вызова системных функций (кроме вывода
+
 
сообщений на консоль), что обеспечивает ее безопасное прекращение.
 
сообщений на консоль), что обеспечивает ее безопасное прекращение.
  
Строка 319: Строка 287:
 
* Тест № 1. При запуске программы в OpenSUSE 10.1 наблюдается порядок вызова деструкторов, отличный от всех других систем, получивших +. Однако все деструкторы вызываются, и утечки памяти не происходит.
 
* Тест № 1. При запуске программы в OpenSUSE 10.1 наблюдается порядок вызова деструкторов, отличный от всех других систем, получивших +. Однако все деструкторы вызываются, и утечки памяти не происходит.
 
* Тест № 2. В двух системах, отмеченных ?, наблюдалась неустойчивая работа теста. Она проявлялась в том, что в одном случае результат теста был «правильным», и вывод на консоль полностью соответствовал ожидаемому, а в другом случае наблюдалась проблема с запуском потоковой функции.
 
* Тест № 2. В двух системах, отмеченных ?, наблюдалась неустойчивая работа теста. Она проявлялась в том, что в одном случае результат теста был «правильным», и вывод на консоль полностью соответствовал ожидаемому, а в другом случае наблюдалась проблема с запуском потоковой функции.
 +
 +
{| style="background:white;color:black;" border="1" cellspacing="0" align="center"
 +
|+ Сводная таблица результатов тестирования
 +
!rowspan="2"| №
 +
!rowspan="2"| Операционная система
 +
!colspan="3"| Номер теста
 +
|-
 +
!1 || 2|| 3
 +
|-
 +
!1
 +
|
 +
; Mandriva 2008 KDE LiveCD
 +
: Ядро: 2.6.22
 +
: GCC: 4.2.2*
 +
: Glibc: 2.6.1
 +
: Libstdc++: 5.0.7 / 6.0.9
 +
| + ||?|| +
 +
|-
 +
!2
 +
|
 +
; Fedora 8 LiveCD
 +
: Ядро: 2.6.23
 +
: GCC: 4.1.2*
 +
: Glibc: 2.7
 +
: Libstdc++: 6.0.8
 +
| + || X|| +
 +
|-
 +
!3
 +
|
 +
; ASP Linux 11 LiveCD
 +
: Ядро: 2.6.14
 +
: GCC: 4.0.2
 +
: Glibc: 2.3.5
 +
: Libstdc++: 5.0.7 / 6.0.7
 +
| + || X|| X
 +
|-
 +
!4
 +
|
 +
; Knoppix 5.3.1 LiveDVD
 +
: Ядро: 2.6.24
 +
: GCC: 4.2.3
 +
: Glibc: 2.7
 +
: Libstdc++: 5.0.7 / 6.0.10
 +
| + || +|| +
 +
|-
 +
!5
 +
|
 +
; SLAX 6.0.3 LiveCD
 +
: Ядро: 2.6.24
 +
: GCC: 4.2.3
 +
: Glibc: 2.7
 +
: Libstdc++: 6.0.8 / 6.0.9
 +
| +|| +|| +
 +
|-
 +
!6
 +
|
 +
; Ubuntu 8.0.4 LiveDVD
 +
: Ядро: 2.6.24
 +
: GCC: 4.2.3
 +
: Glibc: 2.7
 +
: Libstdc++: 6.0.9
 +
| + || +|| +
 +
|-
 +
!7
 +
|
 +
; Ubuntu 7.10 LiveCD
 +
: Ядро: 2.6.22
 +
: GCC: 4.1.3
 +
: Glibc: 2.6.1
 +
: Libstdc++: 6.0.9
 +
| + || ?|| X
 +
|-
 +
!8
 +
|
 +
; ALT Linux 3.0.4 LiveCD
 +
: Ядро: 2.6.12
 +
: GCC: 3.4.4*
 +
: Glibc: 2.3.5
 +
: Libstdc++: 5.0.7 / 6.0.3
 +
|X||X|| X
 +
|-
 +
!9
 +
|
 +
; ALT Linux 4.0.3
 +
: Ядро: 2.6.18
 +
: GCC: 4.1.1
 +
: Glibc: 2.5
 +
: Libstdc++: 5.0.7 / 6.0.8
 +
| + || X|| X
 +
|-
 +
!10
 +
|
 +
; MPentoo 2006.1 LiveCD
 +
: Ядро: 2.6.16
 +
: GCC: 3.3.6
 +
: Glibc: 2.3.6
 +
: Libstdc++: 5.0.7
 +
|X|| X|| X
 +
|-
 +
!11
 +
|
 +
; Puppy Linux (rus_100) LiveCD
 +
: Ядро: 2.6.21
 +
: GCC: 4.1.2*
 +
: Glibc: 2.5
 +
: Libstdc++: 5.0.6 / 6.0.8
 +
| + || X|| X
 +
|-
 +
!12
 +
|
 +
; Gentoo 2008 Beta2 LiveCD
 +
: Ядро: 2.6.24
 +
: GCC: 4.1.2
 +
: Glibc: 2.6.1
 +
: Libstdc++: 6.0.8
 +
| + || +|| +
 +
|-
 +
!13
 +
|
 +
; OpenSUSE 10.1 LiveDVD
 +
: Ядро: 2.6.16
 +
: GCC: 4.1.0*
 +
: Glibc: 2.4
 +
: Libstdc++: 5.0.7 / 6.0.8
 +
| ? || X ||X
 +
|}
 +
{| align="center"
 +
| Обозначения:
 +
: X — тест выполнен с ошибками, результат не соответствует ожиданиям
 +
: ? — неожиданный результат выполнения теста, интересен для анализа
 +
: + — тест успешно выполнен, результат адекватен ожиданиями
 +
: * — на диске компилятор отсутствует, проверка на бинарной сборке из SLAX
 +
|}
  
 
=== Что в итоге? ===
 
=== Что в итоге? ===
Какие же выводы можно сделать на основании результатов, отра-
+
Какие же выводы можно сделать на основании результатов, отраженных в таблице? Во-первых, при разработке многопоточных
женных в таблице? Во-первых, при разработке многопоточных
+
 
приложений с использованием библиотеки Pthreads программист
 
приложений с использованием библиотеки Pthreads программист
 
должен уделять повышенное внимание тем фрагментам кода,
 
должен уделять повышенное внимание тем фрагментам кода,
 
где используются объекты классов. Несмотря на то, что Pthreads
 
где используются объекты классов. Несмотря на то, что Pthreads
имеет средство автоматического освобождения ресурсов стек
+
имеет средство автоматического освобождения ресурсов стек
очистительно-восстановительных операций, гарантировать обяза-
+
очистительно-восстановительных операций, гарантировать обязательность и правильность его использования нельзя. В описании
тельность и правильность его использования нельзя. В описании
+
многих функций библиотеки сообщается, что при завершении потока сначала вызываются процедуры из очистительного стека, а потом
многих функций библиотеки сообщается, что при завершении пото-
+
ка сначала вызываются процедуры из очистительного стека, а потом
+
 
деструкторы потоковых данных, а на самом деле это не всегда так.
 
деструкторы потоковых данных, а на самом деле это не всегда так.
  
 
Во-вторых, разработчики Linux-систем вполне осведомлены об
 
Во-вторых, разработчики Linux-систем вполне осведомлены об
особенностях поведения функций библиотеки Pthreads при исполь-
+
особенностях поведения функций библиотеки Pthreads при использовании в качестве данных объектов классов. Ведется активная
зовании в качестве данных объектов классов. Ведется активная
+
 
работа в этом направлении, и положительные результаты есть!
 
работа в этом направлении, и положительные результаты есть!
 
Показателен пример Ubuntu.
 
Показателен пример Ubuntu.
Строка 341: Строка 438:
 
В-третьих, анализ характеристик дистрибутивов, получивших +
 
В-третьих, анализ характеристик дистрибутивов, получивших +
 
по всем тестам, обнаруживает, что версия ядра (в этих тестовых
 
по всем тестам, обнаруживает, что версия ядра (в этих тестовых
образцах) не ниже 2.6.24 (это обязательное условие), библио-
+
образцах) не ниже 2.6.24 (это обязательное условие), библиотека glibc не ниже 2.6.1 (лучше 2.7), библиотека libstdc++ не
тека glibc не ниже 2.6.1 (лучше 2.7), библиотека libstdc++ не
+
ниже 6.0.8 (лучше 6.0.9), версия компилятора GCC не ниже 4.1.2.
ниже 6.0.8 (лучше 6.0.9), версия компилятора GCC не ниже 4.1.2.
+
 
Соответственно, дистрибутивы, получившие за тест -, имеют другие
 
Соответственно, дистрибутивы, получившие за тест -, имеют другие
версии библиотек. Например, в ALT Linux 4.0.3 используется библи-
+
версии библиотек. Например, в ALT Linux 4.0.3 используется библиотека glibc версии 2.5. Кстати, очень интересно сравнить результаты
отека glibc версии 2.5. Кстати, очень интересно сравнить результаты
+
Gentoo 2008 Beta 2 и Fedora Core 8. Исход их «спора» решила версия ядра. У Gentoo она выше, хотя у Fedora библиотека glibc новее.
Gentoo 2008 Beta 2 и Fedora Core 8. Исход их «спора» решила вер-
+
сия ядра. У Gentoo она выше, хотя у Fedora библиотека glibc новее.
+
 
Это говорит о том, что своевременное обновление ядра и ключевых
 
Это говорит о том, что своевременное обновление ядра и ключевых
системных библиотек позволяет повысить надежность работы опе-
+
системных библиотек позволяет повысить надежность работы операционной системы.
рационной системы.
+
  
Читателя наверняка интересует вопрос: а что будет, если про-
+
Читателя наверняка интересует вопрос: а что будет, если программу, скомпилированную в «правильной» системе, например,
грамму, скомпилированную в «правильной» системе, например,
+
 
Knoppix 5.3.1, попробовать запустить в «неправильной», скажем,
 
Knoppix 5.3.1, попробовать запустить в «неправильной», скажем,
 
Knoppix 3.2 RE? Здесь возможны два варианта:
 
Knoppix 3.2 RE? Здесь возможны два варианта:
Строка 364: Строка 456:
  
 
=== И что делать? ===
 
=== И что делать? ===
 +
{{Врезка
 +
|Заголовок=Скорая помощь
 +
|Содержание=Узнать параметры своей
 +
системы можно,
 +
набрав в консоли следующие
 +
команды:
 +
* Версия ядра:
 +
uname -a
 +
* Компилятор GCC:
 +
gcc --version
 +
* Библиотека glibc :
 +
getconf GNU_LIBC_VERSION
 +
* Библиотека libstdc++:
 +
ls -l /usr/lib/libstdc++.so.*
 +
|Ширина=250px}}
 
Что можно посоветовать, чтобы свести к минимуму издержки, связанные с особенностями взаимодействия библиотеки Pthreads с
 
Что можно посоветовать, чтобы свести к минимуму издержки, связанные с особенностями взаимодействия библиотеки Pthreads с
 
объектами классов?
 
объектами классов?
  
Что касается теста №1, то тут может помочь метафора «песочницы»: прием, при котором вся работа с объектами классов, имеющих тип памяти auto (не динамические, а «обычные» переменные), ведется в пределах блока, выделенного в тексте программы
+
Что касается теста № 1, то тут может помочь метафора «песочницы»: прием, при котором вся работа с объектами классов, имеющих тип памяти auto (не динамические, а «обычные» переменные), ведется в пределах блока, выделенного в тексте программы
 
фигурными скобками {…}. При выходе из блока происходит автоматический вызов деструкторов, после чего можно «запускать»
 
фигурными скобками {…}. При выходе из блока происходит автоматический вызов деструкторов, после чего можно «запускать»
 
pthread_exit().
 
pthread_exit().
Строка 377: Строка 484:
 
трудно и хлопотно, однако на текущий момент это, наверное, единственный эффективный выход из ситуации. Кстати, для объектов с
 
трудно и хлопотно, однако на текущий момент это, наверное, единственный эффективный выход из ситуации. Кстати, для объектов с
 
классом памяти auto деструктор можно вызвать как обычную функцию.
 
классом памяти auto деструктор можно вызвать как обычную функцию.
 +
 +
[[Категория:Андрей Кузьменко]]

Текущая версия на 19:56, 3 июня 2015

Содержание

[править] Дырки в паутине

Утечка ресурсов — в первую очередь, памяти — одна из проблем современных сложных приложений. Андрей Кузьменко покажет, как избежать ее в ваших программах.

Можно смело считать 2007 год переломным моментом в переходе на многоядерные процессоры и активном внедрении многопоточного программирования в повседневную практику. Компании Intel и AMD обозначили производство многоядерных процессоров как основное направление своего развития на ближайшие несколько лет. Покупая современный ноутбук или ПК, вы обязательно увидите шильдик Core2Duo или Athlon X2. В текущем году производители чипов уже делают упор на четырехъядерные процессоры. Очевидно, что производители ПО должны адекватно реагировать на эти изменения, выпуская продукты, задействующие все преимущества новых технологий. А на чем они пишутся?


В языке C++, в отличие, например, от Java, отсутствует встроенная поддержка многопоточного программирования. Иными словами, с помощью «голого» C++ нельзя создавать соответствующие стандарту языка многопоточные приложения. Разумеется, данная функциональность все же доступна в виде библиотек, например, Pthreads (POSIX Threads), особенной популярной в мире Unix. Кроме нее, разработчик может использовать библиотеку Boost (http://www.boost.org) или ThreadWeaver (http://api.kde.org). Коммерческие Unix-системы, например, Sun Solaris, предлагают собственные библиотеки многопоточного программирования.

На данном уроке мы рассмотрим некоторые проблемы, возникающие при написании программ на C++ с использованием библиотеки Pthreads. Наряду со встроенными базовыми типами (int, char, double) в функциях работы с потоками могут использоваться объекты классов. Вот тут-то нас и подстерегают проблемы и неожиданности, которые мы сегодня обсудим. Кроме этого, мы оценим работу многопоточных программ, написанных на C++ и Pthreads, в различных Linux-системах.

[править] И снова классы…

Одной из серьезных проблем языка C++ (настолько серьезной, что иные компании создают целые платформы со сборкой мусора, лишь бы с нею не сталкиваться) является утечка ресурсов. Это может быть память, выделенная с помощью оператора new, файловые дескрипторы, сетевые сокеты, мьютексы. Для освобождения ресурсов, используемых объектом, в C++ предусмотрены специальные методы — деструкторы. Задача программиста — правильно написать деструктор и убедиться в том, что в программе происходит его вызов. Только так можно гарантировать, что ресурсы будут возвращены системе.

Во всех тестах, описываемых в этой статье, будет использоваться класс checker, объявленный в файле checker.hpp. Вот он:

class checker{
 private: string name;
 public:
 explicit checker(string s) ;
 ~checker( );
 void calc(long N);
 void say_hello(void);
};

Класс обладает «говорящими» конструктором и деструктором, метод calc() имитирует длительную по времени расчетную задачу, а функция say_hello() выводит сообщение на консоль.

[править] С вещами на выход!

Выполнение потоковой функции может быть прервано по трем причинам:

  1. В результате «естественного завершения» оператором return;
  2. В результате вызова функции pthread_exit();
  3. В результате аннулирования другим потоком.

Особый интерес для нас будет представлять вызов деструкторов объектов в случаях 2 и 3.

В документе The Open Group Base Specifications Issue 6 IEEE Std 1003.1, 2004 Edition в разделе, посвященном функции pthread_exit(), по этому поводу говорится следующее [здесь и далее перевод автора]: «Функция void pthread_exit(void *value_ptr) завершает вызывающий поток и делает значение value_ptr доступным для успешного присоединения к завершающему потоку. Любые обработчики отмены, помещенные в стек, но еще не извлеченные из него, будут извлечены в порядке, обратном по отношению к порядку помещения в стек, а после выполнены. Если потоку принадлежат данные, то после выполнения всех обработчиков отмены будут вызваны соответствующие функции деструкторов, при этом порядок их вызова не определен. При завершении потока ресурсы процесса, включая мьютексы и дескрипторы файлов, не освобождаются, и не выполняются никакие восстановительные действия уровня процесса, включая всевозможные вызовы любых функций atexit()». То есть, если объект в потоковой функции представляет собой локальную переменную с классом памяти auto, то после вызова pthread_exit() для него должен быть автоматически выполнен деструктор. Однако, как показывает практика, бывают случаи, когда этого не происходит.

Рассмотрим следующую программу:

void* task1(void *X) {
 std::cout<<" Start task_1!"<<std::endl;
 checker P("First"); P.calc(5);
 pthread_exit(NULL);
 return 0;
}
void* task2(void *X) {
 std::cout<<" Start task_2!"<<std::endl;
 checker Q("Second"); Q.calc(8);
 return 0;
}
int main(void){
 std::cout<<" Start test #1!"<<std::endl;
 pthread_t threadA, threadB;
 pthread_create(&threadA, NULL, task1, NULL);
 pthread_detach(threadA);
 pthread_create(&threadB, NULL, task2, NULL);
 pthread_detach(threadB);
 pthread_exit(NULL);
 return 0;
}

Здесь объявлены две потоковых функции: task1() и task2(). В качестве элемента данных в каждой из них используется объект класса checker, выделенный в стеке. Функция task1() завершается принудительно с помощью pthread_exit(), а task2() выходит «естественным образом» через return 0. При этом потоки создаются как открепленные, то есть при их уничтожении ресурсы, которые они использовали, сразу же возвращаются системе. Рассмотрим результат работы программы в различных дистрибутивах Linux. Например, в SLAX 6.0.3 вывод на консоль будет таким:

Start test #1!
Start task_1!
Constructor done! Name:First
Start task_2!
Constructor done! Name:Second
Destructor done! Name:First
Destructor done! Name:Second

А вот что получается в ALT Linux 3.0.4:

Start test #1!
Start task_1!
Constructor done! Name:First
Start task_2!
Constructor done! Name:Second
Destructor done! Name:Second

Видите? Деструктор объекта из потоковой функции task1() вызван не был! Конечно, 3.0.4 — не самая актуальная версия данного дистрибутива, но, как мы увидим далее, аналогичные проблемы имеют место и в более современных ОС.

[править] Поел — убери за собой

Очень часто при завершении потока бывает необходимо выполнить некоторые заключительные операции: освободить память, закрыть файлы, снять блокировки с разделяемых переменных и т.п. Желательно, чтобы эти действия выполнялись единообразно, как для стандартного завершения потоковой функции оператором return, так и при аннулировании другим потоком. Библиотека Pthreads предоставляет для этого возможность, называемую «стеком очистительно-восстановительных операций». Как она работает? С каждым потоком, имеющимся в программе, связывается стек очистительно-восстановительных операций, который содержит указатели на функции, вызываемые во время аннулирования (завершения) потока. Для работы с данным стеком используются две функции (или макроса):

  • pthread_cleanup_push() Принимает в качестве параметров указатель на помещаемую в стек функцию и передаваемый ей аргумент;
  • pthread_cleanup_pop() Принимает в качестве параметра целочисленное значение и извлекает завершающую функцию с вершины стека. Если аргумент отличен от нуля, завершающая функция выполняется.

Давайте рассмотрим еще один пример. Здесь мы определяем потоковую функцию, которая в зависимости от значения параметра, заданного пользователем, завершается либо «обычным образом», либо вызовом pthread_exit():

void* task1(void *X){
 std::cout<<" Start test thread!"<<std::endl;
 checker *Z = new checker("agent");
 pthread_cleanup_push(del_ptr_checker, Z);
 int *counter = static_cast<int*>(X);
 for(int i=0; i<(*counter); ++i)
 {
  if(i==1000) pthread_exit(NULL);
 }
 pthread_cleanup_pop(1);
 std::cout<<" Thread go boom!"<<std::endl;
 return 0;
}

Обратите внимание, что в качестве элемента данных теперь используется экземпляр класса checker, расположенный в динамической памяти. Для ее освобождения была написана функция del_ptr_checker(), указатель на которую помещается в стек очистительно-восстановительных операций. Мы вызываем деструктор объекта при помощи pthread_cleanup_pop(1) — кажется, это должно гарантировать выполнение очистительных действий вне зависимости от способа завершения потока. Функция del_ptr_checker() сама по себе довольно проста:

void del_ptr_checker(void *X){
checker *del = static_cast<checker*>(X);
std::cout<<" #-> START del_ptr_checker!"<<std::endl;
delete del;
del = 0;
std::cout<<" #-> END del_ptr_checker!"<<std::endl;
}

Мы приводим переданный указатель к типу checker * и вызываем оператор delete для освобождения памяти. del_ptr_checker() определена в файле helper.hpp. Функция main() для нашего примера выглядит так:

int main(void)
{
std::cout<<" START TEST #2!"<<std::endl;
int N=0;
std::cout<<" Enter N:"; std::cin>>N;
pthread_t threadA;
pthread_create(&threadA, NULL, task1, &N);
pthread_detach(threadA);
std::cout<<" End MAIN"<<std::endl;
return 0;
}

Приведу результаты выполнения двух тестов в SLAX 6.0.3:

START TEST #2!
Enter N:222
Start test thread!
Constructor done! Name:agent
#-> START del_ptr_checker!
Destructor done! Name:agent
#-> END del_ptr_checker!
Thread go boom!
End MAIN
START TEST #2!
Enter N:4589
Start test thread!
Constructor done! Name:agent
#-> START del_ptr_checker!
Destructor done! Name:agent
#-> END del_ptr_checker!
End MAIN

Как мы видим, функция del_ptr_checker() вызывается независимо от значения параметра N, задаваемого пользователем, и динамическая память, занимаемая объектом класса checker, всегда корректно освобождается. А вот что происходит в OpenSUSE 10.1:

linux@linux:~/super> ./test_2
Enter N:654
End MAIN
linux@linux:~/super> ./test_2
Enter N:8888
End MAIN
linux@linux:~/super>

Любопытно, но судя по выводу на консоль, поток, использующий динамическую переменную класса checker, даже не создается, не говоря уже о вызове деструктора для объекта. Очень интересный результат! Кстати, аналогичное поведение наблюдается и в Mpentoo Linux 2006.1.

[править] Я тебя породил…

Бывают ситуации, когда одному потоку нужно завершить другой: это может делаться при организации управления программой или для экономии ограниченных ресурсов. В библиотеке Pthreads для этих целей предназначена функция pthread_cancel(). В качестве параметра она принимает идентификатор потоковой функции, которую надо завершить, и возвращает 0 в случае успешного выполнения. В уже упоминавшемся документе The Open Group Base Specifications Issue 6 IEEE Std 1003.1, 2004 Edition в разделе, описывающем функцию pthread_cancel(), говорится: «Функция pthread_cancel() создает запрос на отмену потока. Когда он будет реализован, зависит от текущего состояния потока и его типа. При отмене потока должны быть вызваны обработчики, которые выполнят связанные с отменой подготовительные действия. По завершению последнего обработчика должны быть вызваны деструкторы данных, используемых потоком».

Посмотрим, что же действительно происходит в данной ситуации, с помощью следующей простой программы:

void *task1(void *X){
 std::cout<<" Start thread!"<<std::endl;
 checker Q("spy");
 checker *Z = new checker("agent");
 pthread_cleanup_push(del_ptr_checker, Z);
 pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
 std::cout<<" @-> Hello, Threads!"<<std::endl;
 for(int i=0; i<5; ++i)
 {
  Z->say_hello( );
  sleep(1);
 }
 pthread_testcancel();
 pthread_cleanup_pop(1);
 std::cout<<" @-> Logical End of THREAD function"<<std::endl;
}
int main(int argc, char * argv[]){
 cout<<" Start test #3!"<<std::endl;
 int ret;
 pthread_t thread;
 pthread_create(&thread, NULL, task1, NULL);
 ret = pthread_cancel(thread);
 if(ret==0)
 {
  std::cout<<" $$$ Thread CANCEL OK!"<<std::endl;
 }
 pthread_join(thread, NULL);
 std::cout<<" $$$ The thread go boom!"<<std::endl;
 return 0;
}

В качестве элемента данных потоковая функция task1() использует два объекта класса checker: один расположен в динамической памяти, второй имеет класс auto. Для уничтожения первого экземпляра мы опять используем стек очистительно-восстановительных операций. Вызов pthread_setcancelstate() разрешает аннулирование нашего потока другим. Функция pthread_testcancel() проверяет наличие необработанных запросов на уничтожение. Если они есть, процесс аннулирования активизируется в точке вызова pthread_testcancel(). Обратите внимание, что в потоковой функции не используются разделяемые переменные и не происходит вызова системных функций (кроме вывода сообщений на консоль), что обеспечивает ее безопасное прекращение.

Результат выполнения тестовой программы в SLAX 6.0.3 таков:

Start test #3!
Start thread!
Constructor done! Name:spy
Constructor done! Name:agent
@-> Hello, Threads!
Hello from cheker!
#-> START del_ptr_checker!
Destructor done! Name:agent
#-> END del_ptr_checker!
Destructor done! Name:spy
$$$ Thread CANCEL OK!
$$$ The thread go boom!

А вот вывод в системе Mpentoo 2006.1:

Start test #3!
$$$ Thread CANCEL OK!
Start thread!
$$$ The thread go boom!

Мы видим, что работа потоковой функции, судя по выводу на консоль, завершилась раньше ее начала. При этом объекты класса checker в потоковой функции не создаются.

Давайте проанализируем полученные результаты: для удобства они сведены в таблицу. Ситуации, отмеченные знаком ?, очевидно, нуждаются в комментариях.

  • Тест № 1. При запуске программы в OpenSUSE 10.1 наблюдается порядок вызова деструкторов, отличный от всех других систем, получивших +. Однако все деструкторы вызываются, и утечки памяти не происходит.
  • Тест № 2. В двух системах, отмеченных ?, наблюдалась неустойчивая работа теста. Она проявлялась в том, что в одном случае результат теста был «правильным», и вывод на консоль полностью соответствовал ожидаемому, а в другом случае наблюдалась проблема с запуском потоковой функции.
Сводная таблица результатов тестирования
Операционная система Номер теста
1 2 3
1
Mandriva 2008 KDE LiveCD
Ядро: 2.6.22
GCC: 4.2.2*
Glibc: 2.6.1
Libstdc++: 5.0.7 / 6.0.9
+ ? +
2
Fedora 8 LiveCD
Ядро: 2.6.23
GCC: 4.1.2*
Glibc: 2.7
Libstdc++: 6.0.8
+ X +
3
ASP Linux 11 LiveCD
Ядро: 2.6.14
GCC: 4.0.2
Glibc: 2.3.5
Libstdc++: 5.0.7 / 6.0.7
+ X X
4
Knoppix 5.3.1 LiveDVD
Ядро: 2.6.24
GCC: 4.2.3
Glibc: 2.7
Libstdc++: 5.0.7 / 6.0.10
+ + +
5
SLAX 6.0.3 LiveCD
Ядро: 2.6.24
GCC: 4.2.3
Glibc: 2.7
Libstdc++: 6.0.8 / 6.0.9
+ + +
6
Ubuntu 8.0.4 LiveDVD
Ядро: 2.6.24
GCC: 4.2.3
Glibc: 2.7
Libstdc++: 6.0.9
+ + +
7
Ubuntu 7.10 LiveCD
Ядро: 2.6.22
GCC: 4.1.3
Glibc: 2.6.1
Libstdc++: 6.0.9
+  ? X
8
ALT Linux 3.0.4 LiveCD
Ядро: 2.6.12
GCC: 3.4.4*
Glibc: 2.3.5
Libstdc++: 5.0.7 / 6.0.3
X X X
9
ALT Linux 4.0.3
Ядро: 2.6.18
GCC: 4.1.1
Glibc: 2.5
Libstdc++: 5.0.7 / 6.0.8
+ X X
10
MPentoo 2006.1 LiveCD
Ядро: 2.6.16
GCC: 3.3.6
Glibc: 2.3.6
Libstdc++: 5.0.7
X X X
11
Puppy Linux (rus_100) LiveCD
Ядро: 2.6.21
GCC: 4.1.2*
Glibc: 2.5
Libstdc++: 5.0.6 / 6.0.8
+ X X
12
Gentoo 2008 Beta2 LiveCD
Ядро: 2.6.24
GCC: 4.1.2
Glibc: 2.6.1
Libstdc++: 6.0.8
+ + +
13
OpenSUSE 10.1 LiveDVD
Ядро: 2.6.16
GCC: 4.1.0*
Glibc: 2.4
Libstdc++: 5.0.7 / 6.0.8
 ? X X
Обозначения:
X — тест выполнен с ошибками, результат не соответствует ожиданиям
 ? — неожиданный результат выполнения теста, интересен для анализа
+ — тест успешно выполнен, результат адекватен ожиданиями
* — на диске компилятор отсутствует, проверка на бинарной сборке из SLAX

[править] Что в итоге?

Какие же выводы можно сделать на основании результатов, отраженных в таблице? Во-первых, при разработке многопоточных приложений с использованием библиотеки Pthreads программист должен уделять повышенное внимание тем фрагментам кода, где используются объекты классов. Несмотря на то, что Pthreads имеет средство автоматического освобождения ресурсов — стек очистительно-восстановительных операций, гарантировать обязательность и правильность его использования нельзя. В описании многих функций библиотеки сообщается, что при завершении потока сначала вызываются процедуры из очистительного стека, а потом деструкторы потоковых данных, а на самом деле это не всегда так.

Во-вторых, разработчики Linux-систем вполне осведомлены об особенностях поведения функций библиотеки Pthreads при использовании в качестве данных объектов классов. Ведется активная работа в этом направлении, и положительные результаты есть! Показателен пример Ubuntu.

В-третьих, анализ характеристик дистрибутивов, получивших + по всем тестам, обнаруживает, что версия ядра (в этих тестовых образцах) не ниже 2.6.24 (это обязательное условие), библиотека glibc — не ниже 2.6.1 (лучше 2.7), библиотека libstdc++ — не ниже 6.0.8 (лучше 6.0.9), версия компилятора GCC — не ниже 4.1.2. Соответственно, дистрибутивы, получившие за тест -, имеют другие версии библиотек. Например, в ALT Linux 4.0.3 используется библиотека glibc версии 2.5. Кстати, очень интересно сравнить результаты Gentoo 2008 Beta 2 и Fedora Core 8. Исход их «спора» решила версия ядра. У Gentoo она выше, хотя у Fedora библиотека glibc новее. Это говорит о том, что своевременное обновление ядра и ключевых системных библиотек позволяет повысить надежность работы операционной системы.

Читателя наверняка интересует вопрос: а что будет, если программу, скомпилированную в «правильной» системе, например, Knoppix 5.3.1, попробовать запустить в «неправильной», скажем, Knoppix 3.2 RE? Здесь возможны два варианта:

  1. Программа не запустится из-за отсутствия необходимых библиотек и выдаст сообщение следующего содержания:
    knoppix@ttyp0[knoppix]$ ./etalon
    ./etalon: error while loading shared libraries: libstdc++.so.6: cannot open shared object file: No such file or directory
    knoppix@ttyp0[knoppix]$
  2. Однако, даже если «правильная» программа запустится в «неправильной» среде, вести себя она будет «неправильно».

[править] И что делать?

Что можно посоветовать, чтобы свести к минимуму издержки, связанные с особенностями взаимодействия библиотеки Pthreads с объектами классов?

Что касается теста № 1, то тут может помочь метафора «песочницы»: прием, при котором вся работа с объектами классов, имеющих тип памяти auto (не динамические, а «обычные» переменные), ведется в пределах блока, выделенного в тексте программы фигурными скобками {…}. При выходе из блока происходит автоматический вызов деструкторов, после чего можно «запускать» pthread_exit().

Однако возникает вопрос: что делать, если pthread_exit() вызывается в результате выполнения некоторого условия при работе программы, как, например, в тесте № 2, или происходит аннулирование потока, как в тесте № 3? Здесь относительно универсальным, хотя и достаточно трудозатратным будет такой выход, как «ручное» управление памятью посредством операторов new и delete. Да, это трудно и хлопотно, однако на текущий момент это, наверное, единственный эффективный выход из ситуации. Кстати, для объектов с классом памяти auto деструктор можно вызвать как обычную функцию.

Персональные инструменты
купить
подписаться
Яндекс.Метрика