in Профессиональное

Асинхронный пул на jQuery

Приведенный ниже скрипт имеет скорее теоретический нежели практический интерес.

Итак, постановка задачи:

  • Есть скрипт получения некоего набора данных (JSON).
  • Требуется на основе этого набора данных циклически вызывать асинхронный запрос, который выполняет действия на основе входных данных от первого скрипта.
  • При этом требуется лимитировать число одновременно запущенных асинхронных процессов (чтобы не порождать десятки или даже сотни запросов сразу).

Алгоритм решения:

  • Получить JSON с данными для последующей обработки.
  • Для каждого элемента в полученных данных:
  • — Если пул не заполнен – запустить асинхронный процесс.
  • — Если пул заполнен – ждать пока в пуле освободится слот.
  • По окончанию обработки данных очистить пул.

Проблема:

  • Если организовывать опрос пула циклически, съедается 100% одного ядра CPU, начинает дико тормозить интернет-обозреватель и в конце концов может аварийно завершить работу.

Как можно реализовать скрипт для данной задачи – смотрите ниже:


Начнем с приминания травы – заготовим каркас страницы:

<html>
<head>
 <title>asynchronous pool on jquery demo</title>
 <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js" type="text/javascript"></script>
</head>
<body>
<script>
// тут будет наш скрипт
</script>
<div>
 <div id="stat">тут статистика</div>
 <div id="ajax-results">тут результаты</div>
</div>
</body>
</html>

Небольшая теоретическая вводная по эмуляции многопоточности в javascript: http://javascript.ru/blog/tenshi/mnogopotochnyi-yavaskript. Что дает эта статья? Способ избежать 100%-й загрузки CPU. Применяем:

<script>
/**
 * Emulate mulithreading
 */
Function.prototype.process = function( state ){
  var process = function(){
  var args = arguments;
  var self = arguments.callee;
  setTimeout( function( ){
      self.handler.apply( self, args );
    }, 500 )
  }
  for( var i in state ) process[ i ]= state[ i ];
  process.handler= this;
  return process;
}

Sleep позволяет добиться некоего подобия многопоточности, так как “форкает” таймер с callback-функцией, где переопределяется self.handler. Также нам понадобится менеджер пула:

/**
 * Pool manager
 */
 $.poolManager = function( input, id, data ){
   switch( input.message ){
     case 'start':
       if( this.state !== 'wait' ) return this( input, id, data );
       this.state = 'run';
       this.iteration = 0;
     case 'next':
       if( this.state !== 'run' ) return;
       this.iteration++;
       if( !$.pushInPool( id, data ) ) return this( { message: 'next' }, id, data ); // no slot in pool
       return this( { message: 'finish' } );
     case 'finish':
       if( this.state !== 'run' ) return;
       this.state= 'wait';
       return;
     return;
   }
}.process({ state: 'wait' })

Пока этот код нам ничего не дает. Добавим стартер процесса:

/**
 * Main entry point
 */
 $.main = function(){
   $.results_container = $( "#ajax-results" );
   $.results_container.html( "" );
   $.results_container.append( $( "<div>" ).html( "Chunk size: " + $.pool_settings["chunk_size"] + "; Pool size: " + $.pool_settings["pool_size"] ) );

   // get data
   $.getJSON(
     "getData.php",
     {chunk_size: $.pool_settings["chunk_size"]},
     function( data ) {
       for( i in data ){
         $.poolManager( {message:"start"}, "i-"+i, data[i] );
       }
     }
   );
   // flush pool when finished
   $.threads_pool = [];
 }

В данной функции получаем данные и запускаем цикли их обработки. Изначально вместо for(…) $.poolManager(…) была конструкция вида

  • для каждого чанка
  • пока не добавлен в пул запусти таймер на N милисекунд

Логичная вроде бы конструкция. Только вот она и съедала 100% CPU, приводила к подвисанию и даже крэшу браузера. Давайте теперь взглянем на вставку задачи в пул:

/**
 * Push active work in pool
 */
 $.pushInPool = function( id, data ){
   if( $.sizeof( $.threads_pool ) < this.pool_settings["pool_size"] ){
     $.threads_pool.push( id );
     $.workChunk( id, data );
     return true;
   }
   return false;
 }

Тут все просто – если размер пула равен критическому, возвращаем “ложь”, если нет, то добавляем в массив значение с ID треда и запускаем воркер для текущего чанка. Воркер выглядит так:

/**
 * Do one chunk of work
 */
 $.workChunk = function( id, data ){
   $.results_container
     .append( $( "<div>" )
     .html(
       "Iteration. Working with " + $.sizeof( data ) + " items"
       + "<div id="+id+" style='display:inline;'><img src='indicator.gif'>"
   ));
   $.ajax({
     type: "POST",
     url: "doAction.php",
     data: {},
     success: function( data, textStatus, XMLHttpRequest ){
       $.success_threads++;
       $( "#"+id ).html( " - <b style='color:green;'>OK</b>" );
     },
     complete: function( XMLHttpRequest, textStatus ){
       $.removeFromPool( id );
       $("#stat").html("").append("Success: " + $.success_threads + "<br>Failed: " + $.failed_threads + "<br>Success percentage: " + (($.success_threads)/($.success_threads+$.failed_threads))*100 );
     },
     error: function( XMLHttpRequest, textStatus, errorThrown ){
       $.failed_threads++;
       $( "#"+id ).html( " - <b style='color:red;'>FAILED</b>" );
     }
 });
 }

Самый главный тут – ajax запрос, он же, вне зависимости от того как отработает, удаляет процесс из пула:

/**
 * Remove finished work from pool
 */
 $.removeFromPool = function( id ){
   for( i in $.threads_pool ){
     if( $.threads_pool[i] == id ){
       $.threads_pool.splice( i, 1 );
       return true;
     }
   }
   return false;
 }

Для удаления делаем проход по пулу и ищем ID текущего чанка и если находим, вырезаем при помощи splice.

Вот в общем-то и все.

Можете попробовать демо.

Или скачать пример себе:

  • распакуйте его в любой директории вашего веб-сервера, где выполнится PHP скрипт и запустите asynchronous-pool-on-jquery.php.
  • В комплект входят также:
  • getData.php – скрипт эмулирует отдачу JSON данных
  • doAction.php – воркер, который имеет случайную задержку (чтобы локально все отрабатывало не так быстро) и с вероятностью около 50% он возвращает error 500 – для того чтобы протестировать исключение чанка из пула при ошибке.

Вот в общем-то и все. Надеюсь было интересно )

Write a Comment

Comment

*