355 500 произведений, 25 200 авторов.

Электронная библиотека книг » Олег Цилюрик » QNX/UNIX: Анатомия параллелизма » Текст книги (страница 7)
QNX/UNIX: Анатомия параллелизма
  • Текст добавлен: 8 октября 2016, 23:59

Текст книги "QNX/UNIX: Анатомия параллелизма"


Автор книги: Олег Цилюрик


Соавторы: Владимир Зайцев,Егор Горошко
сообщить о нарушении

Текущая страница: 7 (всего у книги 23 страниц) [доступный отрывок для чтения: 9 страниц]

Спорадическая диспетчеризация

Системы реального времени принципиально отличаются от систем общего назначения тем, что для таких систем важна не только корректность выполнения возложенных на них функций, но и время, за которое эти функции реализуются. Можно даже сказать, что для задач реального времени опоздание с выполнением практически эквивалентно невыполнению задачи: требуемая реакция или управляющее воздействие не поступили в срок. Предельный срок, в который задача реального времени должна быть выполнена, называют критическим сроком обслуживания(deadline).

Если система реального времени реализуется как многопоточная система (а в настоящее время такой вариант рассматривается фактически как стандартный), то при ее разработке зачастую возникает проблема определения того, действительно ли все задачи реального времени, конкурирующие в системе за вычислительный ресурс, успевают исполниться в их критический срок обслуживания.

Примечание

Здесь мы следуем «классической» модели обсуждения из области систем реального времени, хотя уместнее было бы акцентировать внимание не на абсолютной минимизации времени приложения, а именно на том, что приложение обязано «уложиться» в некоторый критический интервал времени (см. выше). Величина же того, насколько быстро приложение выполнит свои критические функции (если оно укладывается в критический интервал) по принципу «меньше – больше», практически уже не имеет никакого значения. Из этого не совсем четкого толкования сложился общий стереотип, состоящий в том, что системы реального времени (в частности, операционные системы реального времени) принято считать «быстрыми» (в том смысле, что они потенциально могут исполнять аналогичные функции быстрее, чем системы общего назначения). Этот взгляд в корне ошибочен: системы реального времени в общем случае, скорее, будут даже «медленнее», чем системы общего назначения, за счет более тщательной отработки операций, например диспетчеризации и переключений контекстов. Во многих случаях можно ожидать, что при многократном выполнении участка кода средняя величина времени его выполнения в ОС общего назначения будет ниже, но вот дисперсия этой средней величины будет намного ниже в системах реального времени.

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

Одна из наиболее известных систем математического анализа временных характеристик систем реального времени с периодическим поступлением запросов на выполнение задач называется «Частотно-монотонный анализ» (ЧМА – Rate Monotonic Analyzing) [13]. Свое название эта система получила от ее основного принципа: « Чем короче период поступления (выше частота) задачи, тем выше ее приоритет». Как уже говорилось, ЧМА предназначен для анализа систем реального времени, в которых каждая задача реального времени обрабатывается со своим периодом, причем еще одним ограничением ЧМА является условие, что период поступления задачи является также и ее критическим сроком обслуживания. В настоящее время появился ряд новых методов анализа характеристик систем реального времени для случаев критических сроков обслуживания, больших или меньших периода поступления, но здесь мы не будем на них останавливаться.

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

Планирование обслуживания таких задач можно свести к планированию периодических задач и, таким образом, провести для них анализ диспетчеризуемости. Для этого теория ЧМА предлагает введение дополнительной периодической задачи (называемой спорадический сервер), которая проводит обслуживание непериодических (спорадических) задач.

Алгоритм работы такого сервера [13] следующий:

•  Шаг 1. Если спорадический запрос прибывает и сервер не может его обработать, потому что уже занят или не имеет свободного ресурса вычислений, запрос будет поставлен в очередь обработки.

•  Шаг 2. Если получен спорадический запрос и сервер может его обработать, он делает следующее:

•  Шаг 2а. Выполняется до служебного завершения или истощения ресурса вычисления.

•  Шаг 2с. Уменьшает текущий ресурс вычисления на используемое количество и на столько же увеличивает его ресурс вычисления в точке пополнения.

Для реализации теоретически обобщенной модели спорадического сервера в качестве механизма, реализующего эту модель, в QNX 6.2.1 была введена специализированная дисциплина диспетчеризации – спорадическая.

Сутью спорадической диспетчеризации в QNX является установка для соответствующего потока двух значений приоритета: основного (normal) и фонового (foreground). В момент запуска потока, подчиняющегося спорадической диспетчеризации (момент времени 0), поток имеет запас времени (С), называемый начальным бюджетом(initial budget) потока, в течение которого поток выполняется со своим основным приоритетом (N). Когда же запас времени исчерпывается, его приоритет понижается до уровня фонового (L). Через некоторый период времени T происходит пополнение(replenishment) запаса времени потока до значения начального бюджета, и он снова может выполняться с основным приоритетом.

