Цитата:
map.keys() – возвращает итерируемый объект по ключам, map.values() – возвращает итерируемый объект по значениям, map.entries() – возвращает итерируемый объект по парам вида [ключ, значение], этот вариант используется по умолчанию в for..of. let recipeMap = new Map([ ["огурец", 500], ["помидор", 350], ["лук", 50] ]); // перебор по ключам (овощи) for (let vegetable of recipeMap.keys()) { alert(vegetable); // огурец, помидор, лук } // перебор по значениям (числа) for (let amount of recipeMap.values()) { alert(amount); // 500, 350, 50 } // перебор по элементам в формате [ключ, значение] for (let entry of recipeMap) { // то же самое, что и recipeMap.entries() alert(entry); // огурец,500 (и так далее) } А зачем их искать, если массив по итогу каждый раз перебирается от начала до конца? |
Вся реализация со списком Не отлаживал, конечно, но как то так
class CPoolItem { next = null; prev = null; operation; constructor (operation) { this.operation = operation; } } class CPool { first = null; last = null; constructor () {} // добавить в конец add (item) { this.fist ??= item; item.prev = this.last; if (this.last) this.last.next = item; this.last = item; return this; } // удалить item delete (item) { const next = item.next; const prev = item.prev; if (!prev) this.first = next; else prev.next = next; if (!next) this.last = prev; else next.prev = prev; return this; } // последовательно просмотреть pool, выполняя операции, какие возможно. view () { let item = this.first; while (item) { if (canExec(item.operation)) { exec (item.operation); this.delete(item) } item = item.next } } } const pool = new CPool(); // Когда поcтупила новая операция делаем poll.add(new CPoolItem(operation)); pool.view(); // Когда какая то операция выполнилась удаляем участников из буфера и делаем pool.view(); // canExec(operation) проверяет, что участников операции нет в буфере // exec (operation) добавляет участников в буфер и отправляет операцию на исполнение. |
Цитата:
|
Цитата:
|
Простая работа с двунаправленным списком.
operation - некий объект, который мне точно не известен, но он описывает операцию, которую надо выполнить. От кого, кому, что .... operation оборачивается в CPoolItem. Это айтем нашего пула. new CPoolItem(operation) Сам пул задается классом CPool. в нем определены методы add - добавление айтема в конец пула, delete - удаления айтема из пула и view последовательный просмотр пула с выполнением операций, которые на данный момент возможны. |
Ну список это обычная структура данных.
Есть всякие очереди, деревья, списки... В них элементы располагаются не последовательно, как в массиве, а где угодно, но связаны между собой указателями. В двунаправленных списках у каждого элемента есть указатели на предыдущий элемент (prev) и следующий (next). В самом заголовке списка есть указатели на первый (first) и последний (last) Обычный список |
Цитата:
|
Ну если только такой перебор с инкрементным ключом, то наверно и Map подойдет.
|
Цитата:
Это хорошо, но могут возникать какие-либо тормоза в обработке запросов. В этом случае в очереди накопится много элементов. Если длина очереди равна N, то разгребать её надо будет за время примерно N*N/2 (просмотр всей очереди после завершения каждой операции). И это может стать проблемой. |
Я тесты провел.
Просмотр очереди из 100_000 записей с удалением половины (все четные, например) занимает 15-40 мс даже на моем довольно старом AMD Я сравнивал свою реализацию со списком и реализацию через map. Результаты разнятся от теста к тесту. То список чуть быстрее, то map. |
Цитата:
У вас есть идеи?) |
Цитата:
|
Цитата:
А всякие варианты с timeout мне не нравятся из-за непонятно какого подбора времен. А если ситуация хоть временно изменится, то эти подобранные для среднего случая времена будут ни на что не годится. |
Цитата:
|
Тут такая ситуация.
Приходят запросы. Некоторые запросы не могут быть сразу выполнены. Что с ними делать? Варианта 2: 1 - отправлять отказ клиенту, сейчас не могу, попробуй снова прислать запрос позже. Не самый лучший вариант, т.к. может случиться так, что клиент долго и много раз посылать свой запрос. 2 - хранить такие запросы на сервере до тех пор пока они не смогут быть исполнены. Осталось продумать реализацию такого хранилища. Должна быть возможность помещать туда запрос. И просматривать запросы, что бы выяснить какие могут быть исполнены. И удалять запросы, которые отправляются на выполнение. Как реализовать такое хранилище можно долго рассуждать. Очередь напрашивается как бы сама собой (ну не совсем очередь "первый пришел - первый обслужен". Хотелось бы, что бы запросы там не застревали надолго. Т.е не было ситуации, что какой то запрос там долго находится, хотя позже пришедшие запросы с этими же пользователями уже выполнились. Для этого из множества запросов, которые могут быть выполнены в данный момент, первыми надо выбирать те, которые раньше пришли. Вот такое должно быть хранилище. Как его называть - списком или еще как не суть. Вот как реализовывать действительно стоит думать Я придумал списки, вы map. И то и то работает и примерно одинаково по времени. Где и как можно оптимизировать время просмотра - не очень понятно. Если бы надо было там искать каких то определенных членов операций. можно еще было бы придумать списки для каждого члена и т.п. Только боюсь, что таких списков будет слишком много, и работа с ними не слишком оптимизирует, т.к их тоже надо как то просматривать. Но нам приходится искать и запросы, членов которых нет в буфере. Как это оптимизировать - совсем не понятно. В любом случае, если за единицу времени постоянно приходит больше запросов, чем может быть обработано за это же время - любое хранилище будет расти. И тут уже надо думать, не о том, как его организовать, а как увеличить количество запросов обрабатываемых в единицу времени. Ну или уменьшать количество запросов - давать отказ клиенту если количество необработанных запросов в хранилище превышает определенную величину. Так и говорить "система перегружена, попробуй минут через 10 (20, 30)" |
Цитата:
|
Цитата:
Это же тоже работа. |
voraa,
это копеечная работа, которая не зависит от размера очереди. Зато вместо одного большого списка для всех, надо будет смотреть два маленьких списка для освободившихся участников. А если таки понадобится соблюдать очередность действий для каждого участника, бонусом имеем быструю проверку на присутствие в очереди. Если различных участников много, то выигрыш весьма изрядный. А если мало, но может быть много задач на одни и те же пары участников, то можно добавить в персональные списки ещё группировку по партнеру. Впрочем, пока нет смысла упарываться до такой степени.. |
Цитата:
Вот начало: 1. На сервер приходит запрос <Петя> хочет передать <Васе> 2. Проверяем Петю и Васю в buffer 3. Если хотя бы один есть в buffer, то ... [... что дальше должно быть исходя из ваших маленьких списков?] Или у вас вообще иная логика подразумевается? (если да, то какая?) |
Цитата:
/* * Ситуация, когда большое множество различных участников хотят провести операцию с участником <X> — очень актуальная. * */ И исходя из ваших маленьких списков непонятно как участнику X что-то сделать в промежутке этих бесчисленных операций. В примере с 1 списком это понятно — операция вставляется в очередь и при достижении выполняется. А как это делается в вашем случае? |
Цитата:
Предполагаем, что pool организован как Map (<id> => <запрос>) c инкрементными идентификаторами. Вводится дополнительная структура данных (Map вполне подойдет). Ключом являются имена участников, которые в настоящее время стоят в очереди. Значениями - список (Set наверно подойдет) идентификаторов запросов в очереди с этим участником. Когда запрос ставится в очередь, мы вносим информацию об этом в эту структуру. Когда какой то запрос завершается, мы не только удаляем участников из набора buffer, но берем с нашей структуры участников информацию об участниках завершившегося запроса. Берем их наборы идентификаторов запросов, стоящих в очереди, и по ним просматриваем именно эти запросы на возможность выполнения, а не всю очередь. Ну как то так. |
Вообще это все надо проверять на практике.
Выяснять, насколько большая очередь скапливается в реальных условиях. Растет она, стабилизируется или пульсирует. Если там 100-300 элементов и ее просмотр занимает считанные миллисекунды, то стоит ли огород городить. |
Цитата:
|
Ну я писал эти тесты, просто для себя. И сравнивал в основном скорость реализации очереди в виде списка, по сравнению с map.
Попробуйте разобраться <body> <script> function canExec(op) { return op.v % 2; } function exec(op) {} class CPoolItem { next = null; prev = null; operation; constructor(operation) { this.operation = operation; } } class CPool { first = null; last = null; size = 0; constructor() {} // добавить в конец add(item) { this.first ??= item; item.prev = this.last; if (this.last) this.last.next = item; this.size++; this.last = item; } // удалить item delete(item) { const next = item.next; const prev = item.prev; if (!prev) this.first = next; else prev.next = next; if (!next) this.last = prev; else next.prev = prev; this.size--; return this; } // последовательно просмотреть pool, выполняя операции, какие возможно. view() { let item = this.first; while (item) { if (canExec(item.operation)) { exec(item.operation); this.delete(item); } item = item.next; } } values() { const _this = this; return (function* () { let item = _this.first; while (item) { yield item; item = item.next; } })(); } } const pool = new CPool(); const map = new Map(); let inckey = 0; const nr100 = () => Math.round(Math.random() * 100_000); const genKeys = n => { const keys = []; while (n--) { const nkey = nr100(); const key = ('' + nkey).padStart(6, '0'); keys.push(key); } return keys; }; const N = 100_000; let t0; const keys = genKeys(N); const buffer = new Set(); // Заполняем buffer из массива имен пользователей keys по принципу // Если имени нет в buffer оно туда заносится // Если именя есть в buffer оно оттуда удаляется for (let i = 0; i < N; i++) { key = keys[i]; if (!buffer.has(key)) buffer.add(key); else buffer.delete(key); } const nseta = buffer.size; // Проверяем может ли быть выполнен запрос op function canExec(op) { return !(buffer.has(op.u1) || buffer.has(op.u2)); } // Выполняем запрос op function exec(op) {} // Тестируем скорость заполнения очереди в 100_000 элементов // Заполняем pool случайно сгенрированными записями {u1:, u2:} t0 = performance.now(); for (const key of keys) { const u1 = keys[nr100()]; const u2 = keys[nr100()]; pool.add(new CPoolItem({ u1, u2 })); } const taddpool = performance.now() - t0; const np = pool.size; // Заполняем map случайно сгенрированными записями id => {u1:, u2:} t0 = performance.now(); for (const key of keys) { const u1 = keys[nr100()]; const u2 = keys[nr100()]; map.set(++inckey, { u1, u2 }); } const taddmap = performance.now() - t0; const nm = map.size; // Тестируем скорость удаления записей из очереди в 100_000 элементов // Из pool удаляются элементы {u1, u2}, если u1 и u2 не присутствуют в buffer t0 = performance.now(); pool.view(); const tdelpool = performance.now() - t0; // Из map удаляются элементы {u1, u2}, если u1 и u2 не присутствуют в buffer t0 = performance.now(); for (const [key, value] of map.entries()) if (canExec(value)) { exec(value); map.delete(key); } const tdelmap = performance.now() - t0; console.log('nset', nseta); console.log('Заполнение pool', taddpool.toFixed(0), np); console.log('Заполнение map', taddmap.toFixed(0), nm); console.log('Удаление pool', tdelpool.toFixed(0), pool.size); console.log('Удаление map', tdelmap.toFixed(0), map.size); </script> </body> |
nset 43228
Заполнение pool 93 100000 Заполнение map 41 100000 Удаление pool 19 81202 Удаление map 40 81297 |
Цитата:
Исходя из вышеперечисленного — что вы имеете ввиду под pool и map и что означают цифры? |
рони, И что?
Вообще я сам не понимаю, что я тестировал. Я удалял слишком много записей. В реальности после исполнения какого то запроса <X op Y> может быть удалено максимум 2 записи. Как только мы удаляем какую то запись <X op P>, этот запрос отправляется на исполнение, X помещается в буфер и дальнейшие запросы с X уже не могут исполняться, и соответственно записи не будут удаляться. Тоже самое и про Y. Другие записи удалены быть не могут, т.к запросы с ними еще не закончились, иначе они не были бы в очереди. |
Цитата:
Это просто разные реализации очереди. |
Цитата:
37 - мс на выполнение просмотра очереди из 100000 элементов и удаления какого то количества. 81418 - количество оставшихся элементов. Я писал, что эти тесты я делал вчера для себя. |
Цитата:
|
Цитата:
pool - это Map, где ключём будет участник, а значением - двусвязный список задач, связанных с участником. Большой общий список всё равно не нужен. Когда приходит новая задача, она просто добавляется в списки к обоим участникам (если не может выполниться прямо сейчас, т.е. кто-то из двух занят). Когда участников много (всего М), то оптимизация очень хорошая: если задачи распределены более-менее равномерно, то имеем дело со списками, которые меньше полного списка в М раз. Ну а если оказалось, что почти все задачи завязаны на какого-то одного участника Х, то по нему выстроится длинный список, зато все прочие будут почти всегда свободны, и нам не придется долго искать задачу в этом длинном списке. Долгие проходы по списку будут, но в редких случаях. Итого: довольно простая оптимизация, которая охватывает почти все кейсы. |
Когда выполнился запрос с участниками X-Y, мы должны искать по X и по Y запросы, которые могут быть выполнены.
По X находим X-P, по Y находим Y-Q. Какой взять на выполнение? Если всегда брать по первому имени, не получится так, что много запросов ?-Y и запросы с Y-? , будут слишком долго сидеть в этом пуле? Допустим решили взять X-P. Его надо удалить из списка X. Но список P тоже надо просмотреть и удалить именно этот запрос |
Цитата:
Цитата:
|
Цитата:
Запрос Y-P пришел раньше. Y что то передает P, потом запрос P что то передает Х Они и исполняться должны сначала Y-P, потом P-X. Оба взять не получится и там и там P. |
Цитата:
А если бы вместо Y-P был Y-Х, то дальнейшего поиска по списку Х вообще не надо. |
Но я так понял со слов автора топика, очередность выполнения действий для конкретного участника не особо важна. Иначе все получается даже проще:
- когда прилетает новая задача А-В, проверяем что А и В нет в выполняемых и их списки ожидания пусты - в этом случае сразу стартуем задачу, иначе добавляем в обе очереди. - по завершении задачи А-В смотрим только начала списков для А и В. Там видим, например, А-С и В-Д, которые запускаем, только если С и Д свободны, а оные задачи находятся в голове списков для С и Д. В этой схеме всё отстреливает за наше любимое О(1) |
Цитата:
Завершился запрос А - Б. В очереди у А: С-А (С свободно), у Б: Б-С и Б-Д (Д свободно) Мы берем С-А, (Б-С уже нельзя взять) (а почему именно С-А, а не Б-С? может Б-С пришел раньше, но вынужден ждать), а Б-Д не возьмем, т.к весь список Б мы не просматриваем |
Вообще, Очередь на 10_000 элементов просматривается (с выяснением свободны ли элементы ища их в Set) за 3-4мс.
Мое мнение, что если очередь сильно больше, то дело не во времени просмотра очереди. Значит запросов сильно больше, чем время их обработки, И оптимизировать надо не очередь, а обработку. |
Цитата:
Из С-А и Б-С запустим ту задачу, которая первая в очереди для С. Если там какая-нибудь С-Д, то ничего не запускается в этот раз. Цитата:
|
Цитата:
|
Часовой пояс GMT +3, время: 08:43. |