世界を動かす技術を、日本語で。

非同期キュー – 私のお気に入りのプログラミング面接質問の一つ

概要

  • プログラミング面接 で使われる「async queue interview」の魅力を紹介
  • sendOnce関数 の設計とシングルスレッド環境での実装課題
  • 新たな要件追加による 応用力や設計力 の評価
  • AI活用 の現状と面接での有効性
  • 今後の技術面接とAIの関わりへの考察

「async queue interview」の魅力

  • 7年以上 にわたり実施されてきた伝統的なプログラミング面接手法
  • Jeremy KaplanCarl Sverre から受け継がれた問題
  • 面接実施回数は 500〜1000回、多くの企業で利用実績
  • 「async queue interview」で検索すると多くの情報が見つかる知名度
  • 本記事の目的は、この面接問題の 魅力AIの適用 について共有すること

面接問題の概要と基本設計

  • クライアント(例:Webアプリ)が サーバー と通信するシナリオ
  • サーバーは 同時リクエスト に弱く、1件ずつ処理させる必要
  • クライアントは シングルスレッド 前提(例:JavaScript環境)
  • 既存のsend関数はブラックボックスとして利用
  • 新たに sendOnce関数 を設計し、同時に1リクエストのみ処理保証

初期実装とバグ

  • requestQueue によるキューイングと processNextRequest による逐次処理
  • 初期の実装では、 同時リクエスト制御 が不十分なバグ
  • 正しい実装には isProcessingフラグrequestQueue の両方が必要
  • シングルスレッド環境を前提とした 非同期制御 の理解が問われる
  • JavaScript未経験者は マルチスレッド的発想 に陥りやすい傾向

面接で評価されるポイント

  • フラグ管理コールバックラッピング の正確な実装力
  • コードを 頭で読み解き、デバッグ できる能力
  • シングルスレッド環境での 同期・非同期処理 の理解
  • 新たな要件追加時の 柔軟な設計力

応用問題への発展

  • minDelayMs パラメータ追加による遅延送信要件
    • setTimeoutの活用で 指定ミリ秒後 にリクエスト追加
  • sendMany :一定間隔でsendOnceを繰り返す関数の実装
  • キャンセル機構 の設計(API設計も評価対象)
  • リトライ機構 :失敗時の再送処理
  • テストコード の作成とエッジケースカバー
  • AsyncQueueクラス としての拡張(優先度付きキュー等の追加API設計)
  • 新要件追加ごとに コードの拡張性や保守性 を評価

AIの活用と面接への影響

  • Replit AgentClaude Sonnet 4.0 によるAI実装例
  • sendOnceの基本要件はAIでも高精度で実装可能
  • 複雑な要件追加時にはAIも バグを生みやすい 傾向
  • 面接で AI利用を推奨、AIとの協働力も評価ポイント
  • AIネイティブ なエンジニアはAIの出力を素早くレビュー・修正できる
  • テストコード作成時も AIの支援 が有効、ただしプロンプト設計とレビューが重要

今後の技術面接とAI活用

  • エンジニアの AI活用力 が今後ますます重要
  • AIを活用したコーディング面接は スキル評価の新基準
  • AIと協働しながら 素早く要件を満たす能力が評価対象
  • 低レイヤーなどAIが苦手な領域以外では、AIの恩恵が大きい
  • 技術面接の進化と AI時代のエンジニア像 への期待

まとめと呼びかけ

  • async queue interview は、設計力・応用力・AI活用力を総合的に評価可能
  • AIと共に進化する 技術面接の今後 への展望
  • 他社や他エンジニアの AI面接活用事例 にも関心
  • 興味があれば Twitter/X でフォローを推奨

Hackerたちの意見

うーん、なんかすごく混乱する質問だね。コミュニケーションは大事だけど、面接官がその場で口頭で説明するのは簡単じゃないと思う。特に、シングルスレッドに関する前提がややこしいからね。もしこれが単なるJavaScriptの質問なら、そう言えばいいのに。結局、そういうことだと思う。Goで書くのはめっちゃ簡単だから、質問自体はJavaScriptの理解度を試してるだけなんじゃないかな。

