Я пытаюсь добиться следующего на веб-странице:
- Пользователи могут открывать несколько вкладок/окон страницы.
- Каждые несколько секунд мне нужна ровно одна из этих вкладок/окон для выполнения определенного раздела кода (критическая область).
- Меня не волнует, какая из вкладок/окон выполняет код, т.е. не нужно беспокоиться о свойствах справедливости или голодания решения.
- Поскольку пользователь сам открыл вкладки/окна, разные экземпляры страницы не знают и не имеют прямых ссылок друг на друга (т. е. нет 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);
postMessage
— это еще один способ заставить вкладки «общаться» друг с другом. Будет ли это работать для того, что вы планируете сделать, может зависеть от специфики того, что вам нужно «выполнить». - person CBroe   schedule 24.10.2015postMessage
требует ссылок на другие окна, верно? К сожалению, у меня их нет (см. здесь и пункт 4 в списке требований) - person Markus A.   schedule 24.10.2015