Рассмотрим порядок выполнения такого потока подробнее. В начальный момент времени после запуска поток имеет приоритет N и время С для выполнения с этим приоритетом. Если поток блокируется на время R, то запас времени все равно расходуется и пополнение этого запаса может произойти только через период T после начала выполнения потока. Если же поток вытесняется более приоритетным, то расход его запаса времени прекращается. Когда управление возвращается к потоку, он вновь начинает тратить оставшееся количество времени на основном приоритете. Однако с момента повторного начала выполнения потока начинается отсчет нового периода до момента пополнения.

На рис. 2.6 проиллюстрирована работа спорадического потока. После запуска (момент времени 0) поток переходит в блокированное состояние на время R (10 мс), но его бюджет все равно расходуется. Поток становится активным, но через 3 мс (13 мс от начала выполнения) вытесняется более приоритетным потоком. Факт вытеснения означает, что через период пополнения T (40 мс) бюджет потока будет пополнен на израсходованную величину (13 мс). Еще через 3 мс более приоритетный поток заканчивает свою работу и управление возвращается назад. От начального бюджета потока С (20 мс) осталось еще 7 мс, и поток выполняется это время с основным приоритетом. При этом от повторного начала его выполнения (16 мс) отсчитывается новый период пополнения, то есть через 56 мс бюджет потока будет пополнен на 7 мс. После полного исчерпания бюджета приоритет потока понижается до фонового (L) и поток может вытесняться или нет в зависимости от приоритетов остальных потоков в системе. После наступления очередного времени пополнения бюджет потока восстанавливается на израсходованную в этом периоде величину и т.д.

Рис. 2.6. Периодическое выполнение спорадической задачи

Если поток много раз вытесняется в период своей работы с основным приоритетом, то его выполнение может превратиться в многократное колебание с высокой частотой между основным и фоновым приоритетами. Поэтому в QNX 6.2.1 в параметрах для спорадической диспетчеризации можно установить (ограничить) максимальное количество пополнений бюджета за период.

Как уже описывалось выше, структура shed_paramсодержит в своем составе, в частности, еще и структуру параметров для спорадической диспетчеризации (при других типах диспетчеризации эта часть не используется):

struct {

 _INT32 __ss_low_priority;

 _INT32 __ss_max_repl;

 struct timespec __ss_repl_period;

 struct timespec __ss_init_budget;

} __ss;

где low_priority– фоновый приоритет; max_repl– максимальное количество пополнений бюджета за период; repl_period– период пополнения бюджета и init_budget – начальный бюджет.

Соображения производительности

Выполним «симметричный» тест аналогично тому, как это делалось для переключения контекстов процессов (стр. 44), но теперь применительно к потокам ( файл p5t.cc). При этом мы постараемся максимально сохранить принципы функционирования, имевшие место в приложении «Затраты на взаимное переключение процессов» ( файл p5.сс) (естественно, из-за принципиального различия механизмов тексты кодов будут существенно отличаться).

Затраты на взаимное переключение потоков

#include

#include

#include

#include

#include

#include

#include

#include

unsigned long N = 1000;

// потоковая функция:

void* threadfunc(void* data) {

 uint64_t t = ClockCycles();

 for (unsigned long i = 0; i < N; i++) sched_yield();

 t = ClockCycles() – t;

 // дать спокойно завершиться 2-му потоку до начала вывода

 delay(100);

 cout << pthread_self() << "t: cycles – " << t

  << ", on sched – " << (t / N) / 2 << endl;

  return NULL;

}

int main(int argc, char* argv[]) {

 int opt, val;

 while ((opt = getopt(argc, argv, «n:»)) != -1) {

  switch(opt) {

  case 'n': // переопределения числа переключений

   if (sscanf(optarg, «%i», &val) != 1)

    cout << «parse command line error» << endl, exit(EXIT_FAILURE);

   if (val > 0) N = val;

   break;

  default:

   exit(EXIT_FAILURE);

  }

 }

 const int T = 2;

 pthread_t tid[T];

 // создать взаимодействующие потоки

 for (int i = 0; i < T; i++)

  if (pthread_create(tid + i, NULL, threadfunc, NULL) != EOK)

   cout << «thread create error», exit(EXIT_FAILURE);

 // и дожидаться их завершения ...

 for (int i = 0; i < T; i++)

  pthread_join(tid[i], NULL);

 exit(EXIT_SUCCESS);

}

Результаты выполнения программы:

# nice -n-19 p5t -n100

2       : cycles – 79490; on sched – 397

