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