うん、これがまともな面接質問だとは思えない。async awaitの構文すら触れてないし、コールバックの知識を期待するのは古臭い感じがする。

すごく混乱する質問だね それに同意。‘sendOnce’はほとんどの非同期設定で特定の意味を持つし、この面接質問ではかなり違う意味で使われてるね。

それがさらに良くなるね、候補者は明確化の質問をするべきだよ。曖昧な状況に遭遇したときに、手を挙げるか、適当な仮定をする人と一緒に働いたことがあるから、理解のギャップを埋めるために効果的にコミュニケーションを取る能力は、どんな候補者にも期待したい、特にシニアの人には。

俺にとって混乱するのは、サーバーが壊れてるのにクライアントで何をしてるのかってことだよ。

その通り。JSだけの話じゃなくて、JS的な考え方全般のことだよね。

もしかしたら、解決策について知りすぎてこの記事に入ったのかもしれないけど、これが悪いデザインの面接質問だって言ってるコメントには同意できないな。これはブログ記事だし、候補者に提示されるフォーマットじゃないからね。要件が明確で、面接官からのちょっとしたヒントもあって、評価を無効にすることなく(誰かが特定の要件にトンネルビジョンになってしまったときでも)進められると思う。面接官が知識を示す方法や問題を解決する方法がたくさんあるし、デバウンスの面接質問をやったこともあるけど、要件を重ねていくのが時間的に許されるなら(先頭/末尾、キャンセルなど)、このキュー形式は実際に開発者が日常で作っているものに近いと感じる。

俺も似たような気持ちだし、また同じだね。実際、うちのコードベースにもこのパターンがあって、すべての機能が揃ってるわけじゃないけど、理解しやすくて、議論の機会もたくさんあるからいい感じだよ。

同じく。特にこの問題がそんなに珍しくないと思ってた。すぐ思いつくのは、もしアクセスしているエンドポイントがレート制限されている場合とか。APIコールである必要もないし、同じパターンのものを一度か二度書いたことがあると思う。ただ、これはかなりJavaScript特有だとは思う。

これに対する解決策はすぐに書けるよ。JavaScriptのコールバックにはすごく慣れてるし、デバウンスも実装したことあるから。ただ、この面接官はAIを使って書かなかったからって私を不合格にするんだよね。だから、面接官が何を求めてるのか全然わからない。

ちなみに(これが面接質問としてどれだけ適切かは別として):JSでは、イベントループやプロミスチェーンを使って、キューやリストを手動で管理せずにこれを実現できるよ。グローバル変数としてlet job = Promise.success();を持っていて、新しいジョブをスケジュールするのはjob = job.then(f, errHandler).then(callback, errHandler)になる。デバッグはめっちゃ大変だけど(進行中のキューが「見えない」から)、手動のリストやキュー、ループ、shift/unshift、「isProcessing」フラグなどを扱わなくて済むから、基本的にはそのネイティブ機能をユーザースペースで再実装してるだけなんだ。TFAの素朴な実装のバグを完全に回避できる。これを本番環境で推奨するわけじゃないけど、プログラミングパズルの文脈では面白いかも。追記:皮肉なことに、これはTFAのLLMトークに対するコメントでもあるんだけど、こうやってイベントループをいじることでJSのセマンティクスの強いメンタルモデルが得られる。LLMを使ってたら、ループを受け入れてプロミスチェーンについて学ぶことはなかっただろう。これがLLMを使うリスクなんだよね:成長が止まる。拗らせた比喩を使うなら、SRの素朴な理解は、常に光速で動くけど4次元で、3Dの世界で速く動くほど時間を遅く進むって感じ。スキルも似てて、スキルベクトルは常に固定の大きさ(=「才能」?)なんだ。LLMを使うと、それが基本的にフラットになって、タスクを早く終わらせるけど何も学ばない。使わないと、斜め上に進む:常に改善するけど、「タスク完了」の面では遅くなる。成長が止まる準備はできてる?

