我正在尝试在网页中实现以下目标:
- 用户可以打开页面的多个选项卡/窗口。
- 每隔几秒钟,我就需要其中一个选项卡/窗口来执行特定的代码部分(关键区域)。
- 我不在乎哪个选项卡/窗口执行代码,即无需担心解决方案的公平性或饥饿属性。
- 由于用户自己打开了选项卡/窗口,因此页面的不同实例不了解或直接引用彼此(即没有 window.parent 等)
- 我不想需要 Flash 或 Silverlight 或其他插件,并且一切都需要在客户端运行,因此选项卡/窗口可以通信的方式非常有限(LocalStorage是迄今为止我发现的唯一一个,但可能成为其他人)。
- 任何选项卡/窗口都可能随时崩溃或关闭或刷新,并且还可以随时打开更多选项卡/窗口,并且剩余的窗口必须“做出反应”,这样我仍然可以每次执行一次关键区域几秒钟。
- 这需要在尽可能多的浏览器中可靠地运行,包括移动设备(caniuse评级超过 %90)。
我对解决方案的第一次尝试是使用一种简单的互斥算法,该算法使用 LocalStorage 作为共享内存。由于种种原因,我选择了 Burns 和 Lynch 在他们的论文“Mutual Exclusion Using Indivisible Reads and Writes”(第 4 页(836))中的互斥算法。
我构建了一个jsfiddle(见下面的代码)来尝试这个想法,它在 Firefox 中运行良好。如果您想尝试一下,请在 Firefox 的几个(最多 20 个)窗口中打开指向小提琴的链接,并观察其中一个窗口每秒闪烁橙色。如果您同时看到不止一个眨眼,请告诉我!:)(注意:我在小提琴中分配 ID 的方式有点俗气(简单地循环超过 0..19),只有在每个窗口都分配了不同的 ID 时,事情才会起作用。如果两个窗口显示相同的 ID,只需重新加载一个。)。
不幸的是,在 Chrome 中,尤其是在 Internet Explorer 中,事情并没有按计划工作(多个窗口闪烁)。我认为这是由于我写入 LocalStorage 的数据从一个选项卡/窗口到另一个的传播延迟(请参阅我的问题here)。
所以,基本上,我需要找到一种不同的互斥算法来处理延迟数据(听起来很困难/不可能),或者我需要找到一种完全不同的方法。也许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);