本地存储(localStorage)是否线程安全?

34

我想知道在两个浏览器标签页中同时重写localStorage条目可能会造成损坏的可能性。我是否应该为本地存储创建一个互斥体?
我已经在考虑这样的伪类:

LocalStorageMan.prototype.v = LocalStorageMan.prototype.value = function(name, val) {
  //Set inner value
  this.data[name] = val;
  //Delay any changes if the local storage is being changed
  if(localStorage[this.name+"__mutex"]==1) {
    setTimeout(function() {this.v(name, val);}, 1);
    return null;  //Very good point @Lightness Races in Orbit 
  }
  //Lock the mutext to prevent overwriting
  localStorage[this.name+"__mutex"] = 1;
  //Save serialized data
  localStorage[this.name] = this.serializeData;
  //Allow usage from another tabs
  localStorage[this.name+"__mutex"] = 0;
}

上述函数意味着本地存储管理器正在管理本地存储的一个特定键 - 例如 localStorage["test"]。我想在greasomonkey用户脚本中使用它,其中避免冲突是首要任务。


2
是的,它是线程安全的 - 当单个选项卡进行修改时,它还会向所有其他线程触发更改事件,因此您不需要手动轮询它。 - Benjamin Gruenbaum
5
您的自定义互斥锁实现不具备线程安全性。 - zerkms
1
还有@zerkms说的,你的锁定不是原子的,所以它不会引发线程问题。如果在互斥值被分配之前,我得到了两个线程,它们都会重新分配数据。 - Benjamin Gruenbaum
@BenjaminGruenbaum 关于“不安全”注释:我当然考虑过了 - 但我看不到绕过它的方法 - 所以我决定让它变得“更安全”。通常我认为分配INT比分配整个字符串更快。 - Tomáš Zato
@TomášZato,Lightness 的意思是无论状态如何,您都要继续“锁定”,分配和解锁。您在那里没有 else 或_早期_返回。此外,JS 中的函数隐式返回 undefined 而不是 null。 - Benjamin Gruenbaum
显示剩余8条评论
2个回答

41

是的,它是线程安全的。但是,你的代码不是原子的,那就是你的问题所在。我会讲到localStorage的线程安全问题,但首先要解决你的问题。

两个选项卡都可以通过if检查,并且互相覆盖写入项目。正确处理此问题的方法是使用StorageEvent

这些允许您在localStorage中的键已更改时通知其他窗口,以有效地以一种内置的消息传递安全方式为您解决该问题。 这是一个关于它们的很好的阅读资料。让我们举一个例子:

// tab 1
localStorage.setItem("Foo","Bar");

// tab 2
window.addEventListener("storage",function(e){
    alert("StorageChanged!"); // this will run when the localStorage is changed
});

现在,我来履行我关于线程安全的承诺 :)

就像我喜欢的方式一样——我们从规范和实现两个角度来观察。

规范

让我们从规范上证明它是线程安全的。

如果我们检查一下Web Storage的规范,我们可以看到它特别指出

由于使用了存储互斥锁,多个浏览上下文将能够同时访问本地存储区,使得脚本无法检测到任何并发脚本执行。

因此,在脚本执行时,Storage对象的长度属性和该对象的各种属性的值都不能更改,除了以脚本自身可预测的方式之外。

它甚至进一步详细说明:

每当要检查、返回、设置或删除localStorage属性的Storage对象的属性时,无论是作为直接属性访问的一部分,还是在检查属性存在性时,在枚举属性时,在确定存在的属性数时,还是作为执行Storage接口上定义的任何方法或属性的一部分时,用户代理必须首先获得存储互斥锁

这里强调了我的内容。它还指出,一些实现者不喜欢这个注释。

实践中

让我们从实现上证明它是线程安全的。

选择一个随机的浏览器,我选择了WebKit(因为我之前不知道那里的代码在哪里)。如果我们检查一下WebKit的Storage实现,我们可以看到它有很多互斥锁。

让我们从头开始。当你调用setItem或赋值时,会发生以下情况:

