JavaScript中的多线程

好吧,在我们开始之前,让我整理一下,承认这篇文章的标题有点耸人听闻! JavaScript实际上并没有多线程功能,因此JavaScript程序员无法做任何改变。 在所有浏览器中(除了Google Chrome浏览器),JavaScript都在单个执行线程中运行,事实就是如此。

但是,我们可以做的是模拟多线程,只要它产生了多线程环境的好处之一: 它使我们可以运行非常密集的代码 这是代码,否则将冻结浏览器并在Firefox中生成那些“无响应脚本”警告之一。

时间不等人

这一切都取决于异步计时器的使用。 当我们在异步计时器中运行重复代码时,我们将给浏览器的脚本解释器时间来处理每次迭代。

实际上, for迭代器中的一段代码要求解释器立即执行所有操作:“尽可能快地运行此代码n次。” 但是,异步计时器中的相同代码会将代码分解为细小的离散块。 也就是说,“尽可能快地运行此代码”,然后等待-然后“尽可能快地运行此代码”,依此类推, n次。

诀窍在于,每次迭代中的代码都足够小且足够简单,以使解释器可以在计时器的速度(100或5,000毫秒)内完全处理它。 如果满足该要求,那么整个代码的强度就没关系,因为我们不要求立即运行所有代码。

“太强烈”有多强烈?

通常,如果我编写的脚本过于繁琐,我会考虑对其进行重新设计。 如此大的速度下降通常表示代码有问题,或者应用程序设计有更深层的问题。

但有时并非如此。 有时,根本无法避免某种特定操作的强度,除非根本不使用JavaScript。

在给定的情况下,这可能是最好的解决方案。 也许需要将应用程序中的某些处理移到服务器端,在服务器端,它通常具有真正的线程执行环境(Web服务器)才能具有更大的处理能力。

但是最终您可能会发现,这不是一个选择,而是JavaScript 一定必须能够做某事或被该死。 这就是我开发Firefox扩展程序Dust-Me Selectors时遇到的情况。

该扩展程序的核心是能够测试应用于页面的CSS选择器,以查看它们是否被实际使用。 其本质是使用Dean Edwards的base2中的matchAll()方法进行的一组评估:

for(var i=0; i
{ 
 if(base2.DOM.Document.matchAll
   (contentdoc, selectors[i]).length > 0)
 {
   used ++;
 }
 else
 {
   unused ++;
 }
}

肯定很简单。 但是matchAll()本身非常matchAll() ,它具有-解析和评估任何CSS1或CSS2选择器,然后遍历整个DOM树以寻找匹配项的能力。 扩展名针对每个单独的选择器执行此操作 ,其中可能有数千个选择器 从表面上看,该过程非常简单,可能会非常密集,以至于整个浏览器在运行时都会冻结。 这就是我们所发现的。

锁定浏览器显然不是一种选择,因此,如果要使其完全起作用,我们必须找到一种使它运行无误的方法。

一个简单的测试用例

让我们用一个涉及两个迭代级别的简单测试案例来演示该问题。 内层故意过分密集,因此我们可以创建竞争条件,而外层则较短,因此它可以模拟主代码。 这就是我们所拥有的:

function process() 
{
 var above = 0, below = 0;
 for(var i=0; i<200000; i++)
 {
   if(Math.random() * 2 > 1)
   {
     above ++;      
   }
   else
   {
     below ++;
   }
 }
}


function test1()
{
 var result1 = document.getElementById('result1');
 
 var start = new Date().getTime();
   
 for(var i=0; i<200; i++)
 {
   result1.value =  'time=' +  
     (new Date().getTime() - start) + ' [i=' + i + ']';
   
   process();
 }
 
 result1.value = 'time=' +  
   (new Date().getTime() - start) + ' [done]';
}

我们以简单的形式开始测试,并获得输出(这是测试代码,而不是生产代码,请原谅我诉诸使用内联事件处理程序):


 

   
   
 


现在,让我们在Firefox中运行该代码(在本例中为2GHz MacBook上的Firefox 3)…并按预期,浏览器UI在运行时冻结(例如,无法按刷新并放弃该过程)。 经过大约90次迭代后,Firefox会生成“无响应脚本”警告对话框。

如果我们允许它继续,则在经过90次迭代之后,Firefox再次生成相同的对话框。

Safari 3和Internet Explorer 6在这方面的行为类似,具有冻结的UI和生成警告对话框的阈值。 在Opera中,没有这样的对话框-它只会继续运行代码直到完成-但浏览器的用户界面同样会冻结,直到任务完成。

显然,我们在实践中不能运行这样的代码。 因此,让我们对其进行重构,并对外部循环使用异步计时器:

function test2() 
{
 var result2 = document.getElementById('result2');
 
 var start = new Date().getTime();
 
 var i = 0, limit = 200, busy = false;
 var processor = setInterval(function()
 {
   if(!busy)
   {
     busy = true;
     
     result2.value =  'time=' +  
       (new Date().getTime() - start) + ' [i=' + i + ']';
     
     process();
     
     if(++i == limit)
     {
       clearInterval(processor);

       result2.value = 'time=' +  
         (new Date().getTime() - start) + ' [done]';
     }
     
     busy = false;
   }
   
 }, 100);
 
}

现在,让我们再次运行它……这次,我们收到了完全不同的结果。 当然,代码需要花费一些时间才能完成,但是它一直成功地运行到最后,而不会冻结UI,也不会警告脚本过慢。

查看测试页

busy标志用于防止计时器实例冲突。如果在下一次迭代到来时我们已经在子流程的中间,我们只需等待下一个迭代,从而确保只有一个子流程一次运行。)

