OpenOS. Потоки

fingercomp 2018-03-03   5 минут на чтение

Потоки — очень полезные штуки, позволяющие исполнять несколько кусков кода. Раньше для их использования приходилось скачивать отдельную библиотеку, работающую через костыли. Начиная с OpenOS 1.6.4, они есть в стандартной поставке ОС — в модуле thread. Давайте посмотрим, из чего она состоит — и в чём её преимущество перед любыми другим библиотеками.

Начнём с версий. OpenOS 1.6.4 — версия, включённая в OpenComputers 1.7.0. Если не хотите возиться с обновлением системы вручную, требуется иметь версию выше или равную 1.7.0.

Сразу обращаю внимание на самую важную вещь: потоки не могут исполняться одновременно. В один момент времени только один поток может работать.

В чём тогда красота тредов?

Они автономны, то есть:

Они неблокирующие:

Они отцепляемые:

Они независимы при обработке событий:

Этот набор фич в таком объёме присутствует только в этой библиотеке, и ни одна другая и не даёт столько простоты в работе с ними.

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

local thread = require("thread")

local t = thread.create(
  function(a, b)
    print("В потоке получены аргументы:", a, b)
  end,
  21, 42
)

Функция возвращает объект потока. Его же может получить сам поток вызовом thread.current() — однако если вызвана не в потоке, то возвращает nil. На всякий случай, основной процесс не является потоком.

Объект потока позволяет чудить различные вещи с потоком.

t:suspend() приостанавливает поток. Как уже сказано, такой поток не будет получать события и обрабатывать тики таймера. Забавно, что если приостановить поток, когда он ждёт события, то неизвестно, что он получит после его возобновления.

t:resume() возобновляет работу ранее приостановленного потока. Так как созданные потоки сразу начинают работу, то обычно этот метод вызывать не придётся.

t:kill() убивает поток, то есть завершает его, удаляя всех слушателей и таймеры. Возобновить работу потока после того, как он убит, нельзя.

t:status() возвращает строку со статусом потока:

t:attach() позволяет сменить родителя у потока. Без аргумента поток будет присоединён к текущему процессу. Переданное как аргумент число позволяет указать, к кому присоединить: 0 — текущий процесс, 1 — родитель текущего и т. д.

t:detach() отцепляет поток от родителя. Такой поток будет работать до его остановки или перезагрузки компьютера.

t:join() останавливает процесс, в котором была вызвана это функция, до завершения потока t.

local thread = require("thread")

local t = thread.create(
  function()
    os.sleep(10)
  end
)

t:join() -- остановится на 10 секунд

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

t:join ждёт только одного потока. Для групп потоков есть функции thread.waitForAny и thread.waitForAll — обратите внимание, что это функции библиотеки, а не методы объекта потока.

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

thread.waitForAll ждёт, пока завершатся все потоки из списка.

local thread = require("thread")

local t1 = thread.create(
  function()
    os.sleep(10)
  end
)

local t2 = thread.create(
  function()
    os.sleep(15)
  end
)

thread.waitForAll({t1, t2})
print("Это сообщение будет написано через 15 секунд")

thread.waitForAny ждёт, пока завершится хотя бы один поток из списка.

local thread = require("thread")

local t1 = thread.create(
  function()
    os.sleep(10)
  end
)

local t2 = thread.create(
  function()
    os.sleep(15)
  end
)

thread.waitForAny({t1, t2})
print("Это сообщение будет написано через 10 секунд")

Что будет, если поток бросает ошибку? При ошибке в потоке она не будет проброшена в родительский процесс. Как и со слушателями, она будет записана в файл /tmp/event.log, но родитель не сможет узнать причину ошибки — и, вообще, успешно ли завершился поток.

local thread = require("thread")

local t = thread.create(
  function()
    os.sleep(3)
    error("test")
  end
)

print(t:status()) --> running
t:join()
print(t:status()) --> dead

Кроме того, событие жёстокого прерывания (Ctrl+Alt+C) не передаётся всем процессам — только одному; причём неизвестно, какому именно: родителю или одному из его потоков. Если вы используете потоки, первым делом сделайте один, который будет ждать события interrupted и подчищать ресурсы.

local thread = require("thread")

local cleanupThread = thread.create(
  function()
    event.pull("interrupted")
    print("Принял ^C, чищу всякие ресурсы")
  end
)

local mainThread = thread.create(
  function()
    while true do
      local input = io.read()
      if input == "exit" then
        break
      end
    end
  end
)

thread.waitForAny({ cleanupThread, mainThread })
os.exit(0)

Обратите внимание, что в конце программы стоит os.exit. Где-то я уже упоминал не раз, что родительский процесс, достигнув конца программы, не завершится до тех пор, пока работает хотя бы один из его дочерних потоков. Вызов os.exit() позволяет выйти из программы, закрыв все дочерние потоки. Что, безусловно, достаточно удобно.

Есть ещё один момент. Допустим, данная программа запускается в роботе:

local robot = require("robot")
local thread = require("thread")

local moveThread = thread.create(
  function()
    while true do
      robot.forward()
    end
  end
)

local inputThread = thread.create(
  function()
    while true do
      local input = io.read()
      if input == "exit" then
        break
      end
    end
  end
)

thread.waitForAny({ inputThread, moveThread })
os.exit(0)

Если вы запустите эту программу, то должны заметить, что вы ничего не сможете написать в роботе, хотя работает io.read. Дело в том, что функция robot.forward вызывает метод компонента, который блокирует исполнение компьютера. Пока робот двигается, на компьютере не может выполняться ни одна команда.

Чтобы хоть что-то можно было вставить в строку, то поставьте после robot.forward какой-нибудь os.sleep(0) — он позволит соседнему потоку принять и обработать события. Тем не менее, строка ввода всё равно будет работать с тормозами.

В подобном случае задумайтесь над тем, чтобы использовать вместо строки ввода иное средство коммуникации: редстоун, сеть, интернет-сокет.

Несмотря на всё, библиотека действительно облегчает работу с потоками в OpenOS. Кроме того, очень удобно поместить все слушатели событий в один поток, чтобы они все автоматически были удалены после убийства потока.

local event = require("event")
local thread = require("thread")

local mainThread = thread.create(
  function()
    event.listen("key_down",
      function(evt, addr, key, code, user)
        print("A key has been pressed!")
      end
    )
    while true do
      print("do something")
      os.sleep(0.5)
    end
  end
)

-- событие interrupted не ловится обработчиками
local intThread = thread.create(
  function()
    event.pull("interrupted")
  end
)

thread.waitForAny({ mainThread, intThread })
os.exit(0)

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

В общем, красота.