Программирование LEGO NXT роботов на языке NXC - Параллельные задачи — различия между версиями

Материал из roboforum.ru Wiki
Перейти к: навигация, поиск
(Создана новая страница размером <p align=center><b>Автор: Daniele Benedettelli</b><br><br><i>Перевод: © Ботов Антон aka =DeaD=, 2009<br><br>Экс...)
 
(Подводим итоги)
 
(не показано 12 промежуточных версий этого же участника)
Строка 1: Строка 1:
 
<p align=center><b>Автор: Daniele Benedettelli</b><br><br><i>Перевод: © Ботов Антон aka =DeaD=, 2009<br><br>Эксклюзивно для www.roboforum.ru<br> копирование на другие ресурсы и публикация перевода<br>без разрешения его автора запрещены</i></p>
 
<p align=center><b>Автор: Daniele Benedettelli</b><br><br><i>Перевод: © Ботов Антон aka =DeaD=, 2009<br><br>Эксклюзивно для www.roboforum.ru<br> копирование на другие ресурсы и публикация перевода<br>без разрешения его автора запрещены</i></p>
  
==Параллельные задачи==
+
=Параллельные задачи=
As has been indicated before, tasks in NXC are executed simultaneously, or in parallel as people usually say.
+
Как показывалось ранее, задачи в NXC выполняются одновременно, или как обычно говорят - параллельно. Это очень удобно. Такая возможность позволяет вам следить за датчиками в одной задаче, пока вторая задача управляет движением робота, а, скажем, третья задача играет какую-нибудь музыку. Но параллельные задачи могут создавать проблем. Одна задача может начать мешать другой.
This is extremely useful. In enables you to watch sensors in one task while another task moves the robot around,
+
 
and yet another task plays some music. But parallel tasks can also cause problems. One task can interfere with
 
another.
 
 
==Неправильная программа==
 
==Неправильная программа==
Consider the following program. Here one task drives the robot around in squares (like we did so often before)
+
Разберем следующую программу. В ней одна задача управляет движением робота по квадратам (как мы часто уже делали раньше), а другая задача проверяет датчик касания, когда датчик нажат, она отводит робота немного назад и делает поворот на 90 градусов.
and the second task checks for the touch sensor. When the sensor is touched, it moves a bit backwards, and
+
 
makes a 90-degree turn.
+
task check_sensors()
task check_sensors()
+
{
{
+
  while (true)
while (true)
+
  {
{
+
    if (SENSOR_1 == 1)
if (SENSOR_1 == 1)
+
    {
{
+
      OnRev(OUT_AC, 75);
OnRev(OUT_AC, 75);
+
      Wait(500);
Wait(500);
+
      OnFwd(OUT_A, 75);
OnFwd(OUT_A, 75);
+
      Wait(850);
Wait(850);
+
      OnFwd(OUT_C, 75);
OnFwd(OUT_C, 75);
+
    }
}
+
  }
}
+
}
}
+
task submain()
+
task submain()
{
+
{
while (true)
+
  while (true)
{
+
  {
OnFwd(OUT_AC, 75); Wait(1000);
+
    OnFwd(OUT_AC, 75); Wait(1000);
OnRev(OUT_C, 75); Wait(500);
+
    OnRev(OUT_C, 75); Wait(500);
}
+
  }
}
+
}
task main()
+
{
+
task main()
SetSensor(IN_1,SENSOR_TOUCH);
+
{
Precedes(check_sensors, submain);
+
  SetSensor(IN_1,SENSOR_TOUCH);
}
+
  Precedes(check_sensors, submain);
This probably looks like a perfectly valid program. But if you execute it you will most likely find some
+
}
unexpected behavior. Try the following: Make the robot touch something while it is turning. It will start going
+
 
