【问题标题】:Is localStorage thread safe?localStorage 线程安全吗?
【发布时间】:2014-03-26 21:41:19
【问题描述】:

我很好奇通过同时在两个浏览器选项卡中覆盖 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 用户脚本。

【问题讨论】:

  • 是的,它是线程安全的 - 当单个选项卡进行修改时,它还会向所有其他线程触发更改事件,因此您无需手动轮询它。
  • 您的自定义互斥锁实现不是线程安全的。
  • 正如@zerkms 所说,你的锁定不是原子的,所以它不是线程问题。如果我在分配互斥锁值之前在if 之后得到两个线程怎么办?他们都会重新分配数据。
  • @BenjaminGruenbaum 关于“不安全”的说明:我当然想过这个问题——但我认为没有办法解决这个问题——所以我决定让它“更安全” >”。一般来说,我认为分配 INT 比分配整个字符串要快。
  • 设置超时后你也不要return

标签: javascript multithreading local-storage


【解决方案1】:

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

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

这些可以让您在 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

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

因此,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.

解释这行得通,但它可能更有效。

【讨论】:

  • 再读一遍,这里没有解释——我在这里提到的锁定是第一次使用 localStorage 时,将不同的选项卡同步在一起的实际事情是 SQLite,它在这里处理并发。
  • 不幸的是,并非所有浏览器都遵循规范。是的,Chrome 似乎在某个时候修复了它,但我最近在 2014 年 1 月检查过,它仍然是 IE11 中的一个问题。
  • 我不明白使用 storageEvent 如何帮助解决并发问题。任何人都可以提供一个代码示例,该代码在 localStorage 中增加一个值并且由于使用 storageEvents 而没有并发问题?
  • @user1608790 这样的例子不存在。
  • ...不存在? Javascript 一次只为特定页面工作一个功能。如果我没记错你不能提取然后存储提取值的增量版本,并确保在两个步骤之间(更多,因为增量是它自己的一个步骤)另一个选项卡没有改变它的值,并且您可能会损失至少 1 个增量。
猜你喜欢
  • 1970-01-01
  • 2020-04-15
  • 2011-07-04
  • 2014-04-26
  • 2012-11-30
  • 2010-12-30
  • 2013-03-12
  • 2021-08-03
  • 2010-12-27
相关资源
最近更新 更多