Иллюстрированный самоучитель по теории операционных систем

Кооперативная многозадачность

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

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


По-видимому, самой простой реализацией многозадачной системы была бы библиотека подпрограмм, которая определяет следующие процедуры.

  • struct Thread; В тексте будет обсуждаться, что должна представлять собой эта структура, называемая дескриптором нити.
  • Thread * ThreadCreate(void (*ThreadBody)(void)); Создать нить, исполняющую функцию ThreadBody.
  • void Threadswitch(); Эта функция приостанавливает текущую нить и активизирует очередную, готовую к исполнению.
  • void ThreadExit (); Прекращает исполнение текущей нити.

Сейчас мы не обсуждаем методов синхронизации нитей и взаимодействия между ними (для синхронизации были бы полезны также функции void DeactivateThread(); и void ActivateThread(struct Thread *);). Нас интересует только вопрос: что же мы должны сделать, чтобы переключить нити?

Функция ThreadSwitch называется диспетчером или планировщиком (scheduler) и ведет себя следующим образом.

  • Она передает управление на следующую активную нить.
  • Текущая нить остается активна, и через некоторое время снова получит управление.
  • При этом она получит управление так, как будто ThreadSwitch представляла собой обычную функцию и возвратила управление в точку, из которой она была вызвана.

Очевидно, что функцию ThreadSwitch нельзя реализовать на языке высокого уровня, вроде С, потому что это должна быть функция, которая не возвращает [немедленно] управления в ту точку, из которой она была вызвана. Она вызывается из одной нити, а передает управление в другую. Это требует прямых манипуляций стеком и записью активизации и обычно достигается использованием ассемблера или ассемблерных вставок. Некоторые ЯВУ (Ada, Java, Occam) предоставляют примитивы создания и переключения нитей в виде специальных синтаксических конструкций.

Самым простым вариантом, казалось бы, будет простая передача управления на новую нить, например, командой безусловной передачи управления по указателю. При этом весь описатель нити (struct Thread) будет состоять только из адреса, на который надо передать управление. Беда только в том, что этот вариант не будет работать.

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

  • Каждая нить имеет свой собственный стек вызовов.
  • При создании нити выделяется область памяти под стек, и указатель на эту область помещается в дескриптор нити.
  • ThreadSwitch сохраняет указатель стека (и, если таковой есть, указатель кадра) текущей нити в ее дескрипторе и восстанавливает SP из дескриптора следующей активной нити (переключение стеков необходимо реализовать ассемблерной вставкой, потому что языки высокого уровня не предоставляют средств для прямого доступа к указателю стека (пример 8.1)).
  • Когда функция ThreadSwitch выполняет оператор return, она автоматически возвращает управление в то место, из которого она была вызвана в этой нити, потому что адрес возврата сохраняется в стеке.

Пример 8.1. Кооперативный переключатель потоков.

Thread * thread_queue_head;
Thread * thread_queue_tail;
Thread * current_tread;
Thread * old__thread;
void TaskSwitch () { old_thread=current_thread;
 add_to_queue_tail(current_thread); current_thread=get_from_queue_head(); asm {.
move bx, old_thread
push bp
move ax, sp
move thread_sp[bx], ax
move bx, current_thread
move ax, rhread_sp[bx]
pop bp
}
return;
}
Если Вы заметили ошибку, выделите, пожалуйста, необходимый текст и нажмите CTRL + Enter, чтобы сообщить об этом редактору.