localStorage线程安全吗?

时间:2014-02-24 23:06:53

标签: javascript multithreading local-storage

我很好奇通过在两个浏览器标签中同时覆盖它来损坏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用户脚本,其中避免冲突是一个优先事项。

1 个答案:

答案 0 :(得分:29)

是的,它是线程安全的。但是,您的代码不是原子的,那是你的问题。我将解决localStorage的安全问题,但首先是如何解决问题。

两个标签都可以将if检查一起传递并写入相互覆盖的项目。处理此问题的正确方法是使用StorageEvent

这些允许您在localStorage中更改密钥时通知其他窗口,在内置消息中以安全的方式有效地解决问题。 Here is a nice read about them。我们举个例子:

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

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

现在,我对线程安全的承诺:)

我喜欢 - 让我们从两个角度来看 - 从规范和使用实现。

规范

让我们通过规范显示它的线程安全。

如果我们检查specification of Web Storage,我们可以看到它specifically notes

  

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

     

因此,存储对象的length属性以及该对象的各种属性的值在脚本执行时不能更改,而不是以脚本本身可预测的方式。

它甚至进一步阐述:

  

每当检查,返回,设置或删除localStorage属性的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.

解释这是有效的,但它可以更有效。