Приведенный ниже скрипт имеет скорее теоретический нежели практический интерес.
Итак, постановка задачи:
- Есть скрипт получения некоего набора данных (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 – для того чтобы протестировать исключение чанка из пула при ошибке.
Вот в общем-то и все. Надеюсь было интересно )