void Storage::setItem(const String& key, const String& value, ExceptionCode& ec)
{
    if (!m_storageArea->canAccessStorage(m_frame)) {
        ec = SECURITY_ERR;
        return;
    }

    if (isDisabledByPrivateBrowsing()) {
        ec = QUOTA_EXCEEDED_ERR;
        return;
    }

    bool quotaException = false;
    m_storageArea->setItem(m_frame, key, value, quotaException);

    if (quotaException)
        ec = QUOTA_EXCEEDED_ERR;
}

接下来,在 StorageArea 中发生了这件事:

void StorageAreaImpl::setItem(Frame* sourceFrame, const String& key, const String& value, bool& quotaException)
{
    ASSERT(!m_isShutdown);
    ASSERT(!value.isNull());
    blockUntilImportComplete();

    String oldValue;
    RefPtr<StorageMap> newMap = m_storageMap->setItem(key, value, oldValue, quotaException);
    if (newMap)
        m_storageMap = newMap.release();

    if (quotaException)
        return;

    if (oldValue == value)
        return;

    if (m_storageAreaSync)
        m_storageAreaSync->scheduleItemForSync(key, value);

    dispatchStorageEvent(key, oldValue, value, sourceFrame);
}

注意这里的blockUntilImportComplete。让我们来看一下:

void StorageAreaSync::blockUntilImportComplete()
{
    ASSERT(isMainThread());

    // Fast path.  We set m_storageArea to 0 only after m_importComplete being true.
    if (!m_storageArea)
        return;

    MutexLocker locker(m_importLock);
    while (!m_importComplete)
        m_importCondition.wait(m_importLock);
    m_storageArea = 0;
}

他们甚至还加了一张温馨的便条:

// FIXME: In the future, we should allow use of StorageAreas while it's importing (when safe to do so).
// Blocking everything until the import is complete is by far the simplest and safest thing to do, but
// there is certainly room for safe optimization: Key/length will never be able to make use of such an
// optimization (since the order of iteration can change as items are being added). Get can return any
// item currently in the map. Get/remove can work whether or not it's in the map, but we'll need a list
// of items the import should not overwrite. Clear can also work, but it'll need to kill the import
// job first.

解释如何运作,但可以更高效。


1
不幸的是,并非所有浏览器都遵循规范。是的,Chrome 在某个时候似乎已经解决了这个问题,但我最近在 2014 年 1 月进行了检查,在 IE11 中仍然存在此问题。 - balpha
1
不,我指的是能够同步读写LS,并确保我覆盖的内容正是我之前读取的内容。但我完全同意你关于storageEvent的看法(最近我在一次演讲中谈到了它)。当我写http://balpha.de/2012/03/javascript-concurrency-and-locking-the-html5-localstorage/时,存储事件在当前浏览器中尚不完整和可靠,但现在这真的不再是问题了。 - balpha
10
我不理解如何使用storageEvent可以解决并发问题。有人能否提供一个在localStorage中增加值并且由于使用storageEvents而没有并发问题的代码示例? - user1608790
2
@user1608790,这样的例子不存在。 - Benjamin Gruenbaum
3
JavaScript一次只能在特定页面上运行一个函数,因此不能同时存在两个函数。如果我没错的话,你无法提取然后存储提取值的递增版本,并确保在这两个步骤之间(实际上还有更多步骤,因为递增本身就是一步),另一个选项卡没有更改其值,否则可能会丢失至少1次递增。 - Micaël Félix
显示剩余10条评论

4
不是的。互斥锁已从规范中删除,取而代之的是添加了以下警告: localStorage getter 提供对共享状态的访问。本规范未定义与多进程用户代理中其他浏览上下文的交互,并鼓励作者假设没有锁定机制。例如,一个站点可能尝试读取一个键的值,增加其值,然后将其写回,使用新值作为会话的唯一标识符;如果该站点在同时使用两个不同的浏览器窗口进行此操作,则可能会使用相同的“唯一”标识符用于两个会话,从而产生潜在的灾难性影响。
请参见HTML规范:12 Web存储

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接