back, but immediately moves forwards again, hitting the obstacle. The reason for this is that the tasks may
+
Вероятно программа выглядит как совершенно правильная, но если вы попробуете загрузить её в робота вы скорее всего обнаружите некоторое необычное его поведение. Попробуйте следующее, чтобы убедиться в этом: сделайте так, чтобы робот задел что-то бампером при повороте. Он начнет отъезжать назад, но немедленно снова начнет двигаться вперед, снова ударяясь о препятствие. Причина этого в том, что задачи начинают пересекаться. А происходит вот что. Робот едет в повороте и ударяется об препятствие. Он начинает отъезжать назад, но в этот момент главная задача (submain) завершает ожидание конца разворота и даёт команду ехать прямо на препятствие, игнорируя тот факт, что сейчас выполняется маневр отъезда от него. Вторая задача в это время находится в состоянии ожидания и даже не сможет обнаружить столкновение. Это точно не то поведение робота, которое мы хотели бы видеть. Проблема заключается в том, что пока вторая задача ждёт мы забыли, что первая задача продолжает работу и её действия начинают мешать работе задаче избегания препятствий.
interfere. The following is happening. The robot is turning right, that is, the first task is in its second sleep
+
 
statement. Now the robot hits the sensor. It start going backwards, but at that very moment, the main task is
+
==Критические секции и "мьютекс"-переменные==
ready with sleeping and moves the robot forwards again; into the obstacle. The second task is sleeping at this
+
Один из способов решения этой проблемы это введение механизма, гарантирующего, что только одна задача в данный момент работает с двигателями робота. Такой подход мы уже применяли в главе 6. Давайте посмотрим на эту программу еще раз.
moment so it won't notice the collision. This is clearly not the behavior we would like to see. The problem is
+
 
that, while the second task is sleeping we did not realize that the first task was still running, and that its actions
+
mutex moveMutex;
interfere with the actions of the second task.
+
task move_square()
 +
{
 +
  while (true)
 +
  {
 +
    Acquire(moveMutex);
 +
    OnFwd(OUT_AC, 75); Wait(1000);
 +
    OnRev(OUT_C, 75); Wait(850);
 +
    Release(moveMutex);
 +
  }
 +
}
 +
 +
task check_sensors()
 +
{
 +
  while (true)
 +
  {
 +
    if (SENSOR_1 == 1)
 +
    {
 +
      Acquire(moveMutex);
 +
      OnRev(OUT_AC, 75); Wait(500);
 +
      OnFwd(OUT_A, 75); Wait(850);
 +
      Release(moveMutex);
 +
    }
 +
  }
 +
}
 +
 +
task main()
 +
{
 +
  SetSensor(IN_1,SENSOR_TOUCH);
 +
  Precedes(check_sensors, move_square);
 +
}
 +
 
 +
Загвоздка здесь в том, что обе задачи (check_sensors и move_square) контролируют моторы только если другая задача не использует их в данный момент. Это делается с помощью команд Acquire, которые ждут, чтобы "мьютекс"-переменная moveMutex стала свободной, перед тем как начать работать с моторами, после чего команда блокирует эту переменую и передаёт управление следующей команде. Команда Acquire всегда используется на пару с командой Release, которая сообщает системе, что "мьютекс"-переменная освободилась, и теперь другие задачи могут использовать соответствующий общий ресурс, в нашем случае - двигатели робота. Кусок программы внутри пары  Acquire -  Release называется критической областью программы, в том смысле, что используются общие ресурсы. Это гарантирует что в таких областях задачи не будут конфликтовать друг с другом по объявленным общим ресурсам.
 +
 
 +
==Использование семафоров==
 +
Существует возможность собрать самому альтернативу "мьютекс"-переменым, являющуюся явной реализацией команд Acquire и Release.
 +
 
 +
Стандартный способ решить эту программу - объявить переменную, которая будет указывать, какая задача контроллирует двигатели.
 +
Другие задачи не должны использовать двигатели, пока первая задача не объявит их свободными. Такая переменная часто называется семафором. Пусть sem это такой семафор (это то же самое по сути, что и "мьютекс"). Мы будем считать, что значение 0 показывает, что двигатели свободны. Теперь, если задача хочет использовать моторы - она должна выполнить следующие команды:
 +
 
 +
until (sem == 0);
 +
sem = 1; //Acquire(sem);
 +
// Чего-то делаем с моторами
 +
// (Это критическая область программы)
 +
sem = 0; //Release(sem);
 +
 
 +
Сначала мы ждем, пока моторы будут свободны, затем забираем этот ресурс себе, выставляя 1. Теперь мы управляем ими. Когда мы всё, что хотели, сделали - пишем в sem снова 0. Ниже можно увидеть программу, реализующую семафор. Когда обнаруживается нажатие датчика касания, семафор выставляется в 1 и выполняется процедура отката назад. Во время неё задача move_square обязана ждать. Когда откат выполнен - семафор выставляется снова в 0 и задача move_square может продолжить.
 +
 
 +
int sem;
 +
task move_square()
 +
{
 +
  while (true)
 +
  {
 +
    until (sem == 0); sem = 1;
 +
    OnFwd(OUT_AC, 75);
 +
    sem = 0;
 +
    Wait(1000);
 +
    until (sem == 0); sem = 1;
 +
    OnRev(OUT_C, 75);
 +
    sem = 0;
 +
    Wait(850);
 +
  }
 +
}
 +
 +
task submain()
 +
{
 +
  SetSensor(IN_1, SENSOR_TOUCH);
 +
  while (true)
 +
  {
 +
    if (SENSOR_1 == 1)
 +
    {
 +
      until (sem == 0); sem = 1;
 +
      OnRev(OUT_AC, 75); Wait(500);
 +
      OnFwd(OUT_A, 75); Wait(850);
 +
      sem = 0;
 +
    }
 +
  }
 +
}
 +
 +
task main()
 +
{
 +
  sem = 0;
 +
  Precedes(move_square, submain);
 +
}
  
Critical sections and mutex variables
+
Вы можете сказать, что не ясно зачем в задаче move_square мы выставляем семафор в 1 и обратно в 0. Но на самом деле это полезно. Причина в том, что команда OnFwd() на самом деле состоит из двух команд (см. главу 8). Если вы не хотите, чтобы эту пару команд прервала другая задача - нужно так же блокировать здесь доступ к устройству.
One way of solving this problem is to make sure that at any moment only one task is driving the robot. This was
 
the approach we took in Chapter VI. Let me repeat the program here.
 
  
mutex moveMutex;
+
Семафоры очень полезны и, когда вы пишете достаточно сложные программы с параллельными задачами, они почти всегда будут вам нужны. (Кстати, существует некоторый небольшой шанс, что такой семафор может не сработать. Попробуйте разобраться, почему.)
task move_square()
 
{
 
while (true)
 
{
 
Acquire(moveMutex);
 
OnFwd(OUT_AC, 75); Wait(1000);
 
OnRev(OUT_C, 75); Wait(850);
 
Release(moveMutex);
 
}
 
}
 
task check_sensors()
 
{
 
while (true)
 
{
 
if (SENSOR_1 == 1)
 
{
 
Acquire(moveMutex);
 
OnRev(OUT_AC, 75); Wait(500);
 
OnFwd(OUT_A, 75); Wait(850);
 
Release(moveMutex);
 
}
 
}
 
}
 
task main()
 
{
 
SetSensor(IN_1,SENSOR_TOUCH);
 
Precedes(check_sensors, move_square);
 
}
 
The crux is that both the check_sensors and move_square tasks can control motors only if no other task is
 
using them: this is done using the Acquire statement that waits for the moveMutex mutual exclusion variable
 
to be released before using motors. The Acquire command counterpart is the Release command, that frees the
 
mutex variable so other tasks can use the critical resource, motors in our case. The code inside the acquirerelease
 
scope is called critical region: critical means that shared resources are used. In this way tasks cannot
 
interfere with each other.
 
Using semaphores
 
There is a hand-made alternative to mutex variables that is the explicit implementation of the Acquire and
 
Release commands.
 
A standard technique to solve this problem is to use a variable to indicate which task is in control of the motors.
 
The other tasks are not allowed to drive the motors until the first task indicates, using the variable, that it is
 
ready. Such a variable is often called a semaphore. Let sem be such a semaphore (same as mutex). We assume
 
that a value of 0 indicates that no task is steering the motors (resource is free). Now, whenever a task wants to do
 
something with the motors it executes the following commands:
 
until (sem == 0);
 
sem = 1; //Acquire(sem);
 
// Do something with the motors
 
// critical region
 
sem = 0; //Release(sem);
 
  
So we first wait till nobody needs the motors. Then we claim the control by setting sem to 1. Now we can control
 
the motors. When we are done we set sem back to 0. Here you find the program above, implemented using a
 
semaphore. When the touch sensor touches something, the semaphore is set and the backup procedure is
 
performed. During this procedure the task move_square must wait. At the moment the back-up is ready, the
 