3       : cycles – 78350; on sched – 391

# nice -n-19 p5t -n1000

2       : cycles – 753269; on sched – 376

3       : cycles – 752069; on sched – 376

# nice -n-19 p5t -n10000

2       : cycles – 7494255; on sched – 374

3       : cycles – 7493225; on sched – 374

# nice -n-19 p5t -n100000

2       : cycles – 74897795; on sched – 374

3       : cycles – 74895800; on sched – 374

# nice -n-19 p5t -n1000000

2       : cycles – 748850811, on sched – 374

3       : cycles – 748850432; on sched – 374

Как и в случае с процессами, результаты отличаются очень высокой устойчивостью при изменении «объема вычислений» на 4 порядка, однако по своим величинам значения для потоков почти в 2 раза меньше, чем для процессов (стр. 45).

Завершение потока

Как и в случае обсуждавшегося ранее завершения процесса, для потоков мы будем отчетливо различать случаи:

• «естественного» завершения выполнения потока из кода самого потока;

• завершения потока извне, из кода другого потока или по сигналу. Для этого действия, в отличие от «естественного» завершения, будем использовать другой термин – отмена.

Завершение потока происходит при достижении функцией потока своего естественного конца и выполнения оператора return(явно или неявно) или выполнения потоком вызова:

void pthread_exit(void* value_ptr)

где value_ptr– указатель на результат выполнения потока.

При выполнении pthread_exit()поток завершается. Если этот поток принадлежит к категории ожидаемых, он может возвратить результат своей работы другому потоку, ожидающему его завершения на вызове pthread_join()(только один поток может получить результат завершения). Если же этот поток отсоединенный, то по его завершении все системные ресурсы, задействованные потоком, освобождаются немедленно.

Перед завершением потока будут выполнены все завершающие процедуры, помещенные в стек завершения, а также деструкторы собственных данных потока, о которых мы говорили ранее. Для последнего потока процесса вызов pthread_exit()эквивалентен exit().

Возврат результата потока

Выше отмечено, что вызов pthread_exit(), завершающий ожидаемый поток, может передать результат выполнения потока. То же действие может быть выполнено и оператором returnпотоковой функции, которая из прототипа ее определения должна возвращать значение типа void*.

В обоих случаях результат может иметь сколь угодно сложный структурированный тип; никакая типизация результата не предусматривается (тип void*). Важно, чтобы код, ожидающий результата на вызове pthread_join(), понимал его так же, как и функция потока, возвращающая этот результат.

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

void* threadfunc(void* data) {

 int res; // результат некоторых вычислений

 res = ...

 pthread_exit(&res);

}

А вот один из многих допустимых вариантов:

void* threadfunc(void* data) {

 struct data *res = new struct; // результат некоторых вычислений

 ...

 *res = ...

 pthread_exit(res);

}

...

pthread_t tid;

pthread_create(&tid, NULL, threadfunc, NULL);

struct data *res;

pthread_join(tid, &res);

...

delete res;

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

Уничтожение (отмена) потока

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

Если для процесса в перечень «опасных» (с точки зрения завершения) объектов включаются только объекты со временем жизни выше уровня процесса (их число достаточно ограничено), то для потока в число таких объектов включаются уже все объекты со временем жизни процесса (process-persistent). Завершающийся (покидающий процесс) поток обязан оставить все объекты процесса в состоянии, пригодном для их дальнейшего использования другими потоками процесса.

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

Для отмены (принудительного завершения) потока используется вызов:

int pthread_cancel(pthread_t thread);

где в качестве параметра thread указывается TID отменяемого потока. Однако этот вызов не отменяет поток, а только запрашивает завершение потока. В зависимости от статуса отмены, который мы сейчас рассмотрим, поток может перейти (или нет) к действию завершения, которое состоит в том, что:

• выполняются все процедуры завершения, занесенные ранее в стек завершения вызовами pthread_cleanup_push();

• выполняются деструкторы собственных данных потока;

• отменяемый поток завершается;

• процесс отмены – асинхронный с точки зрения вызывающего pthread_cancel()кода, поэтому вызывающий отмену поток должен дождаться завершения потока на вызове pthread_join().

Прежде всего, поток может вообще отказаться выполнять любые отмены, вызвав из своей функции потока:

int pthread_setcancelstate(int state, int* oldstate);

где stateи oldstate– устанавливаемое и установленное ранее (возвращаемое вызовом) состояния отмены потока, которые могут принимать значения PTHREAD_CANCEL_DISABLEлибо PTHREAD_CANCEL_ENABLE. (Естественно, как и во многих функциях с подобным прототипом, значением oldstateможет быть NULL, и тогда нам не нужно возвращать ранее установленное состояние.)

