Многопоточность

Потоки в Vala

Программы на Vala могут иметь более одного потока выполнения одновременно. Как они реализуются физически, в Vala коде это никак не отражается - потоки могут работать на одном ядре или нет в зависимости от рабочего окружения.

Потоки Vala не определяются во время компиляции, вместо этого определенный участок кода просто помечается, что он должен быть запущен в отдельном потоке. Это делается через использование статических методов класса Thread из библиотеки GLib, как показано в следующем (очень упрощенном) примере:

int thread_func() {
    stderr.printf("Поток работает.\n");
    Thread.usleep(1000000);
    return 42;
}

int main(string[] args) {
    if (!Thread.supported()) {
        stderr.printf("Невозможно запустить без потоков. \n");
        return 1;
    }

    try {
        var t = new Thread<int>("thread name",thread_func);
        int result = t.join();
    } catch (ThreadError e) {
        return 1;
    }

    return 0;
}

Данная программа потребует создания и запуска нового потока. В нем должен выполнится код из thread_func. Обратите внимание на проверку в начале главного метода - Vala программа не сможет воспользоваться потоками, пока она не скомпилирована подходящим образом, иначе она просто выведет ошибку и завершит выполнение. Наличие возможности проверять доступность создания потоков в системе позволяет создавать программы, которые могут работать в один или несколько потоков. Для сборки программы с поддержкой потоков наберите:

$ valac --thread threading-sample.vala

Эта команда подключит необходимые библиотеки и убедится, что система использования потоков инициализирована и готова к работе, если в ней возникнет необходимость.

Теперь программа будет выполняться без ошибок сегментации, но все еще работает не так как ожидалось. Без какого-либо цикла событий Vala программа завершит выполнение, когда завершится выполнение его основного потока (в котором работает метод main). Для контролирования поведения, вы можете создать взаимодействие потоков. Это можно сделать через мощный механизм циклов событий и асинхронных очередей, но в данном введении в потоки будут показаны только базовые возможности потоков.

Можно сказать потоку, что в данный момент ему нечего делать посредством вызова Thread.yield(), поэтому он позволит выполнятся другим потокам. Если вставить данное выражение в конец метода main, то при его достижении он будет проверять, нет ли в программе других осуществляющих работу потоков. При нахождении только что созданного метода в состоянии готовности к запуску, то он будет запущен и программа не будет завершатся, пока не закончат работу все потоки, поэтому программа будет работать как и ожидалось. Однако нет гарантии, что так будет всегда. Система может решить, и не дать новому потоку довести работу до конца, пока не перезапустится основной поток и не завершится выполнение программы.

Для ожидания полного завершения выполнения потока есть метод join(). Вызов его на объекте Thread заставит метод дождаться выполнения остальных потоков, перед дальнейшим продолжением работы. Он так же позволяет получить одному потоку возвращаемое другим потоком значение.

В этот раз при создании потока последним аргументом мы передаем true, после чего на данном потоке можно вызвать join. Так же нужно помнить, что возвращаемое при создании значение - это бесхозяйная ссылка на объект типа Thread (бесхозяйные ссылки разбираются дальше, но их незнание не критично в данном разделе). По этой ссылке можно слиять новый поток с главным. В новом варианте программы есть гарантия, что новый поток завершит свою работу полностью перед продолжением выполнения главного потока дальше и завершением выполнения программы.

Во всех этих примерах есть потенциальная проблема, т.к. новый поток не знает ничего про контекст, в котором работает. В языке С вы передаете потоку данные при создании, а в Vala обычно передается метод объекта в качестве параметра Thread.create, вместо статичного метода.

Управление ресурсами

Всегда, когда работают несколько потоков, существует вероятность того, что произойдет одновременное обращение к данным. Это может привести к состязанию, когда исход работы зависит от того, как система решить переключаться между потоками.

Для контролирования подобных ситуаций вы можете использовать ключевое слово lock, чтобы работа текущего блока кода не была прервана другим потоком, пытающимся получим доступ к тем же данным. Лучше, наверное, показать это на примере:

public class Test : GLib.Object {

    private int a { get; set; }

    public void action_1() {
        lock (a) {
            int tmp = a;
            tmp++;
            a = tmp;
        }
    }

    public void action_2() {
        lock (a) {
            int tmp = a;
            tmp--;
            a = tmp;
        }
    }
}

Данный класс определяет два метода, оба они изменяют значение переменной «а». Если бы не было оператора lock, то эти методы могли бы наложиться и окончательное значение «а» могло бы быть произвольным. Но в данном случае, благодаря оператору lock, Vala гарантирует, что если один поток уже заблокировал а, то другой должен будет дождаться своей очереди.

В Vala можно блокировать только те члены класса, которые выполняют код. Это может показаться серьезным ограничением, однако на самом деле стандартное использование данного технического приема предполагает использование классов, которые сами отвечают за сохранность своих ресурсов, поэтому блокировки будут внутри классов. Например в данном коде весь доступ к «а» инкапсулирован в класс.

Классы Subprocess и SubprocessLauncher

void main(){
  string[] cmd = {"pwd"};
            
  var launcher = new SubprocessLauncher (SubprocessFlags.STDOUT_PIPE);  
  try {
    var subprocess = launcher.spawnv (cmd);  
    var stdout_stream = subprocess.get_stdout_pipe ();
    subprocess.wait_check ();

    var stdout_datastream = new DataInputStream (stdout_stream);
          
    string stdout_line = "";
    while ((stdout_line = stdout_datastream.read_line (null)) != null) {
      message (stdout_line);
    } catch (Error e) {
      warning (@"$(e.message)");
    }
}

Last updated