semaphore is set to 0 and move_square can continue.
 
int sem;
 
task move_square()
 
{
 
while (true)
 
{
 
until (sem == 0); sem = 1;
 
OnFwd(OUT_AC, 75);
 
sem = 0;
 
Wait(1000);
 
until (sem == 0); sem = 1;
 
OnRev(OUT_C, 75);
 
sem = 0;
 
Wait(850);
 
}
 
}
 
task submain()
 
{
 
SetSensor(IN_1, SENSOR_TOUCH);
 
while (true)
 
{
 
if (SENSOR_1 == 1)
 
{
 
until (sem == 0); sem = 1;
 
OnRev(OUT_AC, 75); Wait(500);
 
OnFwd(OUT_A, 75); Wait(850);
 
sem = 0;
 
}
 
}
 
}
 
task main()
 
{
 
sem = 0;
 
Precedes(move_square, submain);
 
}
 
You could argue that it is not necessary in move_square to set the semaphore to 1 and back to 0. Still this is
 
useful. The reason is that the OnFwd() command is in fact two commands (see Chapter VIII). You don't want
 
this command sequence to be interrupted by the other task.
 
Semaphores are very useful and, when you are writing complicated programs with parallel tasks, they are almost
 
always required. (There is still a slight chance they might fail. Try to figure out why.)
 
 
==Подводим итоги==
 
==Подводим итоги==
In this chapter we studied some of the problems that can occur when you use different tasks. Always be very
+
В этой главе мы научились решать некоторые проблемы, которые могут возникнуть при параллельной работе нескольких задач. Всегда будьте внимательны к возможным побочным эффектам при такой работе. Очень много случаев незапланированной работы программ возникает при работе с параллельными задачами. Мы увидели два возможных пути решения таких проблем. Первый заключается в том, что задачи выполняются в критической своей области по очереди. Второй подход использует семафоры для управления порядком выполнения команд. Это гарантирует что только одна задача в каждый момент времени будет находиться в своей критической области связанной с соответствующим общим ресурсом.
careful for side effects. Much unexpected behavior is due to this. We saw two different ways of solving such
 
problems. The first solution stops and restarts tasks to make sure that only one critical task is running at every
 
moment. The second approach uses semaphores to control the execution of tasks. This guarantees that at every
 
moment only the critical part of one task is executed.
 

Текущая версия на 07:49, 20 мая 2009

Автор: Daniele Benedettelli

Перевод: © Ботов Антон aka =DeaD=, 2009

Эксклюзивно для www.roboforum.ru
копирование на другие ресурсы и публикация перевода
без разрешения его автора запрещены

Параллельные задачи

Как показывалось ранее, задачи в NXC выполняются одновременно, или как обычно говорят - параллельно. Это очень удобно. Такая возможность позволяет вам следить за датчиками в одной задаче, пока вторая задача управляет движением робота, а, скажем, третья задача играет какую-нибудь музыку. Но параллельные задачи могут создавать проблем. Одна задача может начать мешать другой.

Неправильная программа

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

task check_sensors()
{
  while (true)
  {
    if (SENSOR_1 == 1)
    {
      OnRev(OUT_AC, 75);
      Wait(500);
      OnFwd(OUT_A, 75);
      Wait(850);
      OnFwd(OUT_C, 75);
    }
  }
}

task submain()
{
  while (true)
  {
    OnFwd(OUT_AC, 75); Wait(1000);
    OnRev(OUT_C, 75); Wait(500);
  }
}

task main()
{
  SetSensor(IN_1,SENSOR_TOUCH);
  Precedes(check_sensors, submain);
}

Вероятно программа выглядит как совершенно правильная, но если вы попробуете загрузить её в робота вы скорее всего обнаружите некоторое необычное его поведение. Попробуйте следующее, чтобы убедиться в этом: сделайте так, чтобы робот задел что-то бампером при повороте. Он начнет отъезжать назад, но немедленно снова начнет двигаться вперед, снова ударяясь о препятствие. Причина этого в том, что задачи начинают пересекаться. А происходит вот что. Робот едет в повороте и ударяется об препятствие. Он начинает отъезжать назад, но в этот момент главная задача (submain) завершает ожидание конца разворота и даёт команду ехать прямо на препятствие, игнорируя тот факт, что сейчас выполняется маневр отъезда от него. Вторая задача в это время находится в состоянии ожидания и даже не сможет обнаружить столкновение. Это точно не то поведение робота, которое мы хотели бы видеть. Проблема заключается в том, что пока вторая задача ждёт мы забыли, что первая задача продолжает работу и её действия начинают мешать работе задаче избегания препятствий.

