Одна из ключевых новых фич GScript3, отличающих язык от предшественников — поддержка многопоточности. Если конкретнее, VM языка реализует гибридную кооперативно-вытесняющую многозадачность. В отличие от большинства языков, где многопоточность завязана на системные API, в GScript3 потоки — это встроенная фича VM. Они ближе к корутинам, но при этом управляются планировщиком, что делает их очень похожими на настоящие потоки, только без оверхеда ОС.

Любая функция может быть выполнена как поток оператором spawn:

const thread = spawn func
{
    let i = 0;
    while(i < 10)
    {
        print i;
        i += 1;
    }
};

while(thread.running)
{
    // busy-wait
}

spawn возвращает ссылку на объект потока, посредством которой можно им управлять. Например, await позволяет заблокировать вызывающий контекст до тех пор, пока поток не выдаст результат. Все потоки являются также корутинами, поэтому они могут в произвольном месте выдавать результат оператором yield, не завершая свою работу:

const thread = spawn func
{
    let i = 0;

    i += 1;
    yield i;

    i += 2;
    yield i;

    i += 3;
    return i;
};

while(thread.running)
{
    // выводит значения yield по мере их поступления
    print await thread;
}

Принимая значение из корутины, await не приостанавливает ее работу — поток идет дальше до следующего yield или возврата. Но это не всегда то, что нужно — например, логика алгоритма может потребовать модифицировать общее состояние перед тем, как продолжить выполнение. Это можно сделать, если ждать результата оператором sync вместо await — он ставит поток на паузу, после чего его нужно принудительно возобновлять методом resume, когда синхронизация завершена:

const thread = spawn(null) func(self)
{
    self.i = 0;

    self.i += 1;
    yield self.i;

    self.i += 2;
    yield self.i;

    self.i += 3;
    return self.i;
};

while(thread.running)
{
    print sync thread;
    // Перезаписываем свойство объекта потока и продолжаем
    thread.i = 0;
    thread.resume();
}

Как уже было отмечено, потоки являются полноценными объектами — у них есть свойства и методы. В примере выше показано, как создать поток, который принимает на вход ссылку на самого себя — она всегда передается первым аргументом. Если вместо null передать в оператор spawn существующий объект, то поток будет использовать его (null означает, что нужно создать новый). Это позволяет запускать в потоках методы объектов:

const obj = {
    test: func(self, init)
    {
        self.i = init;

        self.i += 1;
        yield self.i;

        self.i += 2;
        yield self.i;

        self.i += 3;
        return self.i;
    }
};

const thread = spawn(obj, 5) obj.test;

while(thread.running)
{
    print sync thread;
    thread.foo = "test";
    thread.resume();
}

print obj.foo; // выведет "test"

Потоки поддерживают прототипное наследование — при создании потока можно использовать существующий объект как прототип (через оператор new):

const obj = {
    test: func(self, init)
    {
        self.i = init;

        self.i += 1;
        yield self.i;

        self.i += 2;
        yield self.i;

        self.i += 3;
        return self.i;
    }
};

const thread = spawn(new obj, 5) obj.test;

while(thread.running)
{
    print sync thread;
    thread.foo = "test";
    thread.resume();
}

print thread.foo; // "test"

Наконец, когда поток завершен, и его данные вам больше не нужны, можно освободить его для переиспользования:

thread.release();

После вызова release поток сохраняется в памяти, но его внутреннее состояние обнуляется, все свойства дают null, и новые свойства записать нельзя. Активировать поток может только VM при следующем spawn. Неактивные потоки игнорируются планировщиком — он работает со связным списком потоков, и остановленные потоки просто удаляются из списка, чтобы не создавать нагрузку на рантайм. Это делает потоки весьма легковесными, запуск существующего потока из пула — практически бесплатная операция (особенно если не создавать новый объект).

Оставить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *