Синхронизация между окнами (критические разделы) в браузере

Я пытаюсь добиться следующего на веб-странице:

  • Пользователи могут открывать несколько вкладок/окон страницы.
  • Каждые несколько секунд мне нужна ровно одна из этих вкладок/окон для выполнения определенного раздела кода (критическая область).
  • Меня не волнует, какая из вкладок/окон выполняет код, т.е. не нужно беспокоиться о свойствах справедливости или голодания решения.
  • Поскольку пользователь сам открыл вкладки/окна, разные экземпляры страницы не знают и не имеют прямых ссылок друг на друга (т. е. нет window.parent и т. д.).
  • Я не хочу требовать Flash, Silverlight или другие подключаемые модули, и все должно работать на стороне клиента, поэтому способы взаимодействия между вкладками и окнами очень ограничены (LocalStorage — пока единственное, что я нашел, но могут быть и другие).
  • Любая из вкладок/окон может аварийно завершить работу, закрыться или обновиться в любое время, а также в любое время можно открыть дополнительные вкладки/окна, а оставшиеся окна должны «реагировать» таким образом, чтобы я по-прежнему получал ровно одно выполнение критической области каждые несколько секунд.
  • Это должно надежно работать в как можно большем количестве браузеров, включая мобильные (caniuse — рейтинг более %90).

Моей первой попыткой решения было использование простого алгоритма взаимного исключения, использующего LocalStorage в качестве разделяемой памяти. По по разным причинам я выбрал алгоритм взаимного исключения Бернса и Линча из их статьи "Взаимное исключение с использованием неделимых операций чтения и записи" (стр. 4 (836)).

Я создал jsfiddle (см. код ниже), чтобы опробовать эту идею, и она прекрасно работает в Firefox. Если хотите попробовать, откройте ссылку на скрипку в нескольких (до 20) окнах Firefox и наблюдайте, как ровно одно из них мигает оранжевым каждую секунду. Если вы видите более одного мигания одновременно, дайте мне знать! :) (Примечание: способ, которым я назначаю идентификаторы в скрипке, немного неуклюж (просто зацикливается на 0..19), и все будет работать, только если каждому окну был назначен другой идентификатор. Если два окна показывают один и тот же идентификатор, просто перезагрузить один.).

К сожалению, в Chrome и особенно в Internet Explorer все работает не так, как планировалось (мигает несколько окон). Я думаю, это связано с задержкой распространения данных, которые я пишу в LocalStorage, с одной вкладки/окна на другую (см. мой вопрос по этому поводу здесь).

Итак, в основном мне нужно найти либо другой алгоритм мьютекса, который может обрабатывать задержанные данные (звучит сложно/невозможно), либо мне нужно найти совершенно другой подход. Может быть, StorageEvents может помочь? Или, может быть, есть другой механизм, который не использует LocalStorage?

Для полноты вот код скрипки:

// Global constants
var LOCK_TIMEOUT =  300; // Locks time out after 300ms
var INTERVAL     = 1000; // Critical section should run every second



//==================================================================================
// Assign process ID

var myID;
id = window.localStorage.getItem("id");

if (id==null) id = 0;
id = Number(id);
myID = id;
id = (id+1) % 20;
window.localStorage.setItem("id", id);

document.documentElement.innerHTML = "ID: "+myID;



//==================================================================================
// Method to indicate critical section

var lastBlink = 0;
function blink() {
    col = Math.round(Math.min((new Date().getTime() - lastBlink)*2/3, 255));
    document.body.style.backgroundColor = "rgb(255, "+((col >> 1)+128)+", "+col+")";
}



//==================================================================================
// Helper methods to implement expiring flags

function flagUp() {
    window.localStorage.setItem("F"+myID, new Date().getTime());
}

function flagDown() {
    window.localStorage.setItem("F"+myID, 0);
}

// Try to refresh flag timeout and return whether we're sure that it never expired
function refreshFlag() {
    content = window.localStorage.getItem("F"+myID);
    if (content==null) return false;
    content = Number(content);
    if ((content==NaN) || (Math.abs(new Date().getTime() - content)>=timeout))
        return false;
    window.localStorage.setItem("F"+myID, new Date().getTime());
    return Math.abs(new Date().getTime() - content) < timeout;
}    

function setFlag(key) {
    window.localStorage.setItem(key, new Date().getTime());
}

function checkFlag(key, timeout) {
    content = window.localStorage.getItem(key);
    if (content==null) return false;
    content = Number(content);
    if (content==NaN) return false;
    return Math.abs(new Date().getTime() - content) < timeout;
}



//==================================================================================
// Burns-Lynch mutual exclusion algorithm

var atLine7 = false;

function enterCriticalRegion() {

    // Refresh flag timeout and restart algorithm if flag may have expired
    if (atLine7) atLine7 &= refreshFlag();

    // Check if run is due
    if (checkFlag("LastRun", INTERVAL)) return false;

    if (!atLine7) {
        // 3: F[i] down
        flagDown();

        // 4: for j:=1 to i-1 do if F[j] = up goto 3
        for (j=0; j<myID; j++)
            if (checkFlag("F"+j, LOCK_TIMEOUT)) return false;

        // 5: F[i] up
        flagUp();

        // 6: for j:=1 to i-1 do if F[j] = up goto 3
        for (j=0; j<myID; j++)
            if (checkFlag("F"+j, LOCK_TIMEOUT)) return false;

        atLine7 = true;
    }

    // 7: for j:=i+1 to N do if F[j] = up goto 7
    for (j=myID+1; j<20; j++)
        if (checkFlag("F"+j, LOCK_TIMEOUT)) return false;

    // Check again if run is due
    return !checkFlag("LastRun", INTERVAL);
}

function leaveCriticalRegion() {
    // Remember time of last succesful run
    setFlag("LastRun");

    // Release lock on critical region
    atLine7 = false;
    window.localStorage.setItem("F"+myID, 0);
}



//==================================================================================
// Keep trying to enter critical region and blink on success

function run() {
    if (enterCriticalRegion()) {
        lastBlink = new Date().getTime();
        leaveCriticalRegion();
    }
}

// Go!
window.setInterval(run,   10);
window.setInterval(blink, 10);

person Markus A.    schedule 23.10.2015    source источник
comment
postMessage — это еще один способ заставить вкладки «общаться» друг с другом. Будет ли это работать для того, что вы планируете сделать, может зависеть от специфики того, что вам нужно «выполнить».   -  person CBroe    schedule 24.10.2015
comment
@CBroe Если я правильно понимаю, postMessage требует ссылок на другие окна, верно? К сожалению, у меня их нет (см. здесь и пункт 4 в списке требований)   -  person Markus A.    schedule 24.10.2015
comment
Да, мой плохой. Тем не менее, не могли бы вы немного рассказать о том, что вам на самом деле нужно выполнять каждые несколько секунд, и почему это так важно, что это происходит только с одной конкретной вкладки?   -  person CBroe    schedule 24.10.2015
comment
@CBroe На самом деле у меня есть пара разных вариантов использования, но тот, над которым я сейчас работаю, таков: у меня есть несколько вкладок / окон, постоянно генерирующих данные (пара событий в секунду), которые необходимо отправить на сервер. Чтобы сохранить пропускную способность для себя и пользователей, я хотел бы собирать эти данные, сохранять их в LocalStorage и периодически загружать в пакетном режиме, а не отправлять один запрос на сервер для каждого события.   -  person Markus A.    schedule 24.10.2015
comment
Имеет ли значение, что данные отправляются с «точными» интервалами, или плюс/минус несколько секунд не будут проблемой? Будет ли иметь значение, если данные будут отправляться дважды изредка?   -  person CBroe    schedule 24.10.2015
comment
Вы пробовали что-то вроде того, чтобы одна вкладка помечала себя как «активную» для этого процесса и сохраняла эту информацию в localStorage (например, через случайное имя вкладки, сгенерированное при загрузке), а затем все остальные вкладки смотрели, есть ли активный один - если да, то они позволяют ему взять на себя задачу отправки данных, а если не первый, то один из других делает себя новым активным. В сочетании с отметкой времени, которую активная вкладка устанавливает, чтобы отметить, когда она в последний раз выполняла задачу, другие могут определить, была ли активная вкладка все еще там или, возможно, была закрыта пользователем.   -  person CBroe    schedule 24.10.2015
comment
@CBroe Интервалы не обязательно должны быть точными. Это даже нормально, если обновление иногда пропускается.   -  person Markus A.    schedule 24.10.2015
comment
@CBroe Я пытался придумать способ сделать это, да. Но все не так тривиально, как я думал вначале. Вам по-прежнему необходимо реализовать какой-то алгоритм взаимного исключения между вкладками/окнами, чтобы гарантировать надежную работу. К сожалению, ни при каких обстоятельствах не допустимо, чтобы два процесса вошли в критическую секцию одновременно, так как это, скорее всего, испортит данные в LocalStorage, и я потеряю события.   -  person Markus A.    schedule 24.10.2015
comment
@CBroe Если несколько вкладок/окон загружаются одновременно (все ожидают загрузки, а сетевое соединение только что восстановлено, или пользователь повторно открыл предыдущий сеанс браузера со всеми вкладками), не так просто надежно определить, какая вкладка будет ответственным.   -  person Markus A.    schedule 24.10.2015
comment
Тогда, может быть, какая-то «блокировка»/семафор в локальном хранилище? Есть ли вкладка, которая хочет записывать/манипулировать данными в LS, сначала получает блокировку, и если блокировка уже установлена, то это означает, что она должна ждать и повторять попытку через короткое время (setTimeout) …?   -  person CBroe    schedule 24.10.2015
comment
@CBroe Да. Это именно то, что вышеприведенный алгоритм пытается сделать. :) Но, учитывая задержку распространения данных в LocalStorage, я не знаю ни одного семафора или алгоритма блокировки, который мог бы надежно справиться с ситуацией.   -  person Markus A.    schedule 24.10.2015