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

Материал из roboforum.ru Wiki
Версия от 07:23, 20 мая 2009; =DeaD= (обсуждение | вклад) (Неправильная программа)
Перейти к: навигация, поиск

Автор: 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) завершает ожидание конца разворота и даёт команду ехать прямо на препятствие, игнорируя тот факт, что сейчас выполняется маневр отъезда от него. Вторая задача в это время находится в состоянии ожидания и даже не сможет обнаружить столкновение. Это точно не то поведение робота, которое мы хотели бы видеть. Проблема заключается в том, что пока вторая задача ждёт мы забыли, что первая задача продолжает работу и её действия начинают мешать работе задаче избегания препятствий.

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

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.

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

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.