- 技術 💻
キュー(Queue)とは|スタックとの違い・速度差とRedisでの実演
結論
キュー(Queue)とは、先に入れたデータから順に取り出す「先入れ先出し(FIFO)」のデータ構造です。後に入れたものから取り出すスタック(LIFO)とは、取り出す順番が逆になります。同じ考え方で、システム同士が仕事を順番待ちで受け渡す仕組みを「メッセージキュー」と呼びます。
この記事でわかること
- キューとスタックの違いと、循環キュー・優先度付きキューなどの種類
- Python・Java・JavaScript で「遅いキュー」と「速いキュー」を実測した差(最大で約1,000倍)
- Redis の LPUSH・BRPOP で作ったメッセージキューの実演と、落ちても消えない作り方
キューは、レジの行列と同じです。後から来た人は列の最後に並び、会計は列の先頭の人から進みます。プログラムでは、列の最後に入れる操作をエンキュー(enqueue)、列の先頭から取り出す操作をデキュー(dequeue)と呼びます。
flowchart TD
IN[エンキュー:末尾に入れる] --> T
subgraph Q[キュー]
direction LR
T[C] --> M[B] --> H[A]
end
H --> OUT[デキュー:先頭から出す]
よく調べられることの答えを先にまとめます。
| 知りたいこと | 答え | 詳しい節 |
|---|---|---|
| キューとは | 先に入れたものから順に取り出す(FIFO)データ構造 | キューとは |
| キューとスタックの違い | キューは先入れ先出し、スタックは後入れ先出し(LIFO) | キューとスタックの違い |
| enqueue・dequeue の意味 | 末尾に入れる操作と、先頭から取り出す操作 | 基本操作と種類 |
| Python で queue を使うには | 1つのスレッドなら collections.deque、スレッド間なら queue.Queue | Pythonのキュー |
| list.pop(0) は遅いのか | 30万件で deque の約1,100倍の時間がかかった | Pythonのキュー |
| Java の Queue はどれを使うか | ふつうは ArrayDeque。スレッド間は BlockingQueue | Javaのキュー |
| JavaScript の queue | 専用の型は無い。件数が多いなら shift() を使わない | JavaScriptのキュー |
| メッセージキューとは | 仕事を順番待ちで受け渡し、送る側と処理する側を切り離す仕組み | メッセージキューとは |
| Redis でキューを作るには | LPUSH で入れて BRPOP で待って取り出す。落ちても消さないなら BLMOVE | Redisでメッセージキュー |
確認環境:Ubuntu 24.04.5 LTS(Intel Xeon 2.10GHz・2コア)の上で、作業用のフォルダーの中で実行しました。データ構造としてのキューは Python 3.13.18(collections.deque・queue・heapq)、OpenJDK 21.0.12.1(ArrayDeque・LinkedList・PriorityQueue・ArrayBlockingQueue)、Node.js v22.23.4 で動かし、速度は同じ件数を出し入れした時間を2回ずつ測りました。メッセージキューは Redis 7.0.15 を 127.0.0.1 の自分のポート(46161)と自分のデータフォルダーで起動し、redis-cli と redis-py 8.1.0 から LPUSH・BRPOP・BLMOVE・Streams を実行して、終わったあと自分のプロセスだけを止めています。Amazon SQS・RabbitMQ・Apache Kafka は動かしておらず、それぞれの公式ドキュメントで確かめた範囲だけを書いています。Python・Java・Redis の仕様は公式ドキュメント(docs.python.org・Oracle の Java API・redis.io)で確かめ、図は mermaid 11.16.0 で描画を確かめました(確認日 2026年10月29日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「キュー(Queue)とは|スタックとの違い・速度差とRedisでの実演」
キュー(Queue)とは:先入れ先出し(FIFO)のデータ構造
この節では、キューという言葉の意味と、FIFO・エンキュー・デキューの関係が分かります。
キューは、データを入れた順番のまま取り出すための入れ物です。英語の queue は「順番待ちの列」という意味で、日本語では「待ち行列」とも訳されます。最初に入れたデータが最初に出てくるので、First In, First Out の頭文字をとって FIFO(先入れ先出し)と呼びます。
- エンキュー(enqueue):列の末尾(tail・rear)にデータを入れる。言語によって
append・offer・push・putと名前が変わる。 - デキュー(dequeue):列の先頭(head・front)からデータを取り出す。
popleft・poll・shift・getなど。 - ピーク(peek):先頭のデータを、取り出さずに見るだけ。
身近なところでは、プリンターの印刷待ち、コールセンターの着信の順番待ち、キーボードから打った文字の処理、Web サーバーが受け付けた接続の順番待ちなどがキューの形をしています。どれも「先に来たものを先に片付ける」「片付ける速さより来る速さが一時的に上回っても、取りこぼさずに並べておく」という2つの役目を持っています。
順番を守る
入れた順に取り出すので、受け付けた順に処理できる。
FIFO の本体
速さの差を吸収する
来る速さと処理する速さが違っても、間で待たせておける(バッファ)。
印刷待ち・ジョブ
送る側と受ける側を分ける
入れる人と取り出す人が、お互いを知らなくても仕事を渡せる。
メッセージキュー
近い順に調べる
幅優先探索(BFS)で「近いところから順に」見ていく道具になる。
最短経路
手元の Python で A・B・C の順に入れて取り出すと、入れた順に出てきました。
from collections import deque
q = deque()
for x in "ABC":
q.append(x) # エンキュー(末尾に入れる)
print("queue pop :", q.popleft(), q.popleft(), q.popleft()) # デキュー(先頭から出す)
queue pop : A B C
キューとスタックの違い(FIFOとLIFO)
この節では、キューとスタックの違いを、取り出す順番・操作名・使われる場面で比べて分かります。
違いは「どちらの端から取り出すか」だけです。キューは入れた端の反対側(先頭)から取り出し、スタックは入れた端と同じ側(上)から取り出します。スタックは積み重ねた皿と同じで、最後に置いた皿を最初に取ります。Last In, First Out の頭文字で LIFO(後入れ先出し)と呼びます。
同じ A・B・C を、スタックとキューに入れて取り出した結果です。
from collections import deque
s = []; q = deque()
for x in "ABC":
s.append(x); q.append(x)
print("stack pop :", s.pop(), s.pop(), s.pop())
print("queue pop :", q.popleft(), q.popleft(), q.popleft())
stack pop : C B A
queue pop : A B C
キュー(FIFO・先入れ先出し)
入れる:enqueue(末尾)
出す:dequeue(先頭)
例:レジの行列・印刷待ち・ジョブキュー・BFS
スタック(LIFO・後入れ先出し)
入れる:push(上)
出す:pop(上)
例:元に戻す(Undo)・関数の呼び出し・括弧の対応・DFS
| 比べること | キュー | スタック |
|---|---|---|
| 取り出す順番 | 先に入れたもの(FIFO) | 最後に入れたもの(LIFO) |
| 出し入れする端 | 入れる端と出す端が別 | 同じ端だけを使う |
| 操作の名前 | enqueue・dequeue | push・pop |
| Python で作るなら | collections.deque の append・popleft | list の append・pop |
| Java で作るなら | ArrayDeque の offer・poll | ArrayDeque の push・pop |
| 探索で使うと | 幅優先探索(BFS)。最短の手数が分かる | 深さ優先探索(DFS) |
| 身近なたとえ | レジの行列・駅の改札 | 積み重ねた皿・本の山 |
「スタック」という言葉は、メモリのスタック領域(関数の引数やローカル変数を置く場所)や、Java の StackOverflowError の「スタック」とも同じ考え方から来ています。関数を呼ぶたびに上に積み、戻るときに上から外すので LIFO です。リストや連結リストなど、ほかのデータ構造を選ぶときの注意は、関連記事「データ構造の落とし穴8選|LinkedListが遅い理由」で扱っています。
キューの基本操作と種類(循環キュー・両端キュー・優先度付きキュー)
この節では、キューが持つ操作と計算量、よく出てくる派生の種類が分かります。
キューの操作は、入れる・取り出す・先頭を見る・空かどうか調べる・件数を数える、の5つでほぼすべてです。うまく作られたキューでは、入れるのも取り出すのも件数に関係なく一定の時間(O(1))で終わります。
| 操作 | 意味 | 計算量の目安 | 空のときの動き(例) |
|---|---|---|---|
| enqueue | 末尾に入れる | O(1) | 満杯なら例外か false(上限があるキュー) |
| dequeue | 先頭を取り出す | O(1) | 例外か null・None |
| peek・front | 先頭を見るだけ | O(1) | 例外か null |
| isEmpty・size | 空か・何件あるか | O(1) | true・0 |
| 途中の要素を探す | キューの本来の使い方ではない | O(n) | ― |
配列の先頭を消して詰める作り方だと、取り出すたびに残り全部をずらすため O(n) になります。Python の list.pop(0)、Java の ArrayList.remove(0)、JavaScript の大きな配列の shift() がこの形で、件数が多いと急に遅くなります(後の節で実測します)。
よく出てくるキューの種類
| 種類 | 取り出す順番 | 主な用途 | 標準ライブラリの例 |
|---|---|---|---|
| 普通のキュー | 入れた順(FIFO) | 順番待ち・ジョブ | Python deque・Java ArrayDeque |
| 循環キュー(リングバッファ) | 入れた順。固定長の配列を輪のように使う | 音声・通信のバッファ、組み込み | deque(maxlen=…)・ArrayBlockingQueue |
| 両端キュー(deque) | 両端から出し入れできる | キューとスタックの両方、直近 N 件 | Python deque・Java ArrayDeque |
| 優先度付きキュー | 優先度の高い(値の小さい)順 | タスクの優先度・ダイクストラ法 | heapq・PriorityQueue |
| ブロッキングキュー | 入れた順。空なら来るまで待つ | スレッド間の受け渡し | queue.Queue・LinkedBlockingQueue |
| メッセージキュー | 入れた順(製品による) | システム間・プロセス間の受け渡し | Redis・Amazon SQS・RabbitMQ |
deque は「デック」と読み、double-ended queue(両端キュー)の略です。両方の端で O(1) に出し入れできるので、片側だけ使えばキュー、同じ側だけ使えばスタックになります。Python の公式ドキュメントは、deque は「どちらの端からも、ほぼ同じ O(1) の性能で append と pop ができる」と書いています。
循環キュー(リングバッファ)を配列で作る
この節では、固定長の配列で循環キューを作る方法と、満杯・空の見分け方が、実行結果で分かります。
循環キューは、配列の最後まで使ったら先頭に戻って続きを使うキューです。先頭の位置(head)と件数(size)を覚えておき、末尾の位置は (head + size) % 容量 で計算します。取り出しても要素をずらさないので、出し入れはいつも O(1) です。基本情報技術者試験などでも、配列と添字(ポインター)で作るキューとしてよく出てくる形です。
class RingQueue:
"""配列(固定長)で作る循環キュー"""
def __init__(self, cap):
self.buf = [None] * cap; self.head = 0; self.size = 0
def enqueue(self, x):
if self.size == len(self.buf): raise OverflowError("queue is full")
tail = (self.head + self.size) % len(self.buf) # 末尾の位置を計算
self.buf[tail] = x; self.size += 1
def dequeue(self):
if self.size == 0: raise IndexError("queue is empty")
x = self.buf[self.head]; self.buf[self.head] = None
self.head = (self.head + 1) % len(self.buf); self.size -= 1 # 先頭を1つ進める
return x
q = RingQueue(4)
for x in "ABCD": q.enqueue(x)
print(q.buf, "head=", q.head)
try: q.enqueue("E")
except OverflowError as e: print("OverflowError:", e)
print(q.dequeue(), q.dequeue()); q.enqueue("E"); q.enqueue("F")
print(q.buf, "head=", q.head, "size=", q.size)
print([q.dequeue() for _ in range(4)])
try: q.dequeue()
except IndexError as e: print("IndexError:", e)
['A', 'B', 'C', 'D'] head= 0
OverflowError: queue is full
A B
['E', 'F', 'C', 'D'] head= 2 size= 4
['C', 'D', 'E', 'F']
IndexError: queue is empty
4行目を見ると、配列の中は ['E', 'F', 'C', 'D'] と並んでいますが、先頭(head)は2番目(C)です。取り出すと C・D・E・F と、入れた順に出てきました。配列の中の並び順と、キューとしての順番は一致しません。
1満杯と空を見分ける
head と tail だけだと、満杯と空のどちらも head == tail になる。件数を別に持つか、配列を1つ余らせて区別する。
2満杯のときの決まりを決める
例外にする(上の例)、入れられなかったと返す、一番前を捨てて入れる(直近 N 件だけ残す)のどれかを選ぶ。
3標準ライブラリにもある
Python は deque(maxlen=N)、Java は ArrayBlockingQueue。自分で作るのは学習と組み込み向け。
Python の deque(maxlen=3) に 1〜5 を入れると、満杯になるたびに反対側(先頭)が捨てられ、直近の3件だけが残りました。ログの直近 N 行を持っておく、といった使い方に向いています。
maxlen=3 after 1..5: deque([3, 4, 5], maxlen=3)
Pythonのキュー:deque・queue.Queue・heapqとlist.pop(0)の速度差
この節では、Python でキューを作るときにどれを使えばよいかと、list.pop(0) との速度差が実測で分かります。
1つのスレッドの中で使うなら collections.deque、スレッドの間で受け渡すなら queue.Queue、優先度の順に取り出すなら heapq を使います。list でも pop(0) でキューのように書けますが、件数が増えると極端に遅くなります。
同じ件数を取り出した時間(実測)
N 件入れておいて、空になるまで先頭から取り出した時間です(Python 3.13.18。2回測り、1回目の値。2回目もほぼ同じでした)。
import time, collections, queue
def bench(n):
l = list(range(n)); t = time.perf_counter()
while l: l.pop(0) # list の先頭から取る
a = time.perf_counter() - t
d = collections.deque(range(n)); t = time.perf_counter()
while d: d.popleft() # deque の先頭から取る
b = time.perf_counter() - t
q = queue.Queue()
for i in range(n): q.put(i)
t = time.perf_counter()
while not q.empty(): q.get() # スレッド安全なキュー
c = time.perf_counter() - t
return a, b, c
| 件数 | list.pop(0) | deque.popleft() | queue.Queue.get() |
|---|---|---|---|
| 1万件 | 7.0ms | 0.3ms | 7.8ms |
| 10万件 | 895.5ms | 2.8ms | 70.2ms |
| 30万件 | 9,453.5ms | 8.6ms | 235.7ms |
- list.pop(0) は、件数が3倍になると時間が約10倍になりました。1回の pop(0) が残りの要素を全部ずらす(O(n))ため、全部取り出すと O(n²) になるからです。Python の公式ドキュメントも、list は
pop(0)とinsert(0, v)で O(n) のメモリ移動がかかる、と書いています。 - deque は件数に比例して増えるだけでした(1万件 0.3ms → 30万件 8.6ms)。
- queue.Queue は deque より約27倍遅いものの、件数に比例しています。中で deque を使い、出し入れのたびにロックを取るぶんの時間です。1つのスレッドの中だけで使うなら deque を選びます。
空のときの例外と上限
from collections import deque
import queue
deque().popleft() # IndexError
[].pop(0) # IndexError
queue.Queue().get_nowait() # queue.Empty
bq = queue.Queue(maxsize=1); bq.put(1)
bq.put_nowait(2) # queue.Full
queue.Queue().get(timeout=0.5) # 0.5秒待って queue.Empty
IndexError: pop from an empty deque
IndexError: pop from empty list
Empty: Empty()
Full: Full()
get(timeout=0.5) -> Empty
queue.Empty と queue.Full はメッセージが空の例外なので、ログに Empty() としか出ません。except queue.Empty: のように型で受けます。timeout を付けずに空の q.get() を呼ぶと、何かが入るまでずっと止まったままになります。
優先度付きキュー(heapq・PriorityQueue)
heapq は、普通の list を「いちばん小さい値がいつも先頭にある」形(ヒープ)に保つ関数の集まりです。5・1・4・2・3 の順に入れたときの中身と、取り出した順です。
heap list: [1, 2, 4, 5, 3]
heappop: [1, 2, 3, 4, 5]
list の中身は並べ替えた順ではありません(先頭が最小であることだけが保証されます)。取り出すと小さい順に出てきました。大きい順に取り出したいときは、値にマイナスを付けて入れます。
queue.PriorityQueue に (優先度, 辞書) を入れると、優先度が同じときに辞書どうしを比べようとして失敗しました。
TypeError: '<' not supported between instances of 'dict' and 'dict'
間に連番を挟んで (優先度, 連番, 中身) にすると、優先度が同じものは入れた順に出てきました。
import itertools, queue
c = itertools.count(); pq = queue.PriorityQueue()
pq.put((2, next(c), {"a": 1})); pq.put((2, next(c), {"b": 2})); pq.put((1, next(c), {"c": 3}))
print([pq.get() for _ in range(3)])
[(1, 2, {'c': 3}), (2, 0, {'a': 1}), (2, 1, {'b': 2})]
queue.Queueでスレッド間の受け渡し(生産者・消費者パターン)
この節では、queue.Queue を使って、仕事を作るスレッドと処理するスレッドを分ける書き方と、その動きが分かります。
queue.Queue は、複数のスレッドが同時に入れたり取り出したりしても壊れないように、ロックを中で持っているキューです。公式ドキュメントは「multi-producer, multi-consumer queues」と説明しています。仕事を作る側(生産者・producer)と処理する側(消費者・consumer)を、このキューでつなぐ形を生産者・消費者パターンと呼びます。
上限3件のキューに6件の仕事を入れ、2つのワーカー(処理するスレッド)が1件0.2秒で処理する例です。
import queue, threading, time
q = queue.Queue(maxsize=3) # 上限3件
def producer():
for i in range(1, 7):
t = time.perf_counter()
q.put(f"job{i}") # 満杯なら空くまで待つ
print(f"{time.perf_counter()-T0:4.2f}s put job{i} (待ち {time.perf_counter()-t:.2f}s) size={q.qsize()}")
def worker(name):
while True:
job = q.get()
if job is None: # 終了の合図
q.task_done(); break
time.sleep(0.2) # 重い処理のつもり
print(f"{time.perf_counter()-T0:4.2f}s {name} done {job}")
q.task_done() # 1件終わったと伝える
T0 = time.perf_counter()
ws = [threading.Thread(target=worker, args=(f"w{i}",)) for i in (1, 2)]
for w in ws: w.start()
producer()
q.join() # 全部の task_done を待つ
for _ in ws: q.put(None)
for w in ws: w.join()
print(f"{time.perf_counter()-T0:4.2f}s all done")
0.01s put job1 (待ち 0.00s) size=1
0.01s put job2 (待ち 0.00s) size=1
0.01s put job3 (待ち 0.00s) size=1
0.01s put job4 (待ち 0.00s) size=2
0.01s put job5 (待ち 0.00s) size=3
0.21s w1 done job1
0.21s w2 done job2
0.21s put job6 (待ち 0.20s) size=2
0.41s w2 done job4
0.41s w1 done job3
0.61s w2 done job6
0.61s w1 done job5
0.61s all done
- job6 は、キューが満杯(3件)だったため
putの中で0.20秒待たされました。上限を付けると、処理が追いつかないときに作る側が自動で待つ(背圧・バックプレッシャー)ので、メモリを使い切りません。 - 取り出す順番は FIFO でも、ワーカーが2つあると「終わる順番」は入れた順になりません(job4 が job3 より先に終わった行)。順番どおりに終わってほしい処理は、ワーカーを1つにするか、結果に番号を付けて後で並べ直します。
q.join()は、putした件数と同じ回数のtask_done()が呼ばれるまで待ちます。task_done を忘れると、join が永遠に戻りません。
Python 3.13 からは q.shutdown() が加わり、終了の合図に None を入れなくても、待っている get() を queue.ShutDown 例外で起こせるようになりました。手元の 3.13.18 で試した結果です。
worker: a
worker: b
worker: ShutDown を受け取り終了
ShutDown ShutDown() ← shutdown 後に put した結果
Javaのキュー:ArrayDeque・LinkedList・PriorityQueue
この節では、Java の Queue の実装をどう選ぶかと、offer・poll と add・remove の違い、よく出る例外が分かります。
Java では、普通のキューもスタックも ArrayDeque を使います。Oracle の API ドキュメントは ArrayDeque について「スタックとして使うと Stack より、キューとして使うと LinkedList より速いと見込まれる」と書いています。ただし、null を入れられないこと、複数のスレッドから同時に使えないことに注意します。
Deque<String> q = new ArrayDeque<>();
q.offer("A"); q.offer("B"); q.offer("C"); // キュー:末尾に入れる
System.out.println("queue poll: " + q.poll() + " " + q.poll() + " " + q.poll());
Deque<String> st = new ArrayDeque<>();
st.push("A"); st.push("B"); st.push("C"); // スタック:先頭に積む
System.out.println("stack pop : " + st.pop() + " " + st.pop() + " " + st.pop());
queue poll: A B C
stack pop : C B A
offer・poll・peek と add・remove・element の違い
Queue インターフェースには、同じ操作に2つの名前があります。失敗したときに例外を投げるか、特別な値(false・null)を返すかの違いです。空のキューと、上限1件の ArrayBlockingQueue で試した結果です。
poll()=null peek()=null
remove(): java.util.NoSuchElementException
element(): java.util.NoSuchElementException
ArrayDeque.offer(null): java.lang.NullPointerException
LinkedList.offer(null) ok, size=1 poll()=null
offer when full: false
add when full: java.lang.IllegalStateException: Queue full
poll(500ms) on empty: null
| 操作 | 例外を投げる | 特別な値を返す | 待つ(BlockingQueue) |
|---|---|---|---|
| 入れる | add(満杯で IllegalStateException) | offer(満杯で false) | put・offer(時間) |
| 取り出す | remove(空で NoSuchElementException) | poll(空で null) | take・poll(時間) |
| 先頭を見る | element(空で NoSuchElementException) | peek(空で null) | ― |
LinkedList は null を入れられるため、poll() が null を返したときに「空だった」のか「null が入っていた」のか区別できません(上の5行目)。ArrayDeque が null を拒むのは、この取り違えを防ぐためです。
PriorityQueue の toString は並び順ではない
PriorityQueue toString: [1, 2, 4, 5, 3]
PriorityQueue poll : 1 2 3 4 5
reverseOrder poll : 5 4
PriorityQueue を System.out.println すると、中のヒープの並びがそのまま出ます(Python の heapq と同じ [1, 2, 4, 5, 3])。優先度の順に見たいときは poll で取り出します。大きい順にしたいときは new PriorityQueue<>(Comparator.reverseOrder()) にします。
ArrayDeque・LinkedList・ArrayList.remove(0) の速度(実測)
N 件を offer してから空になるまで poll した時間です(OpenJDK 21.0.12.1。先に3回ずつ動かして JIT を温めてから測定、2回の値)。
| 件数 | ArrayDeque | LinkedList | ArrayList.remove(0) |
|---|---|---|---|
| 10万件 | 2ms・2ms | 3ms・13ms | 450ms・625ms |
| 30万件 | ― | ― | 4,461ms・4,619ms |
| 100万件 | 33ms・59ms | 161ms・95ms | (測っていない) |
ArrayList の先頭を消す方法は、Python の list.pop(0) と同じく、件数が3倍で時間が約10倍になりました。100万件では ArrayDeque が LinkedList より速く、LinkedList は1件ごとに小さなオブジェクト(ノード)を作るぶん遅く、時間のぶれも大きくなりました。
スレッドの間で受け渡すときは、java.util.concurrent の ArrayBlockingQueue(上限あり・配列)や LinkedBlockingQueue を使います。take() は空なら入るまで待ち、put() は満杯なら空くまで待ちます。
JavaScriptのキュー:shift()が遅くなる件数と自作キュー
この節では、JavaScript でキューを作る方法と、配列の shift() を使うと何件から遅くなるかが実測で分かります。
JavaScript には標準のキューの型がありません。少ない件数なら配列の push()(末尾に入れる)と shift()(先頭を取り出す)で足りる形になりますが、件数が多いときは先頭の位置を覚えておく自作のキューを使います。
class Queue { // 先頭の位置を覚えておくキュー
#items = []; #head = 0;
enqueue(x) { this.#items.push(x); }
dequeue() {
if (this.#head === this.#items.length) return undefined;
const x = this.#items[this.#head]; this.#items[this.#head++] = undefined;
if (this.#head > 1024 && this.#head * 2 > this.#items.length) { // 前の空きを詰める
this.#items = this.#items.slice(this.#head); this.#head = 0;
}
return x;
}
peek() { return this.#items[this.#head]; }
get size() { return this.#items.length - this.#head; }
}
const q = new Queue(); ['A', 'B', 'C'].forEach(x => q.enqueue(x));
console.log('dequeue:', q.dequeue(), q.dequeue(), 'peek:', q.peek(), 'size:', q.size);
console.log('empty shift():', [].shift(), ' empty dequeue():', new Queue().dequeue());
dequeue: A B peek: C size: 1
empty shift(): undefined empty dequeue(): undefined
取り出した場所に undefined を入れておくのは、取り出した値への参照を残さず、メモリを解放させるためです。先頭の空きが半分を超えたら、まとめて詰めています。
shift() と自作キューの速度(実測)
| 件数 | 配列の shift() | 自作 Queue.dequeue() |
|---|---|---|
| 1万件 | 1.0ms | 1.9ms |
| 2万件 | 36.6ms | ― |
| 5万件 | 276.3ms | ― |
| 10万件 | 926.4ms | 6.8ms |
| 30万件 | 11,062.5ms | 10.2ms |
Node.js v22.23.4 では、1万件までは shift() のほうが速く、2万件を超えたあたりから急に遅くなりました。30万件では約1,100倍の差です。小さな配列では V8(Node.js と Chrome の JavaScript エンジン)が先頭を消す処理を速く済ませていますが、これはエンジンの内部の作りによるもので、言語の仕様で決まった速さではありません。件数の上限が読めないキュー(ジョブ・イベント・BFS)は、最初から自作のキューか、npm の専用パッケージにしておくと安心です。
ブラウザの JavaScript で「キュー」という言葉が出てくるもう1つの場面が、イベントループのタスクキューとマイクロタスクキューです。setTimeout のコールバックや Promise の then は、それぞれのキューに入り、入った順に実行されます。考え方はこの記事のキューと同じ FIFO です。
幅優先探索(BFS)でのキューの使い方
この節では、迷路の最短手数を求める幅優先探索でキューがどう働くかと、スタックに替えると結果がどう変わるかが分かります。
幅優先探索(BFS)は、スタート地点から近いマスを順に調べていく方法です。見つけたマスをキューの末尾に入れ、先頭から取り出して調べるので、「1手で行けるマス」を全部調べてから「2手で行けるマス」に進みます。そのため、ゴールに初めて着いたときの手数が最短になります。
from collections import deque
grid = ["S.#.....",
".##.###.",
"....#...",
".##...#G"]
H, W = len(grid), len(grid[0])
def solve(pop_front=True):
start = (0, 0); dist = {start: 0}; q = deque([start]); order = 0
while q:
y, x = q.popleft() if pop_front else q.pop() # キューなら先頭、スタックなら末尾
order += 1
if grid[y][x] == "G": return dist[(y, x)], order
for dy, dx in ((1, 0), (-1, 0), (0, 1), (0, -1)):
ny, nx = y + dy, x + dx
if 0 <= ny < H and 0 <= nx < W and grid[ny][nx] != "#" and (ny, nx) not in dist:
dist[(ny, nx)] = dist[(y, x)] + 1
q.append((ny, nx))
print("queue (BFS): steps=%d, visited=%d" % solve(True))
print("stack (DFS): steps=%d, visited=%d" % solve(False))
queue (BFS): steps=12, visited=21
stack (DFS): steps=14, visited=20
取り出す端を1か所変えただけで、キュー(BFS)は最短の12手、スタック(DFS 風)は遠回りの14手の道を見つけました。スタックだと、先に見つけた道をどんどん奥まで進むため、最初に着いた道が最短とは限りません。最短の手数を求めたいときは、キューを使います。
- 1スタートをキューに入れ、距離0と記録する記録したマスは「訪問済み」
- 2キューの先頭を取り出すpopleft。ここをスタックの pop にすると DFS になる
- 3ゴールなら終わりそのときの距離が最短の手数
- 4隣の未訪問のマスに「距離+1」を記録して末尾に入れる入れるときに訪問済みにすると、同じマスを二重に入れない
- 5キューが空になるまで2に戻る空になったらゴールへは行けない
BFS は、迷路のほかに、SNS の「友達の友達」を近い順に探す、Web ページのリンクを近い順にたどる、ネットワークの最短ホップを求める、といった場面でも同じ形で使われます。
メッセージキューとは(非同期処理・SQS・RabbitMQ・Kafkaの位置づけ)
この節では、データ構造のキューとメッセージキューの関係と、主な製品の違いが分かります。
メッセージキューは、プログラムやシステムの間で「やってほしい仕事(メッセージ)」を順番待ちで受け渡す仕組みです。送る側(プロデューサー)はキューに入れたらすぐ次に進み、処理する側(コンシューマー・ワーカー)は自分の速さでキューから取り出して処理します。データ構造のキューを、プロセスやサーバーをまたいで使えるようにしたもの、と考えると分かりやすくなります。
flowchart TD
W[Web アプリ:注文を受付] -->|入れる| Q[(メッセージキュー)]
Q -->|取り出す| A[ワーカー1]
Q -->|取り出す| B[ワーカー2]
- 画面をすぐ返せる:メール送信・画像の変換・帳票の作成など時間のかかる処理をキューに入れ、利用者には「受け付けました」とだけ先に返す(非同期処理)。
- 急な集中をならせる:注文が一度に来ても、キューにためてワーカーの速さで順に処理する。
- 片方が止まっても受け付けを続けられる:処理する側が止まっていても、メッセージはキューに残り、復旧したら続きから処理する。
- ワーカーを足すだけで速くできる:同じキューから取り出すワーカーを増やすと、並べて処理できる。
同期処理と非同期処理をどう使い分けるか、失敗したときのリトライをどう設計するかは、関連記事「非同期処理の設計:同期との使い分け基準とジョブキューのリトライ設計」で扱っています。
主なメッセージキューの位置づけ(公式ドキュメントで確認)
| 製品 | どんなもの | 順番と届き方(公式の記述) |
|---|---|---|
| Redis(リスト・Streams) | メモリ上のデータストア。リストを使うと簡単なキューになる | リストは入れた順。BRPOP で取ると、取った後に落ちれば消える |
| Amazon SQS(標準) | AWS のマネージドなキュー | 少なくとも1回届く(重複あり)。順番はできるだけ守る(ベストエフォート) |
| Amazon SQS(FIFO) | 順番を守る SQS | メッセージグループの中で送った順。重複を除き1回だけ処理 |
| RabbitMQ | AMQP などを話すメッセージブローカー | 1つのチャネルから送ると、送った順にキューに入る。ack(受け取り確認)あり |
| Apache Kafka | イベントを記録して流すストリーミング基盤 | パーティションの中で書いた順。読んでも消えず、保持期間まで残る |
AWS の公式ドキュメントは、SQS の標準キューについて「メッセージが2回以上届くことがある」ため、同じメッセージを2回処理しても結果が変わらない作り(冪等性)にするよう書いています。Kafka の公式の説明では、イベントは読まれた後も削除されず、トピックごとの保持期間を過ぎると捨てられます。「取り出したら消える」キューと、「読んでも残る」ログ(Kafka)は、似ていても別の道具です。
SQS の送受信・削除・可視性タイムアウトは関連記事「Amazon SQSの使い方|送受信・削除と可視性タイムアウト、FIFO」で、処理に何度も失敗したメッセージを別のキューへ移す仕組みは関連記事「SQSデッドレターキューの設定|maxReceiveCountと再処理」で扱っています。
Redisでメッセージキューを作る(LPUSH・BRPOPを127.0.0.1で実演)
この節では、Redis のリストでメッセージキューを作る手順と、待ち受け(BRPOP)とポーリングの速さの違い、処理中に落ちたときに消えない作り方が、実行結果で分かります。
Redis のリストに LPUSH(左端に入れる)で入れて、RPOP・BRPOP(右端から取り出す)で取り出すと、入れた順に出てくるキューになります。BRPOP の B は blocking(待つ)で、空なら指定した秒数まで、何かが入るのを待ちます。
redis-cli で出し入れする
127.0.0.1 の自分のポートで起動した Redis 7.0.15 に、redis-cli で送った結果です(プロンプトは見やすく足しています)。
127.0.0.1:46161> LPUSH jobs job1 job2 job3
(integer) 3
127.0.0.1:46161> LLEN jobs
(integer) 3
127.0.0.1:46161> LRANGE jobs 0 -1
1) "job3"
2) "job2"
3) "job1"
127.0.0.1:46161> RPOP jobs
"job1"
127.0.0.1:46161> BRPOP jobs 1
1) "jobs"
2) "job2"
127.0.0.1:46161> BRPOP jobs 1
1) "jobs"
2) "job3"
127.0.0.1:46161> BRPOP jobs 2
(nil) ← 2秒待ってから返った(計測 2.10秒)
- LRANGE で見ると job3・job2・job1 と逆に並んでいますが、右端から取り出すので、出てくる順は job1・job2・job3 です。
- BRPOP は「どのキーから取ったか」と「値」の2つを返します。複数のキーを並べて待てるためです。
- 空のリストに
BRPOP jobs 2を送ると、2秒待って(nil)が返りました。timeout に 0 を書くと、入るまでずっと待ちます(redis.io)。 - リストが空になると、キーそのものが消えます(TYPE が none になりました)。
2つのワーカーで取り出す(Python・redis-py)
import redis, json, time
c = redis.Redis(host="127.0.0.1", port=46161, decode_responses=True)
while True:
item = c.brpop("jobs", timeout=1) # 来るまで最大1秒待つ
if item is None:
break # 1秒何も来なければ終了
job = json.loads(item[1])
# ここで仕事をする(メール送信など)
このワーカーを2つのスレッドで動かし、別の接続から20件を LPUSH すると、2つのワーカーに交互に配られました。
w1: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
w2: [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
redis.io は、同じキーを複数のクライアントが待っているときは、いちばん長く待っているクライアントから順に渡すと書いています。処理を終えて BRPOP に戻ったワーカーは列の最後に並び直すので、交互になりました。
BRPOP(待ち受け)とポーリングの速さの比較
入れてからワーカーが受け取るまでの時間を、BRPOP と「RPOP して空なら0.5秒休む」ポーリングで比べました(それぞれ20件)。
| 取り出し方 | 受け取るまで(中央値) | 受け取るまで(最大) |
|---|---|---|
| BRPOP(待ち受け) | 0.36ms | 5.63ms |
| RPOP+0.5秒休む(ポーリング) | 260.73ms | 489.71ms |
ポーリングは、休む間に入ったメッセージを最大でその休み時間ぶん待たせます。休み時間を短くすれば速くなりますが、空のときにも Redis へ問い合わせを送り続けることになります。BRPOP は、入った瞬間に Redis 側から渡されるので、待ち時間も無駄な問い合わせもありません。
処理中に落ちるとメッセージが消える(BRPOP)
redis.io は、RPOP・BRPOP で作ったキューは、ワーカーが受け取った直後・処理する前に落ちるとメッセージが失われるので「信頼できない(not reliable)」と書いています。手元で、取り出した直後にプロセスを終了させて確かめました。
## BRPOP で取って落ちる
took order-1
exit=1
q:jobs LLEN=(integer) 0 ← order-1 はどこにも残っていない
## BLMOVE で processing へ移して落ちる
took order-2
exit=1
q:jobs:
(empty array)
q:processing:
1) "order-2" ← 処理中のリストに残っている
## 戻す(processing → jobs)
"order-2"
took order-2 -> done
q:processing:
(integer) 0 ← 終わったので LREM で消した
取り出すと同時に「処理中」のリストへ移す
BLMOVE q:jobs q:processing RIGHT LEFT 1。取り出しと移動が1つのコマンドなので、途中で落ちても消えない。
処理する
メール送信などの本来の仕事をする。
終わったら「処理中」から消す
LREM q:processing 1 <値>。ここまで来て初めて、メッセージが無くなる。
長く残っているものを戻す
別のプロセスが「処理中」を見張り、決めた時間を過ぎたものを LMOVE でキューへ戻す。戻すと2回処理されることがあるので、処理は冪等にしておく。
redis.io によると、この形の説明に出てくる RPOPLPUSH は Redis 6.2.0 から非推奨で、LMOVE に RIGHT と LEFT を付けて置き換えます。待つ版は BLMOVE です。
Redis Streams のコンシューマーグループ
受け取り確認(ack)まで Redis に任せたいときは、Streams を使う方法もあります。取り出しただけでは「ack 待ち」として残り、XACK を送ると消えます。
> XADD orders * id 101
"1793282946836-0"
> XGROUP CREATE orders workers 0
OK
> XREADGROUP GROUP workers w1 COUNT 1 STREAMS orders >
1) 1) "orders"
2) 1) 1) "1793282946836-0"
2) 1) "id"
2) "101"
> XPENDING orders workers
1) (integer) 1 ← w1 が受け取ったが ack していない
2) "1793282946836-0"
3) "1793282946836-0"
4) 1) 1) "w1"
2) "1"
> XACK orders workers 1793282946836-0
(integer) 1
> XPENDING orders workers
1) (integer) 0
シェルから XADD orders * id 101 を打つときは、* がファイル名に置き換えられないように注意します。手元では最初、* がカレントフォルダーのファイル名に展開され、ERR Invalid stream ID specified as stream command argument になりました。'*' と引用符で囲みます。
Redis そのものの用途と速さは関連記事「Redisとは|何に使う・なぜ速いかをRDBとの実測で解説【用途早見表】」で、Python から Redis のキューに仕事を流すフレームワークは関連記事「Celeryの使い方|Redis/Valkeyで非同期タスクを動かす」で扱っています。
キューのよくあるエラー・つまずき(原文つき)
この節では、キューを使っていて出るエラーの原文と、原因・直し方が一覧で分かります。
| 出たもの(原文) | 原因 | 直し方 |
|---|---|---|
IndexError: pop from an empty deque |
空の deque から popleft した | 先に if q: で調べる |
Empty: Empty()(queue.Empty) |
空の queue.Queue から get_nowait・timeout 付き get | except queue.Empty: で受ける |
Full: Full()(queue.Full) |
上限のある Queue に put_nowait した | put で待つか、上限を見直す |
TypeError: '<' not supported between instances of 'dict' and 'dict' |
PriorityQueue で優先度が同じとき中身を比べた | (優先度, 連番, 中身) にする |
java.util.NoSuchElementException |
空の Queue に remove・element | poll・peek にして null を調べる |
java.lang.IllegalStateException: Queue full |
上限のある BlockingQueue に add | offer(false を見る)か put(待つ) |
java.lang.NullPointerException |
ArrayDeque に null を入れた | null を入れない。空の印は別の値にする |
WRONGTYPE Operation against a key holding the wrong kind of value |
Redis で文字列のキーに LPUSH した | キュー用のキー名を分ける(q:jobs など) |
ERR Invalid stream ID specified as stream command argument |
シェルが * をファイル名に展開した |
'*' と引用符で囲む |
NOGROUP No such key 'orders' or consumer group 'workers' |
グループを作る前に XREADGROUP した | 先に XGROUP CREATE … MKSTREAM |
エラーにはならないものの、気づきにくいつまずきもあります。
件数が増える処理でのキュー
30万件で約1,000倍の差になった
スレッドの間の受け渡し
ArrayDeque は公式に「スレッド安全ではない」
落とせない仕事のキュー
取った直後に落ちると消えた(実演)
- 1上限を決めたか上限の無いキューは、処理が追いつかないとメモリを使い切る。maxsize・capacity を付ける
- 2空のときの動きを決めたか例外・null・待つ のどれにするか。timeout の無い get・take は止まったままになる
- 3終わり方を決めたかNone などの終了の合図・shutdown()・timeout で、ワーカーを止められるようにする
- 42回処理されても困らないかメッセージキューは、戻し・再送で同じメッセージが2回届くことがある
- 5終わる順番を当てにしていないかワーカーが2つ以上なら、取り出しは FIFO でも終わる順は入れ替わる
どれを使うか:キューの判断表
この節では、やりたいことごとに、どのキューを選べばよいかが分かります。
| やりたいこと | 選ぶもの | 理由 |
|---|---|---|
| 1つのプログラムの中で順番に処理(Python) | collections.deque | 両端 O(1)。queue.Queue より約27倍速かった |
| 1つのプログラムの中で順番に処理(Java) | ArrayDeque | LinkedList より速い(公式・実測) |
| 1つのプログラムの中で順番に処理(JavaScript) | 先頭の位置を覚える自作キュー | shift() は2万件を超えると急に遅くなった |
| スレッドの間で受け渡す | queue.Queue・ArrayBlockingQueue | ロックと「待つ」が中にある |
| 優先度の高い順に処理 | heapq・PriorityQueue | 取り出しが O(log n) |
| 直近 N 件だけ持つ | deque(maxlen=N)・循環キュー | 満杯になると先に入れたほうから捨てる |
| 最短の手数を求める | キューで BFS | 近い順に調べるので最初に着いた手数が最短 |
| プロセス・サーバーの間で受け渡す(小さく始める) | Redis のリスト(BRPOP・BLMOVE) | コマンド数個で作れる。待ち受けで 0.36ms |
| AWS でマネージドに任せる | Amazon SQS(順番が要るなら FIFO) | サーバーの管理が要らない。標準は重複ありの前提 |
| 同じデータを複数のシステムが読む・後から読み直す | Apache Kafka | 読んでも消えず、保持期間まで残る |
よくある質問
Q. キューとは簡単に言うと何ですか? A. 先に入れたものから順に取り出す入れ物です。レジの行列のように、後から来たものは最後に並び、先頭から順に処理されます。この順番の決まりを FIFO(先入れ先出し)と呼びます。
Q. キューとスタックの違いは何ですか? A. 取り出す順番が逆です。キューは先に入れたものから(FIFO)、スタックは最後に入れたものから(LIFO)取り出します。迷路の探索では、キューを使うと最短の手数が分かり、スタックを使うと深さ優先探索になります。
Q. FIFO と LIFO は何の略ですか? A. FIFO は First In, First Out(先入れ先出し)、LIFO は Last In, First Out(後入れ先出し)の略です。在庫や会計の分野でも同じ言葉が使われます。
Q. enqueue と dequeue の読み方と意味は? A. エンキュー・デキューと読みます。enqueue はキューの末尾に入れること、dequeue はキューの先頭から取り出すことです。両端キューの deque(デック)とは別の言葉です。
Q. Python でキューを使うには何を import しますか?
A. 1つのスレッドの中なら from collections import deque で、append で入れて popleft で取り出します。スレッドの間で受け渡すなら import queue の queue.Queue を使います。
Q. list で pop(0) を使うのはいけないのですか? A. 数十件なら差はほとんど出ませんが、件数が増えると急に遅くなります。手元では30万件で deque の約1,100倍(9.45秒と8.6ミリ秒)かかりました。件数の上限が読めない処理では deque を使います。
Q. Java の Queue は new できますか?
A. Queue はインターフェースなので、そのままでは new できません。Queue<String> q = new ArrayDeque<>(); のように実装クラスを使います。スレッドの間で使うなら ArrayBlockingQueue や LinkedBlockingQueue です。
Q. 優先度付きキューは本当のキューですか? A. 入れた順ではなく、優先度の順に取り出すので FIFO ではありません。「次に取り出すものを決める入れ物」という意味でキューと呼ばれています。同じ優先度のものを入れた順に出したいときは、連番を一緒に入れます。
Q. メッセージキューを使うと何がうれしいのですか? A. 時間のかかる処理を後回しにして画面をすぐ返せること、急な集中をならせること、処理する側が止まっても受け付けを続けられること、ワーカーを足して速くできることです。代わりに、処理の終わりを待つ仕組みや、2回処理されても困らない作りが要ります。
Q. Redis をキューに使っても問題ありませんか? A. 小さく始めるには手軽です。ただし、BRPOP で取った直後にワーカーが落ちるとメッセージが消えるので、落とせない仕事は BLMOVE で処理中のリストへ移すか、Streams の ack を使います。Redis を永続化しない設定で動かしていると、Redis 自身の再起動でもキューの中身が消えます。
Q. Kafka はメッセージキューですか? A. メッセージを受け渡す点は同じですが、Kafka は読まれたメッセージを消さずに保持期間まで残し、複数のシステムが同じデータを何度でも読めます。「取り出したら消える」キューとは考え方が違います。
Q. キューは基本情報技術者試験に出ますか? A. スタックとキュー、配列と添字で作る循環キュー、木やグラフの探索は、データ構造とアルゴリズムの分野で扱われます。出題範囲は IPA の公開しているシラバスで確かめられます。
まとめ
- キューは、先に入れたものから順に取り出す FIFO のデータ構造。入れるのが enqueue、取り出すのが dequeue。スタックは逆の LIFO。
- Python は deque、Java は ArrayDeque、JavaScript は先頭の位置を覚える自作キューを使う。list.pop(0)・ArrayList.remove(0)・大きな配列の shift() は、30万件で約1,000倍遅かった。
- スレッドの間は queue.Queue・BlockingQueue を使い、上限・空のとき・終わり方を決めておく。上限を付けると作る側が自動で待つ。
- キューで幅優先探索をすると、最初にゴールに着いた手数が最短になる(手元の迷路で12手。スタックでは14手)。
- メッセージキューは、システムの間で仕事を順番待ちで渡す仕組み。Redis の LPUSH・BRPOP で作れ、待ち受けは 0.36ms で受け取れた。落とせない仕事は BLMOVE か Streams の ack を使い、SQS・Kafka は届き方の違いを公式ドキュメントで確かめて選ぶ。
参考・出典
確認日はいずれも 2026年10月29日です。
- Python ドキュメント「collections --- コンテナデータ型」(deque):https://docs.python.org/3/library/collections.html#collections.deque
- Python ドキュメント「queue --- 同期キュークラス」:https://docs.python.org/3/library/queue.html
- Python ドキュメント「heapq --- ヒープキューアルゴリズム」:https://docs.python.org/3/library/heapq.html
- Oracle「ArrayDeque (Java SE 21 & JDK 21)」:https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/ArrayDeque.html
- Oracle「Queue (Java SE 21 & JDK 21)」:https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Queue.html
- Oracle「PriorityQueue (Java SE 21 & JDK 21)」:https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/PriorityQueue.html
- Redis「BLPOP」(複数クライアントが待つときの順番・timeout):https://redis.io/docs/latest/commands/blpop/
- Redis「BRPOP」:https://redis.io/docs/latest/commands/brpop/
- Redis「RPOPLPUSH」(Pattern: Reliable queue・非推奨と LMOVE):https://redis.io/docs/latest/commands/rpoplpush/
- Redis「Redis Streams」:https://redis.io/docs/latest/develop/data-types/streams/
- AWS「Amazon SQS queue types」:https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-queue-types.html
- RabbitMQ「Queues」:https://www.rabbitmq.com/docs/queues
- Apache Kafka「Introduction」:https://kafka.apache.org/intro
- Wikipedia「Queue (abstract data type)」:https://en.wikipedia.org/wiki/Queue_(abstract_data_type)
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「キュー(Queue)とは|スタックとの違い・速度差とRedisでの実演」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。