所以你看,虽然我们可以在内部流程做的工作仍然很少, 次数 ,我们可以运行过程现在已经无限:我们基本上可以永远运行外循环,浏览器将不会冻结。

这更像是–我们可以在野外使用它。

你疯了!

我已经听到反对者的声音。 实际上,我可能是我自己一个人:您为什么要这样做?什么样的疯子坚持将JavaScript推向从未设计过的所有这些地方? 您的代码太紧张了。 这是工作的错误工具。 如果您必须克服这些麻烦,那么应用程序的设计从根本上是错误的。

我已经提到了一个示例,在该示例中,我必须找到一种使繁重的脚本工作的方法。 要么就是这样,要么整个想法都必须放弃。 如果您不满意该答案,那么本文的其余部分也可能不会吸引您。

但是,如果您(或者至少是如果您愿意接受说服),那么下面的另一个例子确实可以帮助您使用JavaScript编写可以在计算机上玩的游戏

游戏开始

我在这里谈论的是理解游戏规则所需的代码,然后该代码可以评估情况和战术,以便在该游戏中击败您。 复杂的东西。

为了说明这一点,我将看一个我已经开发了一段时间的项目。 “短暂”是指三年 ,其中大部分时间花在了理论上可以玩的高原上,但由于过于紧张而无法使用……直到我想到这种方法。 游戏是基于颜色和形状匹配的竞争难题。

总结一下:您可以通过相邻的形状和颜色匹配来实现自己的目标。 例如,如果您从一个绿色三角形开始,则可以移动到任何其他三角形或任何其他绿色形状。 您的目标是到达中间的水晶,然后将其带到棋盘的另一侧,而您的对手也尝试这样做。 您也可以从对手那里偷水晶。

因此,我们有确定运动的逻辑规则,并且还可以看到战术的出现。 例如,为避免让对手到达水晶或从您那里窃取水晶,您可以选择阻止他们的举动,或尝试在他们无法到达的地方完成比赛。

计算机的工作是在任何给定情况下找到最佳移动,所以让我们以摘要伪代码看一下该过程:

function compute()  
{  
 var move = null;  
   
 move = tactic1();  
 if(!move) { move = tactic2(); }  
 if(!move) { move = tactic3(); }  
   
 if(move)  
 {  
   doit();  
 }  
 else  
 {  
   pass();  
 }  
}

我们评估一种策略,如果这能给我们一个很好的行动,那么我们就完成了; 否则,我们将评估另一种策略,依此类推,直到我们采取行动或得出结论认为没有策略并且必须通过。

这些战术功能中的每一个都运行一个昂贵的过程,因为它必须评估董事会中的每个职位以及潜在的未来职位,鉴于各种因素,每个职位可能需要多次评估。 该示例仅包含三种策略,但是在实际游戏中,存在数十种不同的可能性,每种可能性的评估成本都很高。

这些评估中的任何一个单独都可以,但是所有这些评估一起连续运行会导致过于紧张的过程冻结浏览器。

因此,我所做的就是将主代码拆分为谨慎的任务 ,每个任务都通过switch语句进行选择,然后使用异步计时器进行迭代。 这样做的逻辑与我小时候曾经选择过的《选择自己的冒险》书相距一百万英里,在本书中,每个任务以实时选择其他任务结束,直到完成为止:

