Использование потоков, задач и таймеров
В OverScript потоки, задачи и таймеры могут быть обычными и локальными. Обычные запускают функцию, а локальные выражение.
Простой пример:
object t=Task(WriteLine(""), "Hello!"); //функция Task берёт ссылку на функцию в первом аргументе и создаёт задачу с этой ссылкой и аргументом "Hello!" t.StartTask(); //запуск задачи. Будет выполнена WriteLine с аргументом "Hello!". //Hello! ReadKey();
Это была обычная задача. А это локальная:
object t=LocalTask(WriteLine("Hello!")); //в LocalTask просто указывается выражение t.StartTask(); //Hello! ReadKey();
Локальные потоки, задачи и таймеры выполняют выражение в контексте функции, в которой они созданы. Они могут менять локальные переменные.
string s; object t=LocalTask(s="Hello!"); t.StartTask(); t.Wait(); //ожидание завершения выполнения задачи WriteLine("s="+s); //s=Hello! ReadKey();
А теперь - внимание!
Test(); ReadKey(); //будет выведено "s=", а не "s=Hello!" Test(){ string s="Hello!"; object t=LocalTask(WriteLine("s="+s)); t.StartTask(); }
Видим, что хоть в s задано "Hello!", WriteLine в локальной задаче выводит не то, что ожидаем. Это потому, что задача (или конкретно чтение s) была выполнена после завершения функции Test(), когда переменная s уже очищена. Т.е. переменная s в выражении WriteLine("s="+s)
ссылается на область памяти, использованную функцией Test(), но в ней уже ничего нет (на самом деле, очищаются только переменные ссылочных типов). Кстати, задача может быть выполнена и до завершения функции, и тогда будет выведено "s=Hello!". После старта задачи заранее точно не известно, когда она реально запустится и когда завершится.
Вывод: при использовании локальных потоков, задач и таймеров, если они используют локальные переменные, то нужно дожидаться их завершения там, где они были созданы. С экземплярными переменными проще: их экземпляр удерживается пока существует поток|задача|таймер.
Есть функции RunTask, RunThread, RunTimer, которые принимают выражение и автоматически упаковывают в него все локальные переменные.
Test(); //s=Hello! ReadKey(); Test(){ string s="Hello!"; object t=RunTask(WriteLine("s="+s)); }
То же самое можно так:
Test(); //s=Hello! ReadKey(); Test(){ string s="Hello!"; var e=Expr(WriteLine("s="+s)).BoxAll(true); //BoxAll упаковывает все значения, а true указывает, что только для локальных переменных object t=LocalTaskByExpr(e); t.StartTask(); }
Подробно о всех функциях
Начнём с обычных потоков
object t=Thread(Test(string\, int\)); //указывать тип нужно со слешем. string\ - это как default(string) в C#. t.StartThread("Hello", 123); //аргументы для Test передаются именно тут (с задачами по-другому) t.Wait(); ReadKey(); //str=Hello; x=123; Test(string str, int x){ WriteLine($"str={str}; x={x};"); }
В первой строке создаётся поток с функцией Test. Ссылка на неё берётся автоматически, но можно и так:
object fn=FuncRef(Test(string\, int\)); object t=ThreadByFuncRef(fn);
В StartThread() вычисляются значения всех передаваемых аргументов, и потом изменение значений в переменных не повлияет на значения в потоке.
И Thread и ThreadByFuncRef принимают опциональные параметры name и isBackground, которыми можно задать имя потока и тип (фоновый или нет).
object fn=FuncRef(Test(string\, int\)); object t=ThreadByFuncRef(fn, "TestThread", true); WriteLine(t.ThreadName()+"; "+t->IsBackground); //TestThread; True
Важно запомнить, что функции Thread первым аргументом нужно передавать функцию, но не ссылку на функцию. Так короче писать. Этот аргумент-функция не вычисляется (вызова не происходит), а нужен только для получения ссылки на функцию.
Теперь про локальные потоки
При создании локального потока нужно указывать выражение, которое будет выполнено в потоке.
object t=LocalThread(Test("It works!", 777)); t.StartThread(); //аргументы не нужны //t->Start(); //можно и так запускать. С нелокальными потоками не получится. t.Wait(); ReadKey(); //str=It works!; x=777; Test(string str, int x){ WriteLine($"str={str}; x={x};"); }
Тут выражение - вызов функции Test, но оно может быть любым.
int a=2, b=3, c; object t=LocalThread(c=a+b); //локальный поток запишет значение в переменную t.StartThread(); t.Wait(); WriteLine("c="+c); ReadKey();
Если нужно несколько действий сделать, то используйте безымянную функцию:
int a, b, c; object t=LocalThread(@(a=2, b=3, c=a+b)); //@ - это указание, что нужно вызвать базовую функцию, а имя пустое. Эта функция без имени выполняет все аргументы и возвращает значение последнего.
Потоки можно прерывать функцией Interrupt (Abort в .NET больше нет). Прерывание происходит только в состоянии WaitSleepJoin.
int n; object t=LocalThread(While(true, Write(++n+" "), Sleep(1000))); //эта функция While то же, что: while(true){Write(++n+" "); Sleep(1000);}, но мы не можем в выражении использовать стейтменты. t.StartThread(); Sleep(5000); t.Interrupt(); //после 5 секунд работы потока прерываем его ReadKey(); //1 2 3 4 5
Функция Wait (ожидание завершения потока) используется и для потоков, и для задач. А ещё можно для WaitHandle.
Можно создавать локальные потоки из экспрешена:
object e=Expr(WriteLine("OK!")); object t=LocalThreadByExpr(e); t.StartThread(); t.Wait(); //OK! ReadKey();
Можно создавать и запускать локальный поток одной функцией RunLocalThread:
object t=RunLocalThread(WriteLine("OK!")); t.Wait(); //OK! ReadKey();
Задачи
Задачи отличаются от потоков тем, что аргументы для вызываемой функции нужно указывать при создании задачи. Значения всех передаваемых аргументов сразу вычисляются, и от своих переменных больше не зависят.
object t=Task(UrlEncode("", ""), "Привет", "windows-1251"); t.StartTask(); //t->Start(); - и так можно WriteLine(t.TaskResult()); //%cf%f0%e8%e2%e5%f2 ReadKey();
Функция TaskResult ждёт завершения потока и возвращает результат. В этом примере возвращается результат функции UrlEncode, которая кодирует строку URL-адреса.
Interrupt нельзя применять к задачам. Задача запускается в потоке, и из кода выполняющегося в потоке можно получить ссылку на текущий поток, а потом прервать его извне:
object tp=ValueTuple(null); //ValueTuple создаёт кортеж (System.ValueTuple<>). В данном случае ValueTuple<object>. object t=Task(Test(object\), tp); //в функцию Test передастся кортеж t.StartTask(); Sleep(5000); //в кортеже tp находится ссылка на поток, в котором выполняется Test. tp->Item1.Interrupt(); //это то же, что и Interrupt(tp->Item1) //tp->Item1->Interrupt(); //можно и так WriteLine("end"); ReadKey(); //1 2 3 4 5 end Test(object tp){ tp->Item1=Thread(); //Thread() возвращает текущий поток. Теперь в кортеже есть ссылка на него. int n; while(true){ Write(++n+" "); Sleep(1000); } }
Вместо ValueTuple можно использовать, например, массив object[] с одним элементом. Передача же переменной по ссылке (ref) в функцию потока|задачи|таймера не поддерживается.
Переделаем с токеном отмены:
object cts=CancellationTokenSource(); object token=cts.CancellationToken(); object t=Task(Test(object\), token); t.StartTask(); Sleep(5000); cts.Cancel(); //cts->Cancel(); //можно и так WriteLine("end"); ReadKey(); //1 2 3 4 5 end Test(object token){ int n; while(!token.IsCancellationRequested()){ Write(++n+" "); Sleep(1000); } }
Как и потоки, создавать задачи можно из ссылки на функцию:
object fn=FuncRef(Test(string\, int\)); object t=TaskByFuncRef(fn);
Кстати, если у функции есть ref-параметры, то получить ссылку можно так:
object fn=FuncRef(Test(ref string\)); //добавил ref //object fn=FuncRef(Test(ref "")); //не получится, т.к. "" не переменная, а литерал (пустая строка) Test(ref string s){}
Вообще-то, ref можно ставить только перед переменными, но в преобразовании типа тоже можно, а ref string\
и есть преобразование ничего в string (т.е. значение по умолчанию). В OverScript ключевое слово ref указывает, что нужно найти именно функцию с ref-параметром. В отличие от C#, его писать не обязательно (если параметр ref, а аргумент не переменная, то при загрузке будет ошибка).
Локальные задачи, как я уже показывал в первых примерах, создаются функциями LocalTask и
LocalTaskByExpr. Покажу ещё:
string name="Jack"; object t=LocalTask(WriteLine(Format("Hello {0}! Today is {1}.", name, Now()->DayOfWeek))); t.StartTask(); t.Wait(); //Hello Jack! Today is Saturday. ReadKey();
Из экспрешена (результата функции Expr):
int a=5, b=2; object e=Expr(a+b); object t=LocalTaskByExpr(e); t.StartTask(); WriteLine(t.TaskResult()); //7 ReadKey();
Как и поток, локальную задачу можно создавать и запускать одной функцией:
object t=RunLocalTask(WriteLine("OK!")); t.Wait(); //OK! ReadKey();
С таймерами то же самое
Таймер - это объект типа System.Timers.Timer.
object t=Timer(OnTimedEvent(null, null), 1000); t.StartTimer(); //SignalTime: 23.01.2022 15:24:46 //SignalTime: 23.01.2022 15:24:47 //SignalTime: 23.01.2022 15:24:48 Sleep(3000); t.StopTimer(); ReadKey("end"); OnTimedEvent(object source, object e){ WriteLine("SignalTime: " + e->SignalTime); }
Можно задать параметры enabled и autoReset.
object t=Timer(OnTimedEvent(null, null), 1000, true, false); //true - это enabled, а false - autoReset ReadKey(); //SignalTime: 23.01.2022 15:56:42 OnTimedEvent(object source, object e){ WriteLine("SignalTime: " + e->SignalTime); }
Таймер сработал один раз потому, что autoReset=false. В OnTimedEvent передаются сам объект таймера и объект типа System.Timers.ElapsedEventArgs.
Таймер срабатывает не дожидаясь завершения выполнения предыдущего вызова. Т.е. если функция выполняемая таймером не успеет завершиться, то появится несколько одновременно выполняемых функций. Чтобы такого не происходило, нужно в Timer() пятым аргументом передать true.
object t=Timer(OnTimedEvent(null, null), 1000, true, true, true);
Так таймер будет выполнять функцию OnTimedEvent только, если предыдущий вызов завершился. Таймер не приостанавливается, а просто пропускаются вызовы функции. В большинстве случаев нужно именно такое поведение, и это также лучше тем, что интерпретатору не нужно создавать каждый раз новый массив с аргументами для функции (просто записывает в старый массив новые значения source и e), а значит будет меньше работы сборщику мусора.
Функции таймера можно передавать дополнительные аргументы.
int X=555; string Str="test"; object t=Timer(OnTimedEvent(object\, object\, int\, string\), 1000, true, true, true, X, Str); //дополнительные аргументы x и str указываются в конце ReadKey(); //SignalTime: 23.01.2022 16:24:59; x=555; str=test //SignalTime: 23.01.2022 16:25:00; x=555; str=test //SignalTime: 23.01.2022 16:25:01; x=555; str=test //... OnTimedEvent(object source, object e, int x, string s){ WriteLine("SignalTime: " + e->SignalTime+"; x="+x+"; str="+s); }
Дополнительные аргументы вычисляются при создании таймера и потом на свои переменные не ссылаются.
Как и потоки/задачи таймеры можно создавать из ссылки на функцию.
object fn=FuncRef(OnTimedEvent(object\, object\, int\, string\)); object t=TimerByFuncRef(fn, 1000, true, true, true, X, Str);
В локальный таймер заряжается выражение:
object t=LocalTimer(WriteLine("Hello! "+TickCount()), 1000, true); ReadKey(); //Hello! 928742687 //Hello! 928743687 //Hello! 928744671 //...
Ещё:
int X; object t=LocalTimer(X++, 100, true); Sleep(500); WriteLine(X); //5 //за 500 миллисекунд таймер сработал 5 раз Sleep(200); WriteLine(X); //7 //за 200 мс таймер сработал 2 раза t.StopTimer(); //кстати, можно так: t.Dispose(); ReadKey();
Локальный таймер можно создавать из экспрешена:
object e=Expr(WriteLine(TickCount())); object t=LocalTimerByExpr(e, 1000, true);
Для ленивых: RunTask, RunThread, RunTimer
Если вам не нужно менять значения локальных переменных, можно просто создавать и запускать задачи|потоки|таймеры этими функциями, которые заменяют локальные переменные в принимаемом выражении конкретными значениями.
Пример с задачей:
object task=GetTask("Hello!"); WriteLine(task.TaskResult()); //HELLO! ReadKey(); object GetTask(string s){ return RunTask(s.ToUpper()); }
В этом примере s.ToUpper()
- это фактически "Hello!".ToUpper()
(после упаковки локальной переменной s).
Пример с потоком:
object thread=GetThread("Hello!"); thread.Wait(); //HELLO! WriteLine(thread.ThreadName()); //TestThread WriteLine(thread.ThreadState()); //Stopped WriteLine(thread.IsAlive()); //False WriteLine(thread.ManagedThreadId()); //4 ReadKey(); object GetThread(string s){ return RunThread(WriteLine(s.ToUpper()), "TestThread", true); //"TestThread" - это имя потока, а true - задаёт, то что он фоновый. Это опциональные параметры. }
Теперь создание и запуск таймера:
object timer=GetTimer("Hello!"); Sleep(3000); //HELLO! HELLO! HELLO! timer.StopTimer(); ReadKey(); object GetTimer(string s){ return RunTimer(Write(s.ToUpper()+" "), 1000); }
В RunTimer третьим параметром можно задать autoReset, а четвёртым, пропускать ли вызовы, если предыдущий не завершен.
return RunTimer(Write(s.ToUpper()+" "), 1000, true, true);
Если вы пытаетесь создать таймер, поток или задачу с упаковкой локальных переменных, а в выражении локальной переменной присваивается значение, то вылетит ошибка:
Test(); Test(){ int x; object t=RunTimer(x=123, 1000, false, true); //Ошибка: Left operand variable 'x' in an assignment expression cannot be boxed. }
Функций RunTaskByExpr, RunThreadByExpr, RunTimerByExpr нет. Используйте Expr+BoxAll. Пример с таймером:
Test(); //TEST ReadKey(); Test(){ string s="Test"; object e=Expr(WriteLine(s.ToUpper())).BoxAll(true); object timer=LocalTimerByExpr(e, 1000, true, false); //object t=RunTimer(e.Eval(), 1000, false); //или так можно }
Не путайте пакующие Run* функции с RunLocalTask и RunLocalThread, которые ничего не пакуют, а просто создают и сразу запускают задачу/поток.