JavaScript中的线程安全性?

34
我有一个名为save()的函数,该函数收集页面上所有的输入,并执行AJAX调用以保存用户工作状态。当前,当用户单击保存按钮或执行某些需要我们在服务器上具有最新状态的操作(例如从页面生成文档)时,会调用save()。
我正在添加定期自动保存用户工作的功能。首先,我想防止自动保存和用户生成的保存同时运行。因此,我们有以下代码(我省略了大部分代码,这不是一对一的,但应足以传达思想):
var isSaving=false;
var timeoutId;
var timeoutInterval=300000;
function save(showMsg)
{
  //Don't save if we are already saving.
  if (isSaving)
  { 
     return;
  }
  isSaving=true;
  //disables the autoSave timer so if we are saving via some other method
  //we won't kick off the timer.
  disableAutoSave();

  if (showMsg) { //show a saving popup}
  params=CollectParams();
  PerformCallBack(params,endSave,endSaveError);

}
function endSave()
{  
    isSaving=false;
    //hides popup if it's visible

    //Turns auto saving back on so we save x milliseconds after the last save.
    enableAutoSave();

} 
function endSaveError()
{
   alert("Ooops");
   endSave();
}
function enableAutoSave()
{
    timeoutId=setTimeOut(function(){save(false);},timeoutInterval);
}
function disableAutoSave()
{
    cancelTimeOut(timeoutId);
}

我的问题是这段代码是否安全?主流浏览器是否只允许同时执行单个线程?

我想到的一个想法是,如果我们正在自动保存数据,用户点击保存却没有响应会更糟糕(我知道如何修改代码来解决这个问题)。还有其他问题吗?


那么基于这个,如果用户点击保存,计时器在保存函数完成之前不会触发? - JoshBerke
1
如果我理解正确,您正在询问用户是否已启动保存操作,setTimeout 启动的保存操作是否会进入同一方法。答案是否定的,它们不会同时进入 save() 方法。 - Jonathon Faust
由于这些信息有点过时,我们该怎么办?JavaScript现在有多个线程:https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API/Using_web_workers - jamesmortensen
@jmort - 没有问题。WebWorkers将脚本文件作为其输入,并提供线程间通信的手段。这个特定的问题不会受到WebWorkers的影响。 - Josh Smeaton
5个回答

46
在浏览器中,JavaScript是单线程的。您在任何时候只能处于一个函数中。函数将在下一个进入之前完成。您可以依赖此行为,因此,如果您在save()函数中,则在当前函数完成之前,您永远不会再次进入它。
当您有异步服务器请求(或setTimeouts或setIntervals)时,这有时会令人困惑(但仍然是真实的),因为那时感觉像您的函数被交错了。它们没有。
在您的情况下,虽然两个save()调用不会重叠,但您的自动保存和用户保存可能会连续发生。
如果您只想每x秒至少发生一次保存,则可以在保存函数上进行setInterval并忘记它。我不认为需要isSaving标志。
我认为您的代码可以大大简化:
var intervalTime = 300000;
var intervalId = setInterval("save('my message')", intervalTime);
function save(showMsg)
{
  if (showMsg) { //show a saving popup}
  params=CollectParams();
  PerformCallBack(params, endSave, endSaveError);

  // You could even reset your interval now that you know we just saved.
  // Of course, you'll need to know it was a successful save.
  // Doing this will prevent the user clicking save only to have another
  // save bump them in the face right away because an interval comes up.
  clearInterval(intervalId);
  intervalId = setInterval("save('my message')", intervalTime);
}

function endSave()
{
    // no need for this method
    alert("I'm done saving!");
}

function endSaveError()
{
   alert("Ooops");
   endSave();
}

是的,由于我的保存函数中有异步调用,使用isSaving标志应该让我防止其他人进入函数,直到我的保存结束运行...对吗? - JoshBerke
1
@Josh 我更新了一下关于isSaving标志的答案。 setTimeout的作用只是在未来的某个时间点调度一个函数调用,至少要等待x毫秒。这并不意味着它会放弃当前正在做的事情并运行您的函数 -- 您计划中的保存操作不会与其他函数同时进行。 - Jonathon Faust
由于保存操作会向我们的服务器发起异步调用,因此 isSaving 变量会阻止保存操作直到 EndSave 函数被调用。 - JoshBerke
当用户发起保存操作时,我们会弹出一种半透明的面板,阻止用户进行任何操作。我更担心的是,当用户执行某些导致保存操作发生的操作时,自动保存会运行。 - JoshBerke
@Anurag,我没有参考资料,因为它在ECMA规范中没有定义。从JavaScript内部来看,您可以假设没有两个函数同时运行。除非您谈论的是像Web Workers这样的新东西。John Resig在这里有一些很好的解释:http://ejohn.org/blog/how-javascript-timers-work - Jonathon Faust
显示剩余2条评论

7

所有主流浏览器只支持一个javascript线程(除非您使用Web Workers)。

XHR请求可以是异步的。但只要禁用保存功能,直到当前保存请求返回,一切都应该正常工作。

我唯一的建议是,请确保以某种方式向用户指示何时发生自动保存(禁用保存按钮等)。


是的,正在考虑向用户指示此功能的方法。非常好的建议。 - JoshBerke
如果您拥有Gmail帐户,您会注意到他们在自动保存草稿时的方法是将“立即保存”按钮变灰并将文本更改为“正在保存”,然后变为“已保存”。只有当用户开始更改草稿时,“立即保存”按钮才会恢复可点击状态。 - mikefrey

2

目前所有主流浏览器都是单线程执行JavaScript(只要不使用Web Workers,因为只有少数浏览器支持这种技术!),因此这种方法是安全的。

有关一堆参考资料,请参见JavaScript是否支持多线程


Web workers的支持程度如何?看起来Firefox 3.5+还没有找到显示此功能的浏览器支持列表。 - JoshBerke
我不是完全确定。据John Resig(这里-http://ejohn.org/blog/web-workers/)的说法,FireFox 3.5和Safari 4(截至2009年7月29日)。他还为构建代码结构的情况辩护,使其在支持时使用网络工作者(如果我理解正确的话)。此外,它们似乎是HTML 5规范的一部分(http://www.whatwg.org/specs/web-workers/current-work/)。 - Jeff Sternal

2

1

我认为你处理它的方式最适合你的情况。通过使用标志,你可以确保异步调用不会重叠。我也曾经处理过对服务器的异步调用,也使用了某种类型的标志来防止重叠。

正如其他人已经指出的那样,JavaScript 是单线程的,但是如果你期望在往返服务器期间事情保持相同或不发生变化,则异步调用可能会很棘手。

不过,有一件事是我认为你实际上并不需要禁用自动保存。如果自动保存尝试在用户保存时发生,则保存方法将简单地返回,什么也不会发生。另一方面,每次激活自动保存时你都不必要地禁用和重新启用自动保存。我建议改为使用 setInterval,然后忘记它。

此外,我非常注重减少全局变量。我可能会像这样重构你的代码:

var saveWork = (function() {
  var isSaving=false;
  var timeoutId;
  var timeoutInterval=300000;
  function endSave() {  
      isSaving=false;
      //hides popup if it's visible
  }
  function endSaveError() {
     alert("Ooops");
     endSave();
  }
  function _save(showMsg) {
    //Don't save if we are already saving.
    if (isSaving)
    { 
     return;
    }
    isSaving=true;

    if (showMsg) { //show a saving popup}
    params=CollectParams();
    PerformCallBack(params,endSave,endSaveError);
  }
  return {
    save: function(showMsg) { _save(showMsg); },
    enableAutoSave: function() {
      timeoutId=setInterval(function(){_save(false);},timeoutInterval);
    },
    disableAutoSave: function() {
      cancelTimeOut(timeoutId);
    }
  };
})();

当然,您不必像那样重构它,但像我说的那样,我喜欢尽量减少全局变量。重要的是整个程序应该在每次保存时都能正常工作,而无需禁用和重新启用自动保存。

编辑:忘记了必须创建一个私有的保存函数才能从 enableAutoSave 中引用。


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