zoukankan      html  css  js  c++  java
  • [Effective JavaScript 笔记]第66条:使用计数器来执行并行操作

    第63条建议使用工具函数downloadAllAsync接收一个URL数组并下载所有文件,结果返回一个存储了文件内容的数组,每个URL对应一个字符串。downloadAllAsync并不只有清理嵌套回调函数的好处,其主要好处是并行下载文件。我们可以在同一个事件循环中一次启动所有文件的下载,而不用等待每个文件完成下载。
    并行逻辑是微妙的,很容易出错。下面有实现有一个隐藏的缺陷。

    function downloadAllAsync(urls,onsuccess,onerror){
      var result=[],length=urls.length;
      if(length === 0){
        setTimeout(onsuccess.bind(null,result),0);
      }
      urls.forEach(function(url){
        downloadAsync(url,function(text){
          if(result){
            reslut.push(text);
            if(result.length===urls.length){
              onsuccess(result);
            }
          }
        },function(error){
          if(result){
            result=null;
            onerror(error);
          }
        });
      });
    }
    

    这个函数有严重的错误,但首先让我们看看它是如何工作的。先确保如果数组是空的,则会使用空结果数组调用回调函数。如果不这样做,这两个回调函数将不会被调用,因为forEach循环是空的。接下来,遍历整个URL数组,为每个URL请求一个异步下载。每次下载成功,就将文件内容加入到result数组中。如果所有URL都被成功下载,使用result数组调用onsuccess回调函数。如果有任何失败的下载,使用错误值调用onerror回调函数。如果有多个下载失败,设置result数组为null,从而保证onerror只被调用一次,即在第一次错误发生时。
    错误示例

    var filenames=[
      'huge.txt',
      'tiny.txt',
      'medium.txt'
    ];
    downloadAllAsync(filenames,function(files){
      console.log('Huge file:'+files[0].length);//tiny
      console.log('Tiny file:'+files[1].length);//medium
      console.log('Medium file:'+files[2].length);//huge
    },function(error){
      console.log('Error: '+error);
    });
    

    由于这些文件是并行下载的,事件可以以任意的顺序发生(因些被添加到应用程序事件序列)。例如,如果tiny.txt先下载完成,接下来是medium.txt文件,最后是buge.txt文件,则注册到downloadAllAsync的回调函数并不会按照它们被创建的顺序进行调用。但downloadAllAsync的实现是一旦下载完成就立即将中间结果保存在result数组的末尾。所以downloadAllAsync函数提供的保存下载文件内容的数组的顺序是未知的。这个API几乎不可用,因为无法确认哪个结果对应哪个文件。
    程序的执行顺序不能保证与事件发生的顺序一致。
    当一个应用程序依赖于特定的事件顺序才能正常工作时,这个程序会遭受数据竞争。数据竞争是指多个并发操作可以修改共享的数据结构,这取决于它们发生的顺序。数据竞争是真正棘手的错误。它们可能不会出现于特定的测试中,因为运行相同的程序两次,每次可能会得不到不同的结果。例如downloadAllAsync的使用者可能会对文件重新排序,基于的顺序是哪个文件可能会最先完成下载。

    downloadAllAsync(filenames,function(files){
      console.log('Huge file:'+files[2].length);
      console.log('Tiny file:'+files[0].length);
      console.log('Medium file:'+files[1].length);
    },function(error){
      console.log('Error: '+error);
    });
    

    在这种情况下大多数时候结果是相同的顺序,但偶尔由于改变了服务器负载均衡或网络缓存,文件可能不是期望的顺序。我们可以顺序下载文件,但也失去了并发的性能优势。
    下面实现downloadAllAsync不依赖不可预期的事件执行顺序而总能提供预期结果。我们不将每个结果放置到数组末尾,而是存储在其原始的索引位置。

    function downloadAllAsync(urls,onsuccess,onerror){
      var result=[],length=urls.length;
      if(length === 0){
        setTimeout(onsuccess.bind(null,result),0);
        return;
      }
      urls.forEach(function(url){
        downloadAsync(url,function(text){
          if(result){
            reslut[i]=text;
            if(result.length===urls.length){
              onsuccess(result);
            }
          }
        },function(error){
          if(result){
            result=null;
            onerror(error);
          }
        });
      });
    }
    

    该实现利用了forEach回调函数的第二个参数。第二个参数为当前迭代提供了数组索引。这也不正确。第51条描述数组更新的契约,即设置一个索引属性,总是确保数组的length属性值大于索引。假设有如下的一个请求。

    downloadAllAsync(['huge.txt','medium.txt','tiny.txt']);
    

    如果tiny.txt文件最先被下载,结果数组将获取索引为2的属性,这将导致result.length被更新为3。用户的success回调函数将被过早地调用,其参数为一个不完整的结果数组。
    正确的实现应该是使用一个计数器来追踪正在进行的操作数量。

    function downloadAllAsync(urls,onsuccess,onerror){
      var pending=urls.length;
      var result=[];
      if(pending === 0){
        setTimeout(onsuccess.bind(null,result),0);
        return;
      }
      urls.forEach(function(url){
        downloadAsync(url,function(text){
          if(result){
            reslut[i]=text;
            pending--;
            if(pending===0){
              onsuccess(result);
            }
          }
        },function(error){
          if(result){
            result=null;
            onerror(error);
          }
        });
      });
    }
    

    现在不论事件以什么样的顺序发生,pending计数器都能准确地指出何时所有的事件会被完成,并以适当的顺序返回完整的结果。

    提示

    • js应用程序中的事件发生是不确定的,即顺序是不可预测的

    • 使用计数器避免并行操作中的数据竞争

  • 相关阅读:
    二分法
    The Distinguish of the share or static lib in MFC
    内部或外部命令
    The Memory Managerment of the Computer
    AfxWinInit
    NoSQL解决方案比较
    修改服务中可执行文件的路径
    MapReduce 笔记
    认识MongoDB
    Add a Console Application
  • 原文地址:https://www.cnblogs.com/wengxuesong/p/5718098.html
Copyright © 2011-2022 走看看