Далее, даже если для потока установлено состояниезавершаемости (также называемое «состоянием отмены») PTHREAD_CANCEL_ENABLE(это значение по умолчанию при создании потока), поток может переопределить еще и типотмены, вызвав:

int pthread_setcanceltype(int type, int* oldtype);

где typeи oldtype– как и в предыдущем случае, новое и ранее установленное значения типа отмены потока, которые могут принимать значения PTHREAD_CANCEL_ASYNCHRONOUS(асинхронный по отмене поток) либо PTHREAD_CANCEL_DEFERRED(синхронный по отмене поток). Значением по умолчанию, устанавливаемым при создании потока, является PTHREAD_CANCEL_DEFERRED, хотя предписываемым POSIX умолчанием является PTHREAD_CANCEL_ASYNCHRONOUS.

Обе рассмотренные функции установок [23]23
  Согласно стандарту POSIX установки состояния и типа завершаемости могут быть сделаны только из уже выполняющегося кода потока (при старте потока эти параметры установлены в значения по умолчанию). QNX делает расширение, позволяя установить соответствующие флаги в атрибутной записи еще до создания потока. Подробнее об этом говорилось при обсуждении создания потока.


[Закрыть]
параметров отмены при успешном выполнении возвращают значение EOK.

Итак, действия потока на запрос его завершения будут определяться текущей комбинацией двух установленных для него параметров: состоянием и типом отмены.

Теперь о том, чем же отличается отмена асинхронно и синхронно завершаемых потоков. Поток с асинхронным типом отмены (установленный с PTHREAD_CANCEL_ASYNCHRONOUS) может быть отменен в любой произвольный момент времени, то есть он всегда «свободен» для отмены и отмена производится немедленно. Поток с синхронным типом отмены (установленный с PTHREAD_CANCEL_DEFERRED) может быть остановлен только в тех точках выполнения потока, когда ему «удобно», и соответствующие места в программе называются точками отмены. При поступлении запроса на отмену такого потока (после выполнения извне pthread_cancel()) запрос помещается в очередь, а процесс отмены активизируется только после того, как отменяемый поток в ходе своего выполнения достигнет очередной точки отмены. Как определяются (создаются) точки отмены в коде потока? Для этого служит функция:

void pthread_testcancel(void);

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

Примечание

( Очень важно!) Достаточно много библиотечных функций могут сами устанавливать точки отмены. Более того, такие функции могут косвенно вызываться из других функций в программе и тем самым неявно устанавливать точки отмены. Информацию о таких функциях следует искать в справочной man-странице по функции pthread_testcancel(). В результате этого эффекта можно получить отмену потока не в той точке, которую вы считаете безопасной и которую явно отмечаете вызовом pthread_testcancel(), а ранее этой точки – когда будет вызвана одна из таких функций. А это, очевидно, вовсе не то, на что вы рассчитывали!

Если состояние отмены потока, как это описывалось ранее, установлено в PTHREAD_CANCEL_DISABLE, то никакая расстановка точек отмены не имеет эффекта и поток остается неотменяемым.

Покажем, как могут быть использованы все эти предосторожности в коде функции потока, чтобы сделать код безопасным с позиции возможной асинхронной отмены потока извне:

void* function(void* data) {

 int state;

 pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &state);

 // ... здесь выполняется инициализация ...

 pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);

 pthread_setcancelstate(&state, NULL);

 while (true) {

  struct blockdata *blk = new blockdata;

  // ... обработка блока данных blk ...

  delete blk;

  pthread_testcancel();

 }

}

...

pthread_t tid;

...

pthread_create(&tid, NULL, function, NULL);

...

pthread_cancel(tid); // отмена потока

void* res;

pthread_join(tid, &res); // ожидание отмены

if (res != PTHREAD_CANCELED)

cout << «Что-то не так!» << endl;

Наконец, в QNX (но не в POSIX) существует вызов, подобный pthread_cancel(), принудительно отменяющий поток независимо от его установок («желания»):

int pthread_abort(pthread_t thread);

В отличие от pthread_cancel(), этот вызов принудительно и немедленно отменяет поток. Кроме того, никакие процедуры завершения и деструкторы собственных данных потока не выполняются. Очевидно, что в результате такого «завершения» состояния объектов процесса будут просто неопределенными, поэтому такой вызов крайне опасен. При таком способе отмены в программный код, ожидающий завершения на pthread_join(), в качестве результата завершения возвращается константа (тип void*) PTHREAD_ABORTED(аналогично возвращается константа PTHREAD_CANCELEDпри выполнении pthread_cancel()).

Но и этих мер безопасности недостаточно на все случаи жизни, поэтому механизм потоков предусматривает еще один уровень (механизм) страховки.


    Ваша оценка произведения:

Популярные книги за неделю