正直言って、それはイベントループやPromiseの悪用でもないよ。こういうキューを作るのは、Promiseの意図された使い方の一つだから。

リクエストの順序が気にならないなら、タスクが実行中かどうかを示すフラグを設定して、他のタスクを再スケジュールし続ければいいよ。例えば、こんな感じで:let isProcessing = false; async function checkFlagAndRun(task) { if (isProcessing) { return setTimeout(() => checkFlagAndRun(task), 0); } isProcessing = true; await task(); isProcessing = false; } これでうまくいくはず。テストはこんな感じでできるよ:function delayedLog(message, delay) { return new Promise(resolve => { setTimeout(() => { console.log(message); resolve(); }, delay); }); } function test(name,num) { for (let i = 1; i <= num; i++) { delayedLog(${name}-${i} waited ${delay} ms, delay); } } test('t1',20); test('t2',20); test('t3',20); ちなみに、4つのスケジュールされたタスクでは、基本的に順序が保たれるけど、なぜかはわからない。最初のタスクが常に最初に実行されても、残りの3つは競争するはずなのに。5つ同時にスケジュールされたタスクは順序を崩す。

これが答えだと思ってた。面接は必ずしもJavaScriptプログラマー向けじゃないかもしれないけど、これが正しい解決策のように思える。エラー処理のための機能も取り入れてるしね。

仕事でこれを試したけど、サーバーにエラーや問題があったり、予想以上に時間がかかったりすると、ジョブキューが大きくなりすぎてOOMの問題が起きたんだ。問題をデバッグするために手動リストにしなきゃいけなかったよ。それに、特定のリクエストはキューの先頭にスキップする必要があるケースもあった。プロミスに頼ることで、ユーザー空間の複雑さはかなり減るけどね。

具体的な意見には深入りしないけど、ここにあるコメントの多くがこの質問に答えてると思う。これを読んで、自分のキャリア選択に恥ずかしさを感じる人、他にもいる? 子供の頃からソフトウェアが大好きだったけど、年を重ねるにつれて、友達がプライベートエクイティや医療、法律、つまり他の分野でキャリアを築いていくのを見てると、自分の分野との違いがはっきりわかる。例えば、他の分野の大人が、ここで見られるような評価方法で別の大人を評価することなんてありえないと思う。これは事実だよ。先週、CSVを使ったデータベースから何百万ものウェブページを誇らしげに提供している人のコメントを見たけど、その理由は他のデータベースでも普通にカバーされてることばかりだった。なんか、これって正しい気がしないんだよね。

弁護士は学位の後に法科大学院があって、バー試験、過失に対する法的責任、継続的なライセンス要件がある。医学も同様で、学位の後に医学校、5年以上の厳しい監督下での研修、過失に対する法的責任、継続的なライセンス要件がある。だから、彼らがどうして楽だと思うのかを「事実として知っている」と説明してほしい。これを読んでいるほとんどの人、私も含めて、法律や医学のようにハードルが高かったら、この業界に入ることすらできなかったし、ましてや残ることなんて無理だったよ。

実は、エンジニアリングの多くの分野にも似たような質問があると思う。電気工学の人は回路を説明したり、新しい回路を描いたりすることを求められるし、機械工学の人はちょっとしたハードウェアをデザインするように言われる。面接って本当に難しいよね。45分で幅広い知識をカバーするのは不可能だから、結局はちょっとした脳トレみたいな問題になる。この特定の質問はちょっと不明瞭で混乱を招くけど、候補者に他の場所で働くべきだという良いサインになるかもしれないから、全てが失われたわけじゃないね。

こういうのを読むと、自分のキャリア選択に恥ずかしさを感じる人、他にもいる?逆に、誇りに思うけどね。プライベートエクイティや医療、法律の世界では、正しいアクセントがあって、いい学校を出て、履歴書に良い名前があれば、無能でも仕事がもらえるんだよね。逆に、天才でも正しい資格がなければ見過ごされちゃう。プログラミングは、結局実際にプログラムができるかどうかが全てだから、これが続いてほしいな。