Критические секции и "мьютекс"-переменные

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

mutex moveMutex;
task move_square()
{
  while (true)
  {
    Acquire(moveMutex);
    OnFwd(OUT_AC, 75); Wait(1000);
    OnRev(OUT_C, 75); Wait(850);
    Release(moveMutex);
  }
}

task check_sensors()
{
  while (true)
  {
    if (SENSOR_1 == 1)
    {
      Acquire(moveMutex);
      OnRev(OUT_AC, 75); Wait(500);
      OnFwd(OUT_A, 75); Wait(850);
      Release(moveMutex);
    }
  }
}

task main()
{
  SetSensor(IN_1,SENSOR_TOUCH);
  Precedes(check_sensors, move_square);
}

Загвоздка здесь в том, что обе задачи (check_sensors и move_square) контролируют моторы только если другая задача не использует их в данный момент. Это делается с помощью команд Acquire, которые ждут, чтобы "мьютекс"-переменная moveMutex стала свободной, перед тем как начать работать с моторами, после чего команда блокирует эту переменую и передаёт управление следующей команде. Команда Acquire всегда используется на пару с командой Release, которая сообщает системе, что "мьютекс"-переменная освободилась, и теперь другие задачи могут использовать соответствующий общий ресурс, в нашем случае - двигатели робота. Кусок программы внутри пары Acquire - Release называется критической областью программы, в том смысле, что используются общие ресурсы. Это гарантирует что в таких областях задачи не будут конфликтовать друг с другом по объявленным общим ресурсам.

Использование семафоров

Существует возможность собрать самому альтернативу "мьютекс"-переменым, являющуюся явной реализацией команд Acquire и Release.

Стандартный способ решить эту программу - объявить переменную, которая будет указывать, какая задача контроллирует двигатели. Другие задачи не должны использовать двигатели, пока первая задача не объявит их свободными. Такая переменная часто называется семафором. Пусть sem это такой семафор (это то же самое по сути, что и "мьютекс"). Мы будем считать, что значение 0 показывает, что двигатели свободны. Теперь, если задача хочет использовать моторы - она должна выполнить следующие команды:

until (sem == 0);
sem = 1; //Acquire(sem);
// Чего-то делаем с моторами
// (Это критическая область программы)
sem = 0; //Release(sem);

Сначала мы ждем, пока моторы будут свободны, затем забираем этот ресурс себе, выставляя 1. Теперь мы управляем ими. Когда мы всё, что хотели, сделали - пишем в sem снова 0. Ниже можно увидеть программу, реализующую семафор. Когда обнаруживается нажатие датчика касания, семафор выставляется в 1 и выполняется процедура отката назад. Во время неё задача move_square обязана ждать. Когда откат выполнен - семафор выставляется снова в 0 и задача move_square может продолжить.

int sem;
task move_square()
{
  while (true)
  {
    until (sem == 0); sem = 1;
    OnFwd(OUT_AC, 75);
    sem = 0;
    Wait(1000);
    until (sem == 0); sem = 1;
    OnRev(OUT_C, 75);
    sem = 0;
    Wait(850);
  }
}

task submain()
{
  SetSensor(IN_1, SENSOR_TOUCH);
  while (true)
  {
    if (SENSOR_1 == 1)
    {
      until (sem == 0); sem = 1;
      OnRev(OUT_AC, 75); Wait(500);
      OnFwd(OUT_A, 75); Wait(850);
      sem = 0;
    }
  }
}

task main()
{
  sem = 0;
  Precedes(move_square, submain);
}

Вы можете сказать, что не ясно зачем в задаче move_square мы выставляем семафор в 1 и обратно в 0. Но на самом деле это полезно. Причина в том, что команда OnFwd() на самом деле состоит из двух команд (см. главу 8). Если вы не хотите, чтобы эту пару команд прервала другая задача - нужно так же блокировать здесь доступ к устройству.

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

Подводим итоги

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