Пост Запуск Go в браузере с WASM и Web Workers впервые появился на Qvault.

Недавно мы внесли большие изменения в то, как мы запускаем Go в браузере на Qvault, и хотим объяснить улучшения. Web Workers — это причина, по которой мы смогли решить некоторые серьезные проблемы с кодированием, связанные с браузером, которые сдерживали нас. Считайте эту статью продолжением статьи Запуск Go в браузере с помощью Web Assembly.

При публикации нашего последнего курса Алгоритмы Big-O нам понадобился способ печатать вывод консоли во время выполнения кода. Мы столкнулись с проблемой при запуске в браузере ресурсоемких алгоритмов; браузер настолько увязает, что не может отображать новые строки вывода. Мы решили внедрить веб-воркеров, и они ловко решили проблему.

Проблема

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

Например, этот код использовался для вывода всего вывода сразу:

package main

import (
	"fmt"
)

func main(){
	const max = 100000000
	for i := 0; i < max; i++{
		if i % (max/10) == 0{
			fmt.Println(i)
		}
	}
}

После добавления веб-воркеров теперь он правильно печатает каждое число во время выполнения. Можете убедиться сами на детской площадке здесь.

Что такое веб-воркер?

Веб-воркеры — это простое средство веб-контента для запуска сценариев в фоновых потоках.

Мозилла

Другими словами, это наш способ, наконец, вырваться из тисков однопоточного JavaScript! Мы можем разгрузить дорогостоящие задачи в другой поток выполнения. При этом браузер может свободно отображать обновления на экране.

Как это работает — Рабочий

Как вы знаете, мы компилируем код в редакторе в WASM на наших серверах. Если вам интересна эта часть, вы можете прочитать об этом в нашем предыдущем посте. Как только код скомпилирован в Web Assembly, он отправляется обратно в наш интерфейс для выполнения.

Чтобы запустить Web Worker, нам нужен скрипт, определяющий worker. Это просто файл JavaScript:

addEventListener('message', async (e) => {
	// initialize the Go WASM glue
	const go = new self.Go();

	// e.data contains the code from the main thread
	const result = await WebAssembly.instantiate(e.data, go.importObject);

	// hijack the console.log function to capture stdout
	let oldLog = console.log;
	// send each line of output to the main thread
	console.log = (line) => { postMessage({
		message: line
	}); };

	// run the code
	await go.run(result.instance);
	console.log = oldLog;

	// tell the main thread we are done
	postMessage({
		done: true
	});
}, false);

The worker communicates with the main thread by listening to message events, and sending data back via the postMessage function.

Примечание. Я пропустил файл wasm_exec.js, который необходим рабочему процессу для запуска кода Go, но его можно найти на вашем компьютере, если у вас установлен Go.

cat $(go env GOROOT)/misc/wasm/wasm_exec.js

Как это работает — Основная тема

Теперь, когда у нас есть рабочий файл, который может выполнять скомпилированную веб-сборку, давайте посмотрим, как основной поток взаимодействует с рабочим. Я создал модуль ES6, который экспортирует некоторые вспомогательные функции:

export function getWorker(lang) {
  return {
    webWorker: new window.Worker(`/${lang}_worker.js`),
    lang
  };
}

export function useWorker(worker, params, callback) {
  const promise = new Promise((resolve, reject) => {
    worker.webWorker.onmessage = (event) => {
      if (event.data.done) {
        resolve();
        return;
      }
      if (event.data.error) {
        reject(event.data.error);
        return;
      }
      callback(event.data.message);
    };
  });
  worker.webWorker.postMessage(params);
  return promise;
}

export function terminateWorker(worker) {
  worker.webWorker.terminate();
}

Когда страница загрузится, мы создадим новый веб-воркер, используя getWorker. Когда пользователь запускает некоторый код, мы отправляем код рабочему процессу, используя useWorker. Когда мы выходим из редактора кода, мы можем очистить рабочий процесс, используя terminateWorker.

Функция useWorker — самая интересная часть поста. Требуется рабочий процесс, созданный с помощью getWorker, объект с именем params, который будет передан рабочему процессу (он содержит скомпилированный WASM), и функция обратного вызова, которая будет выполняться, когда рабочий закончит работу.

Например, в нашем приложении Vue мы используем эти функции следующим образом:

this.output = [];
this.isLoading = true;
const wasm = await compileGo(this.code);
await useWorker(this.worker, wasm, (data) => {
  this.output.push(data); 
});
this.isLoading = false;

А поскольку this.output является реактивным свойством нашего экземпляра Vue, каждый раз, когда мы получаем данные от Web Worker, в консоль выводится новый вывод.

Спасибо за чтение!

Подпишитесь на нас в Твиттере @q_vault, если у вас есть какие-либо вопросы или комментарии

Пройдите несколько курсов кодирования на нашей новой платформе

Подпишитесь на нашу рассылку, чтобы получать больше статей по программированию