うちの職業って、知的に不安定な人が多い気がする。こういう小さなパズルや細かいことにこだわって、優越感を感じるための手段になってるんだよね。みんな、自分たちが「良い開発者」と「ダメ開発者」を見分ける方法を見つけたと思ってる。でも、テストを実施する時には、彼らが「良い開発者」以外になりようがないんだ。恥ずかしいよね。信じない?なんでブログに書かずにはいられないの?

法律事務所での仕事を探している友達とランチしたんだけど、彼が言うには、面接はただの雰囲気で、実際の法律の質問をされたら新鮮だって。だから、他の業界が必ずしも良いわけじゃないかもね。

彼らは自分たちの「良いプログラマー像」に基づいてチームを作り上げて、マネージャーやディレクターになったら、同じように会社全体を形作るんだよね。知的には豊かだけど、機能的にも生産的にも無能な人たちの温床になってしまって、リソースを使ってエソテリックなサイドクエストに没頭することを重視する。ビジネスを「ちょうど持続可能な方法」で前に進めることは二の次。FAANGレベルの状況には常に周辺にいて、テクノロジーと人間が出会うところに焦点を当ててきたけど、キャリアの30年が経つにつれて、ソフトウェアエンジニアリングはますますばかげたものになってきて、今ではそれとそれに関わる開発者が、私の目標との間のただの邪魔者になってる。

もちろん、これは正しくないよね。正直に言うと、私たちの職業は「ソフトウェアエンジニア=工場労働者」の時代に入ってるし、最悪なのは、ここ数年ずっと音楽椅子をしてたってこと。だから、これらの職業は安定した地位や富、生活の質の向上があって、経験を積むにつれて進化していくけど、ソフトウェア開発では経験年数やどの会社で働いたか、業種、会社が使ってるSaaSがどうかなんて関係ない。結局、トリビアの質問やLeetCodeで評価されるんだよね。

他の分野でも、面接で似たような質問があるよね。医学や法律は特別なケースで、面接を受ける前に通過しなきゃいけない基準があるけど、プライベートエクイティの面接でも、投稿にあるようなケーススタディや技術的な質問が含まれることが多いよ。

以前は、特にひどいPythonコードのコードレビュー課題を出してた。文法については全力でサポートするし、Pythonやその文法を知ってるとは期待してないって強調してたんだ。これが候補者にとってストレスの少ない課題になってるみたい。脳トレを解くわけじゃなくて、仕事の一部をやってるだけだからね。コードを読んで、何をしているのか理解して、よりメンテナブルにするためのフィードバックを提供する。周りのFAANG系のエンジニアたちがリートコードみたいな質問をしてる中で、私は彼らにとって新鮮な風になろうとしてたんだ。でも、今はこれを見直さなきゃいけないね。LLMに投げればほぼ完全な答えが返ってくるから。もう一人の候補者がそれをやったのが明らかだった。

「sendOnce」の質問は全然問題ないと思う。ソフトウェア開発は他の職業とは違うし、いいこと言うけど実際にはプログラムできない候補者が多いからね。まともな開発者にとって、これはプログラミングじゃなくてタイピングだよ。でも「じゃあ、この機能を追加してみて」みたいなのは、面接官が不安なアホってことのサインだね。他の職業でも、不安なアホの面接官が変な法律や診断について聞いてくることもあるし。ソフトウェアはまだ少し職人技的なところがあって、建設現場で「釘を打ってみて」って聞くのは全然合理的だよね。でも、その後に「じゃあ、Lジョイントもやってみて」なんて言う人はいないよ。

このインタビューは、面接官が「JavaScriptでやります」と言ってから、明らかにJavaScriptじゃないコードを紹介するところから始まるんだ。「declare function send ( payload: P, callback: () => void ): void;」って。面接官の準備レベルに自信が持てないよね。

候補者が混乱して、面接官が自分はすごいと思っている限り、世の中はうまくいってるってことだね。

