Актуально для версии 4.11+
Многопоточность в Clickermann
Как все знают со школы, Windows – многозадачная операционная система. Впрочем, как и большинство современных ОС, за исключением особо специализированных. Это значит, что на машине под управлением ОС могут одновременно выполняться несколько задач, в нашем случае приложений. Можно долго рассуждать на тему, что одно неудачное действие повесит все намертво и что задачи выполняются не параллельно, а просто быстро переключаются друг между другом. Но примем это за аксиому.
Подобно этому, в составе приложения можно организовать несколько потоков (нитей, англ. thread). И они точно так же могут выполняться параллельно, как и сами приложения. С рядом оговорок. Во-первых, потоки могут свободно использовать общие данные своего родительского приложения. Во-вторых (хотя косвенно это вытекает из первого) потоки могут останавливать друг друга, снова запускать и так далее.
Для чего нужны потоки? Для оптимизации работы. Если взять за догмат то, что две параллельные задачи выполняются по крайней мере не медленнее, чем две последовательные (хотя это спорное утверждение), то целесообразно наиболее ресурсоемкие выделять в отдельные потоки. Это позволит выиграть в общем логическом быстродействии. Поясню почему. Если один какой-то поток повиснет, серьезно задумается или в нем произойдет сбой, это не убьет (замедлит) приложение целиком. Это вы можете наблюдать например в браузере Хром, где повисание одной вкладки не парализует весь браузер. Огнелиса так не умеет…
Существует лишь одна особенность – разделение ресурсов. При прочих, равных нельзя знать в какой момент начнет и бросит выполняться поток, помещать в них стоит относительно автономные задачи. Плохая идея в одном потоке наращивать переменную, а в другом записывать ее значение в файл столбиком. Пропуски значений и наоборот повторяющиеся – обычное дело при данном некорректном логическом подходе. Для решения этой задачи используются общие переменные флаги – «семафоры», анализируя которые потоки понимают, какой из них отработал и чья очередь теперь выполнить свою задачу.
Перейдем наконец к коду. Поток в кликермане объявляется следующим, похожим на подпрограмму, образом.
Thread(thr_name)
// тело потока
End_thread
Данный поток с именем thr_name начнет крутиться сразу после старта скрипта. Если вы хотите что бы поток запустился позже, то необходимо в шапке после имени добавить 0. То есть
Thread(thr_name, 0)
// тело потока
End_thread
Теперь данный поток можно запустить лишь из самого скрипта специальной процедурой контроля потоков setThread:
setThread(thr_name, 1)
в которой thr_name – имя потока, а 1 – присваиваемое состояние «запущен», заменив которое на 0 можно так же приостановить поток. Потоки могут невозбранно контролировать друг друга.
Каждый поток обладает собственным адресным пространством, поэтому задержка wait() в одном ни коим образом не влияет на остальные потоки. При этом, потоки имеют доступ к общим переменным и графическому буферу. Стоит отметить, что из-за особенностей архитектуры кликера, подпрограммы Sub() не могут разделяться потоками, поэтому для каждого они индивидуальны и должны быть объявлены внутри потока. Другими словами, подпрограммы описанные внутри одного потока не могут быть вызваны другим потоком.
Основное (абстрактно) тело скрипта, даже если оно описано без thread() .. end_thread так же является отдельным потоком, поэтому для него справедливо все вышесказанное. Поэтому фактически, сценарии вида
Thread(thr1)
Wait(1)
End_thread
Print("Hello")
Wait(3)
И
Thread(thr1)
Wait(1)
End_thread
Thread(thr2)
Print("Hello")
Wait(3)
End_thread
Абсолютно одинаковы.
Последнее что вам необходимо знать, это то, что как и основной скрипт, потоки должны «давать передохнуть» процессору, поэтому потрудитесь разместить внутри них wait’ы, на которые он обязательно наткнется, иначе ваш сценарий будет серьезно нагружать процессор в ряде случаев.
Несколько показательных примеров
Первый случай, о котором иногда спрашивают – это нажатие клавиш (да и вообще действия) по интервалу. Например, каждые 3 секунды жмем A, каждые 5 секунд – B. В обычном случае нам бы потребовался единичный интервал (секунда) и два счетчика, которые наращиваются. Затем в двух условиях проверяются их значения и если счетчик достиг 3 (или 5) то выполняются действия и счетчик обнуляется. Два счетчика нужны, потому что действия независимы друг от друга. Сам скрипт выглядит так (для удобства сами нажатия заменены на вывод в лог)
wait(1)
$cnt1 = $cnt1 + 1
$cnt2 = $cnt2 + 1
if($cnt1 = 3)
print("A")
$cnt1 = 0
end_if
if($cnt2 = 5)
print("B")
$cnt2 = 0
end_if
При этом, если внутри условия будет ряд других затратных инструкций или упаси Боже задержка – в классическом решении реализовать такое было бы весьма затратно (приходилось бы вычислять временные затраты на каждое действие, вводить поправки на разницу и т.д.)
Реализуем тот же самый скрипт на потоках
thread(th1)
print("A")
wait(3)
end_thread
thread(th2)
print("B")
wait(5)
end_thread
Как вы видите, во первых скрипт стал более компактный и гораздо удобнее воспринимается визуально. Во вторых, даже если в первом потоке реализовать часовую задержку, второй поток стабильно продолжит чеканить B каждые 5 секунд.
Второе распространенное решение – реализация псевдо хоткеев через iskeydown(). Как вы знаете эта функция проверяет нажата ли клавиша. Но проверить это она может только в момент собственного выполнения. То есть если у вас есть два условия с iskeydown() и первое сработало, то в том случае если внутри него будут пресловутые задержки и долгие действия, проверить нажатие второй клавиши удастся только когда первое условие отработает целиком.
Пример старого образца. Два последовательных условия, которые в случае нажатия клавиши печатают ее в лог каждые 3 секунды наши любимые буквы A и B. Поскольку задержка в каждом условии составляет 3 секунды, то у вас не получится вывести в протокол сразу обе буквы даже если вы зажмете обе клавиши одновременно.
if (iskeydown(#A)=1)
print("A")
wait(3)
end_if
if (iskeydown(#B)=1)
print("B")
wait(3)
end_if
waitms(10)
Безусловно, можно попробовать добавить третье условие двойной проверки обоих нажатий iskeydown() & iskeydown(), но тогда будут срабатывать и одиночные условия, ведь клавиша то зажата. Неразрешимая для неискушенного программиста задача элементарно решается введением потоков под каждый хоткей.
thread(th1)
if (iskeydown(#A)=1)
print("A")
wait(3)
end_if
waitms(10)
end_thread
thread(th2)
if (iskeydown(#B)=1)
print("B")
wait(3)
end_if
waitms(10)
end_thread
Визуально код стал более объемным, однако не стоит забывать, что в отличие от первого, этот – работает. Вы легко можете зажимать клавиши хоть одновременно, хоть как – такты не собьются.
Таким образом, потоки позволяют легко решить ряд прикладных задач, которые до этого решались если не запущенными копиями кликера, то как минимум жуткими костылями. И их потенциал неограничен приведенными примерами, конечно же.