Linux Format Август 2008



Скачать 220.86 Kb.

Дата06.12.2016
Размер220.86 Kb.
Просмотров102
Скачиваний0
ТипУчебник

88
Linux Format Август 2008
Учебник
Pthreads и C++
Дырки

Исходный код тестовых программ
На DVD
Наш эксперт
Андрей
Кузьменко
Убежденный сторонник надежного ПО и любитель C++. Из всех дистрибутивов
Linux отдает предпочтение
Knoppix.
М
ожно смело считать 2007 год переломным моментом в переходе на многоядерные процессоры и активном внедрении многопоточного программирования в повсе- дневную практику. Компании Intel и AMD обозначили производство многоядерных процессоров как основное направление своего раз- вития на ближайшие несколько лет. Покупая современный ноутбук или ПК, вы обязательно увидите шильдик Core2Duo или Athlon X2. В текущем году производители чипов уже делают упор на четырехъя- дерные процессоры. Очевидно, что производители ПО должны адек- ватно реагировать на эти изменения, выпуская продукты, задейству- ющие все преимущества новых технологий. А на чем они пишутся?
В языке C++, в отличие, например, от Java, отсутствует встро- енная поддержка многопоточного программирования. Иными сло- вами, с помощью «голого» C++ нельзя создавать соответствующие стандарту языка многопоточные приложения. Разумеется, данная функциональность все же доступна в виде библиотек, например,
Pthreads (POSIX Threads), особенной популярной в мире Unix. Кроме нее, разработчик может использовать библиотеку Boost (
www.boost.
org
) или ThreadWeaver (
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!"< checker P("First"); P.calc(5);
pthread_exit(NULL);
return 0;
}
void* task2(void *X) {
std::cout<<" Start task_2!"< checker Q("Second"); Q.calc(8);
Долой утечки! Пишем надежные многопоточные приложения на C++
Утечка ресурсов

– в первую очередь, памяти – одна из проблем современных сложных приложений. Андрей Кузьменко покажет, как избежать ее в ваших программах.
Если вы хотите узнать больше о программирова- нии с использо- ванием Pthreads, обратитесь к нашим учебникам, опубликованным в LXF86-87/88.
Мы про это уже писали

Август 2008 Linux Format
89
Pthreads и C++
Учебник в паутине return 0;
}
int main(void){
std::cout<<" Start test #1!"< 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!"< checker *Z = new checker("agent");
pthread_cleanup_push(del_ptr_checker, Z);
int *counter = static_cast(X);
for(int i=0; i<(*counter); ++i)
{
if(i==1000) pthread_exit(NULL);
}
pthread_cleanup_pop(1);
std::cout<<" Thread go boom!"< return 0;
}
Обратите внимание, что в качестве элемента данных теперь используется экземпляр класса checker
, расположенный в дина- мической памяти. Для ее освобождения была написана функ- ция del_ptr_checker()
, указатель на которую помещается в стек очистительно-восстановительных операций. Мы вызываем деструк- тор объекта при помощи pthread_cleanup_pop(1)
– кажется, это должно гарантировать выполнение очистительных действий вне зависимости от способа завершения потока.
Функция del_ptr_checker()
сама по себе довольно проста:
void del_ptr_checker(void *X){
checker *del = static_cast(X);
std::cout<<" #-> START del_ptr_checker!"< delete del;
del = 0; std::cout<<" #-> END del_ptr_checker!"< }
Мы приводим переданный указатель к типу checker *
и вызы- ваем оператор delete
для освобождения памяти. del_ptr_checker() определена в файле helper.hpp. Функция main()
для нашего примера выглядит так:
int main(void)
{
std::cout<<" START TEST #2!"< 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"< return 0;
}

90
Linux Format Август 2008
Учебник
Pthreads и C++ pthread_testcancel();
pthread_cleanup_pop(1);
std::cout<<" @-> Logical End of THREAD function"< }
int main(int argc, char * argv[]){
cout<<" Start test #3!"< int ret;
pthread_t thread;
pthread_create(&thread, NULL, task1, NULL);
ret = pthread_cancel(thread);
if(ret==0)
{
std::cout<<" $$$ Thread CANCEL OK!"< }
pthread_join(thread, NULL);
std::cout<<" $$$ The thread go boom!"< 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. В двух системах, отмеченных
?
, наблюдалась неустой- чивая работа теста. Она проявлялась в том, что в одном случае результат теста был «правильным», и вывод на консоль полностью соответствовал ожидаемому, а в другом случае наблюдалась про- блема с запуском потоковой функции.
Что в итоге?
Какие же выводы можно сделать на основании результатов, отра- женных в таблице? Во-первых, при разработке многопоточных
Приведу результаты выполнения двух тестов в 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!"< 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!"< for(int i=0; i<5; ++i)
{
Z->say_hello( );
sleep(1);
}

Август 2008 Linux Format
91
Pthreads и C++
Учебник приложений с использованием библиотеки 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 деструктор можно вызвать как обычную функ- цию.
LXF
Скорая помощь
Узнать параме- тры своей системы можно, набрав в консо- ли следующие команды:
Версия ядра: uname -a
Компилятор
GCC:
gcc --version
Библиотека
glibc :
getconf GNU_
LIBC_VERSION
Библиотека
libstdc++:
ls -l /usr/lib/
libstdc++.so.*
Сводная таблица результатов тестирования

Операционная система
Номер теста
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


Поделитесь с Вашими друзьями:


База данных защищена авторским правом ©nethash.ru 2017
обратиться к администрации

войти | регистрация
    Главная страница


загрузить материал