ええ、実装は全部JavaScriptで、どの言語でもアプローチできるよ。彼らは候補者が何を扱っているか分かるように、型付きの関数シグネチャを提供してるだけ。しかも、シグネチャはTypeScriptで、面接の文脈ではそれほど遠くないよ。純粋なJSコードベースでも、IDEがパッケージのTS定義を引っ張って基本的な型チェックを提供するのは珍しくないし、純粋なJSライブラリでも通常はドキュメントに型付きシグネチャを提供してる。むしろ、これは面接官が準備していることを示していると思うよ。候補者が質問を完了するために必要なものを確保してるからね。

また他の言語(擬似コードでも)

実装がちょっとごちゃごちゃしてる感じがするな。プロミス使っちゃダメなの?数ヶ月のJavaScript経験しかない人には単純すぎる解決策に思えるけど、これってズルなのかな? const PromiseQueue = { queue: Promise.resolve(true), sendOnce(request) { return new Promise((resolve, reject) => { this.queue = this.queue .then(request) .then(resolve) .catch(reject) }) } }

いいね!最小限の遅延はPromise.raceで実現できるよ。

実際、このパターンを使ってオーディオコントローラーのインターフェースをもっと良くしようとしたんだけど、キューが長くなるとエラーが出始めるんだよね(正確なメッセージは忘れちゃったけど、最大再帰深度に似た感じだった)。

で、The Anyoneがもう少し経験を積むと、またごちゃごちゃしたやつに戻っちゃうんだよね。

これがうまくいくのは信じてるけど、かなりの努力をしないと簡単には読めないし理解できないから、自分が理解できる解決策よりも低い評価をつけるかな。

メンテナブルなコードには、簡潔さよりも可読性が大事だと思ってる(面接では求められないけど、私は重視してる)。これがうまくいくけど、直感的ではないよね。

このアプローチを使うなら、定期的にプロミスをフラットにしないと、キューに何かを追加するたびにパフォーマンスがちょっとずつ落ちちゃうよ。

候補者を面接する時は、主に彼らがやってきたことに注目してる。理想を言えば、事前に見られる作品がオンラインにあるといいんだけどね。それから、高レベルのシステムデザインを一緒に話し合うんだ。最後に、かなりシンプルなコーディングの質問を出して、彼らが本当にできるか確認するだけ。大体、みんなできないことが多いけどね。面接官がよくやる間違いは、候補者にパズルを解かせようとすること。あの「ハッ!」って瞬間を求めるんだよね。でも、これはリラックスしたデスクワークだから、そんなの必要ないよ(笑)。一度、コーディングでダメだった人が、数時間後にクリーンな答えをメールしてきたことがあったけど、彼は最高の採用だった。

私も同じプロセスを踏んでるよ。事前にどんな質問があるか説明して、トリッキーな質問はないって強調する。彼らの経験や興味のある分野、コード設計の好み、コードの品質やユーザーサポートへの取り組みについて知りたいだけなんだ。こうやって採用して大きな問題はなかったけど、あなたが言ったように、全然ダメそうな人や、GitHubや会社のGitLab、DockerHub、ResearchGateなどに何も公開してない人は拒否したことがある。入門レベルのポジションや、研究や政府で働いていて公開できない人には例外を設けてるけど、通常は研究出版物や会議、技術ノートに参加してることが多いよ。

私もこれを一度やったことがある!「Battleshipを作成する」という宿題を出されたんだ。普通なら「うん、そんな時間ないよ」って言うけど、実はそのゲームが大好きで、コードでどうモデル化するか考えたことがなかったから、挑戦することにした。クラスやインターフェースをホワイトボードに書いたり、テストを書いたりして、最終的にはしっかりした解決策ができた。コードを提出する時に、「これ、セットデータ構造で簡単に処理できるかも」と思ったんだ。送信ボタンを押した後に、「これ、すごくシンプルな解決策があるよ」とフォローアップして、メールの本文に例のコードをタイプした。問題を放っておけない時が好きなんだ。採用担当者もそれを評価してくれた!

誰かがその作品をプロとして作ったら、何を違った風にやったかを聞くのはいいアイデアだと思う。実験やサイドプロジェクトでは優先順位が違うことが多いからね。何かを作った理由が、どうやって作ったかよりも意味があることもあると思う。