function compute()  
{  
 var move = null;  
   
 var busy = false, task = 'init';  
 var processor = setInterval(function()  
 {  
   if(!busy)  
   {  
     switch(task)  
     {  
       case 'init' :  
         
         move = tactic1();  
         if(move) { task = 'doit'; }  
         else { task = 'tactic2'; }  
         
         busy = false;  
         break;  
           
       case 'tactic2' :  
         
         move = tactic2();  
         if(move) { task = 'doit'; }  
         else { task = 'tactic3'; }  
         
         busy = false;  
         break;  
           
       case 'tactic3' :  
         
         move = tactic3();  
         if(move) { task = 'doit'; }  
         else { task = 'pass'; }  
         
         busy = false;  
         break;  
           
       case 'doit' :  
         
         doit();  
         task = 'final';  
           
         busy = false;  
         break;  
 
       case 'pass' :  
         
         pass();  
         task = 'final';  
           
         busy = false;  
         break;  
 
       case 'final' :  
         
         clearInterval(processor);  
         
         busy = false;  
         break;  
     }  
   }  
     
 }, 100);  
}

该代码比原始代码冗长得多,因此,如果唯一必须减小代码大小,那显然不是可行的方法。

但是我们在这里试图做的是创建一个没有上限的执行环境 ,也就是说,这个过程在复杂性和长度方面没有上限。 这就是我们所做的。

此模式可以无限期扩展,可以执行数百甚至数千个任务。 这可能需要很长的时间来运行,但运行会,只要每一个人的任务是不是太激烈,它会不杀死浏览器中运行。

一条不归路

这种方法的优点也是它的主要缺点:由于内部函数是异步的,因此我们无法从外部函数返回值 因此,例如,我们无法做到这一点(或者,虽然可以,但是毫无意义):

function checksomething()  
{  
 var okay = false;    
   
 var i = 0, limit = 100, busy = false;  
 var processor = setInterval(function()  
 {  
   if(!busy)  
   {  
     busy = true;  
       
     if(condition)  
     {  
       okay = true;  
     }  
 
     if(++i == limit)  
     {  
       clearInterval(processor);  
     }  
       
     busy = false;  
   }  
     
 }, 100);  
   
 return okay;    
}

checksomething()函数将始终返回false因为内部函数是异步的。 外函数将在内部函数的第一次迭代发生之前返回!

下一个示例同样毫无意义:

if(++i == limit)  
{  
 clearInterval(processor);  
   
 return okay;  
}

我们不在外部函数的范围内,因此我们无法从外部函数中返回。 那个返回值无用地消失在以太里。

我们在这里可以做的是从Ajax编码技术中脱颖而出,并使用一个callback函数(在本示例中,我将其称为“ oncomplete”):

function checksomething(oncomplete)  
{  
 var okay = false;  
   
 var i = 0, limit = 100, busy = false;  
 var processor = setInterval(function()  
 {  
   if(!busy)  
   {  
     busy = true;  
       
     if(condition)  
     {  
       okay = true;  
     }  
 
     if(++i == limit)  
     {  
       clearInterval(processor);  
         
       if(typeof oncomplete == 'function')  
       {  
         oncomplete(okay);  
       }  
     }  
       
     busy = false;  
   }  
     
 }, 100);  
}

因此,当我们调用checksomething() ,我们将传递一个匿名函数作为其参数,并在作业完成时使用最终值调用该函数:

checksomething(function(result)  
{  
 alert(result);  
});

优雅? 否。但是功能强大吗? 是。 这就是重点。 使用这种技术,我们可以编写原本不可能的脚本。

机器人梦见硅羊吗?

利用我们工具包中的这项技术,我们现在可以解决以前超出可能性范围的JavaScript项目。 我为这种模式开发的游戏具有相当简单的逻辑,因此也具有相当简单的大脑 ,但是对于常规迭代而言仍然太多了。 而且还有很多其他的游戏需要更多的影响力!

我的下一个计划是使用此技术来实现JavaScript Chess引擎。 国际象棋具有多种可能的方案和策略,导致决策可能需要非常长的时间来计算,比没有这种技术的情况下要长得多。 即使要创建最基本的思维机,也需要大量的计算,我承认对这种可能性感到非常兴奋。

如果我们能拉出这样的把戏,谁能说呢? 自然语言处理,试探法……也许我们有构建JavaScript的基石

如果您喜欢阅读这篇文章,您会喜欢Learnable的 ; 向大师学习新鲜技能的地方。 会员可以立即访问所有SitePoint的电子书和交互式在线课程,例如JavaScript JavaScript for the Web 。

本文的评论已关闭。 对JavaScript有疑问吗? 为什么不在我们的论坛上提问呢?

图片来源: Randen L Peterson

From: https://www.sitepoint.com/multi-threading-javascript/

你可能感兴趣的:(JavaScript中的多线程)