Синхронизация окон (критические разделы) в браузере
Я пытаюсь добиться следующего на веб-странице:
- Пользователи могут открывать несколько вкладок / окон страницы.
- Каждые несколько секунд мне нужна ровно одна из этих вкладок / окон для выполнения определенной части кода (критическая область).
- Мне все равно, какая из вкладок / окон выполняет код, т.е. не нужно беспокоиться о свойствах справедливости или недостатка решения.
- Так как пользователь сам открыл вкладки / окна, разные экземпляры страницы не знают или не имеют прямых ссылок друг на друга (т.е. нет окна. Родитель и т. Д.)
- Я не хочу требовать 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);