コーディングはセックスみたいなもので、他人の前でやるとパフォーマンス不安になりやすいよね。

これは見た中で最悪の面接質問ではないけど、改善の余地はあるね。名前の付け方がかなり混乱してる。Async queue、send once、send manyは、何をしようとしているのかをうまく説明してないと思う。これが会社の実際のコードベースを反映していないことを願うよ。ちょっとした赤信号だね。また、これはJSの質問ではないとフレームされているのに、面接官はJSでしか意味がない答えを求めてる。しかも、現代のJSでもないし。いくつかの赤信号がある。こういう質問は一般的に嫌いだけど、良い会話を促進する面接もあった。結局、ケースバイケースだね。これはただのブログ投稿だから、著者の実際の面接スタイルについて多くを推測するのは難しいけど、素晴らしくて協力的かもしれない。ただ、私が今まで働いた中で最悪のエンジニアたちの面接スタイルを思い出させるね…。

へへ。何がそんなに騒がれてるのか分からないけど、AsyncQueueはJSでは結構クールだと思う。時々使ってるよ、for awaitでイテレートできる非同期ジェネレーターを実装するために。実装にはシーケンシングはないけど、必要なかったし、もっと重要なのはリトライとタイムアウトがあること。リトライはもう少し上のレベルで実装したかもしれないけど。多分、こういう質問を楽しめる珍しい人間の一人なんだろうな、私のバージョンを自慢するチャンスとして。

せっかくだから、私のバージョンも見せるね。フィードバックがあれば教えて!

export interface AsyncQueueOptions { timeoutSeconds?: number }
export class AsyncQueue {
    private readonly queue: Promise[] = []
    readonly timeoutSeconds: number
    private timeout: ReturnType | undefined
    private reject = () => {}
    private resolve = (value: T | PromiseLike) => {}

    /**
     * @param timeoutSeconds
     * @default 25
     */
    constructor({ timeoutSeconds = 25 }: AsyncQueueOptions = {}) {
        this.timeoutSeconds = timeoutSeconds
        if (timeoutSeconds > 100) {
            console.warn(`100秒以上のタイムアウトでAsyncQueueを初期化しています: ${timeoutSeconds}`)
        }
        this.queue.push(
            new Promise((resolve, reject) => {
                this.resolve = resolve
                this.reject = reject
                this.timeout = setTimeout(() => resolve(undefined), timeoutSeconds * 1000)
            })
        )
    }

    next(): Promise | undefined {
        return this.queue.shift()
    }

    push(msg: T) {
        this.resolve(msg)
        this.queue.push(
            new Promise((resolve, reject) => {
                this.resolve = resolve
                this.reject = reject
                clearTimeout(this.timeout)
                this.timeout = setTimeout(() => resolve(undefined), this.timeoutSeconds * 1000)
            })
        )
    }

    close(msg?: T) {
        if (msg) {
            this.resolve(msg)
        }
        clearTimeout(this.timeout)
    }
}

「でも、そのサーバーは故障してるよ!!複数のリクエストを同時に処理しなきゃいけないと、壊れ始めるんだ。」うん、これは全部仮定だって分かってるけど、その前提は納得できないな。なんでサーバーが故障してるの?どんな風に失敗してるの?どうしてそれが同時に複数のリクエストを処理してるからだって分かるの?もし、各クライアントが一つのリクエストだけしてたらどうするの?サーバーのコードをコントロールできるの?できるなら、そこで直せばいいじゃん!--- 要するに、この解決策は間違った問題を直して、新たな問題を引き起こしてるってこと。サーバーのバグが直ったら、クライアントに人工的なパフォーマンスボトルネックを作っちゃうことになるよ。それについて知ってる開発者は組織を辞めるだろうし、他の人はその周りのコードを「最適化」しようとするだろうね。こういう修正を許すことで、間違った文化を作ってるんだよ。常に根本的な問題を直すべき。あるいは問題の前提を変えるべきだね。