Многозадачность в Euphoria
Euphoria позволяет вам поставить на решение множественные независимые задачи. Каждая задача имеет свой текущий исполняемый оператор, свой собственный стек вызова и свой собственный набор частных переменных. Задачи решаются параллельно. Это означает, что перед тем, как любая данная задача будет закончена решением, все другие задачи получат шанс продвинуться в процессе решения. Диспетчер задач Euphoria устанавливает, какая задача должна решаться в любой данный момент времени.
Большинство программ, действительно, не нуждается в этой самой многозадачности и не будет в выигрыше от её наличия. Тем не менее, она очень полезна в некоторых случаях:
Euphoria поддерживает два типа задач: задачи реального времени и задачи общего времени. Задачи реального времени (ЗРВ) диспетчируются по интервалам, задаваемым числом секунд или долей секунды. Вы можете назначить активацию одной ЗРВ через каждые 3 секунды, в то время как вторая ЗРВ активизируется через каждые 0.1 секунды. Задачи общего времени (ЗОВ) нуждаются в разделении доступа к процессору машины, но не требуют жёсткой привязки ни к каким часам. 4 задачи сортировки в программе demo\dos32\tasksort.ex делят доступ к процессору, но для них неважно, чтобы переключение процессора с одной задачи на другую происходило в определённое время. С другой стороны, в программе Language War, когда корабль Euphoria идёт на скорости 4, или торпеда движется через весь экран, важно, чтобы они двигались с неизменным временнЫм шагом. Возможно редиспетчирование задачи в любое время и динамическое преобразование задачи из одного типа в другой.
Этот пример показывает главную задачу (ГЗ, с которой стартуют все программы Euphoria), создающую две дополнительные ЗРВ. Вы можете скопировать и запустить этот пример на своей машине. Вы увидите, что ЗРВ-1 получает управление через каждые 2,5..3 секунды, в то время как ЗРВ-2 включается через каждые 5..5,1 секунды. В промежутках управление принадлежит ГЗ, задаче-0, так как она проверяет нажатие на клавишу 'q', чтобы завершить исполнение программы. constant TRUE = 1, FALSE = 0 type boolean(integer x) return x = 0 or x = 1 end type boolean t1_running, t2_running procedure task1(sequence message) for i = 1 to 10 do printf(1, "task1 (%d) %s\n", {i, message}) task_yield() end for t1_running = FALSE end procedure procedure task2(sequence message) for i = 1 to 10 do printf(1, "task2 (%d) %s\n", {i, message}) task_yield() end for t2_running = FALSE end procedure puts(1, "main task: start\n") atom t1, t2 t1 = task_create(routine_id("task1"), {"Hello"}) t2 = task_create(routine_id("task2"), {"Goodbye"}) task_schedule(t1, {2.5, 3}) task_schedule(t2, {5, 5.1}) t1_running = TRUE t2_running = TRUE while t1_running or t2_running do if get_key() = 'q' then exit end if task_yield() end while puts(1, "main task: stop\n") -- программа завершается, когда ГЗ закончена
В ранних выпусках Euphoria игра Language War уже имела механизм для осуществления многозадачности, и некоторые люди представляли на странице Свежих поступлений свои собственные схемы многозадачности. Эти схемы были воплощены с использованием обычного кода Euphoria, в то время как новая система многозадачности встроена в интерпретатор. В старой системе Language War диспетчер *вызывал* очередную задачу, которая со временем должна была вернуться в диспетчер, чтобы он мог запустить следующую задачу. В новой системе задача может вызывать встроенную процедуру task_yield() в любой точке, например, с любой глубины во вложенных вызовах подпрограммы, и диспетчер, который теперь является частью интерпретатора, будет способен передать управление любой другой задаче. Когда управление вернётся к исходной задаче, её исполнение продолжится с точки, следующей за оператором task_yield(), с её стеком вызова и всеми частными переменными, остающимися в неприкосновенности. Каждая задача имеет свой собственный стек вызова, программный счётчик (т.е. текущий оператор, подлежащий исполнению) и частные переменные. Вы можете иметь несколько задач, использующих одну и ту же подпрограмму в одно и то же время, но каждая из задач будет иметь свой собственный набор частных переменных для этой подпрограммы. Глобальные и местные переменные являются общими для всех задач. Очень легко можно взять любой кусок кода и запустить его как задачу. Просто вставьте в него несколько операторов task_yield(), так чтобы этот кусок не застрял в горле у процессора.
Когда люди говорят о "нитях" (threads), они обычно имеют в виду тот механизм многозадачности, который обеспечивается операционной системой. Поэтому мы предпочитаем термин "многозадачность". Нити в общем случае работают по "вытесняющей" схеме, в то время как задачи Euphoria работают по "кооперативной". С вытесняющими нитями операционная система может жёстко переключаться с одной нити на другую практически в любое время. С кооперативными задачами каждая задача сама "знает", когда отдать процессор и позволить другой задаче принять управление им. Если задача "жадная", она может держать процессор для себя длительные периоды времени. Тем не менее, поскольку программу пишет один человек или группа единомышленников, всегда найдётся достаточно здравого смысла, чтобы скоординировать работу задач внутри одной программы должным образом. Необходимо пытаться сбалансировать нужды задач так, чтобы программа в целом работала по требованиям пользователя. Операционная система обычно исполняет множество нитей (процессов, программ), которые написаны разными людьми. И в этих условиях необходимо обеспечить некоторую степень справедливости в использовании процессорного времени между параллельно исполняемыми программами. Вытесняющая схема имеет смысл для операционной системы в целом. И в ней гораздо меньше смысла, если речь идёт об отдельной программе. Более того, система с вытесняющими нитями печально знаменита своими скрытыми и неуловимыми ошибками. Отвратительные вещи могут случаться, когда "нить разговора" рвётся и задача теряет управление в неверный момент времени. Это может быть момент обновления глобальной переменной, которая в результате неожиданно остаётся со старым значением. Иногда даже такие тривиальные вещи, как просто счетчик, могут сбиваться. Например, рассмотрим две нити. Одна имеет: x = x + 1 а вторая тоже имеет: x = x + 1 На машинном уровне первая нить загружает величину x в регистр, а затем теряет контроль в пользу второй нити, которая увеличивает x и помещает результат обратно в x (в оперативной памяти). Со временем управление переходит обратно к первой нити, которая также увеличивает x *используя величину x в регистре*, и затем помещает её в x в памяти. Таким образом, x оказывается увеличенной только однажды, вместо того, чтобы быть увеличенной дважды, как это было предусмотрено. Чтобы избежать данной проблемы, каждая нить должна иметь что-либо наподобие:
lock x где lock (закрыть) и unlock (открыть) являются специальными командами, обеспечивающими безопасность для вытесняющей схемы. Нередко программист забывает об этих специальных средствах, но программа может месяцами работать нормально. Потом, в один прекрасный день, программа рушится самым мистическим образом. Кооперативная многозадачность намного безопаснее и требует гораздо меньше затратных блокирующих операций. Задачи переключаются в безопасных точках, предусмотренных заранее.
Все эти подрограммы встроены в Euphoria, так что нет никакой необходимости в специальной библиотеке.
|