サーバーからユーザーに通知を送信するC#アプリケーション(モバイルアプリ)を構築しようとしています。各ユーザーはどのスレッドでも通知コレクションを変更できるため、誰かが通知の読み取り、追加、削除を試みるたびに、これらのコレクションをロックする必要があります。
同時にログインしているユーザーの数が多ければ(おそらく数百万人ですが)直面する可能性がある問題は、コレクションごとに個別のロックを保持する必要があることですが、プロセスが保持できるのは制限されていますハンドルの数と私が持っていることが許可されているよりも多くのロックが必要になります。
これは本当の問題ですか、それとも私は何も心配していませんか?これに対するより良い解決策はありますか?
多くのロックを使用すると、いくつかの問題が発生する可能性があります。
Mutex
やWaitHandle
から派生した他のすべての同期プリミティブなどのプロセス間ロックの数は、OSによって制限されます。Compare-And-Swapキャッシュラインロック操作(クラスInterlocked
を介して提供される.net内)はCPU命令であり、制限なく使用できます。重要なセクション(c#のキーワードlock
によって提供される)および.netのMonitor
クラス)は、おそらくReaderWriterLockSlim
およびSemaphoreSlim
(Greeble31のコメントで追加された)のように、使用可能なメモリによってのみ制限されます。代替案:
コメントで述べたように、スケーリングすると、状態をメモリに保持しようとして問題が発生します。サーバーがクラッシュするとどうなりますか?すべてのユーザーが通知を失いますか?スケールアップすると、通常は通知を保持するデータベースが導入されます。データベースは、あなたが説明した単純なケースでロックを処理するので、データベースが大きくなり始めるまで心配する必要はありません。
インプロセスの同時実行性に関しては、ロックの代替手段がいくつかあります。
並行データ構造は、説明した問題のロックの実装に役立ちます。通知を並行コレクションに保持し、自分でデッドロックすることなく、別のスレッドから通知にアクセスできます。ロックを自分で実装するのではなく、可能な場合はこれらを使用することをお勧めします。誤って実装するか、最適に実装しない可能性が高くなるためです。
システムは、メッセージを介して通信する一連のアクターとしてモデル化されます。メッセージの順序とメッセージの処理については、並行システムとロックの合理性を劇的に改善する保証があります。メッセージは順次処理されるため、スループットが低下する可能性がありますが、適切に実行するとデッドロックが発生せず、状態が安全でない状態でアクセスされる可能性が低くなります(状態はアクターにプライベートであるため)。
楽観的並行性では、長期間ロックを保持しません。代わりに、クライアントは最後に状態を読み取ったときからの状態のバージョンを追跡します。次に、状態を設定しようとするときに、バージョンがまだ一致していることを確認します。バージョンが一致する場合は、状態を設定します。バージョンの不一致がある場合は、別のスレッドがすでに状態を変更していることを意味します。次に、再試行ポリシーを適用して、最新の状態をフェッチし、変更を適用して状態の設定を試みることにより、変更を再試行できます。これは、読み取りより書き込みがはるかに少ない場合に適したアプローチです。これは、ほとんどのリクエストが読み取りである場合、ロックおよびアクターモデルよりもスループットが向上します。
むしろnoですが、あなたの質問は根本的な概念の誤設計を示唆しているので...はい。
リソースがほとんどなく、ハンドルが少ない(またはまったくない)軽量のロックは簡単に実装できるため、実際には「いいえ」です。 "最大で12のスレッド"と"100万のロック可能なもの"のような短いロック期間の超低輻輳状況では、スピンロックが最適です。
回転が怖い場合(このような状況ではいけません-回転が悪いのは、競合がある場合のみです)、プロセス全体の単一のハンドルをブロックして消費するロックを構築できます。 WindowsではNtWaitForKeyedEvent
の周りに、ブロックして必要なロックまったくハンドルなしは、WindowsではWaitOnAddress
の周りに構築できます> = 8、またはfutex
Linux> = 2.6システム。それは超軽量であり、文字通り数百万個も持つことができます(ただし、実際にはwantはそれほど多くありませんが)。
何百万もの同時ユーザーはnotサーバー上で実行されている何百万ものスレッドであることに注意してください。一般に、同時ユーザーに関しては、とにかく「100万」ではなく「10,000から50,000」を考えるとよいでしょう。前者は、完全なサーバーのファームがない限り、非常に非現実的だからです。それは...ええと...完全に別の獣です。いずれにせよ、これらのために必要な並行スレッドはごくわずかです。
また、何百万ものユーザーがnotサーバー上の何百万もの特異でロック可能なコレクション(またはもの)です。通常は、少なくとも。通常、それらすべてを含むデータベースのようなものが存在します(あなたに言っているわけではありませんできませんでした別の方法で行います、それは非常に珍しいことです)。 Nitpick:はい、行レベルのロックは一部のデータベースが実装している機能であるため、そうですcanいずれかの方法で、知らないうちに何百万ものロックが発生します。
読み取りと更新の頻度に応じて、ロックを回避するread-copy-updateなどの手法も検討できます。 RCUはハンドルを必要としません。あなたはそれを行いますどちらか追加のポインターの間接参照とポインターのアトミック読み取り/書き込みを介してorカウンターを(原子的に)二重にインクリメントし、途中でリーダーを再起動することによって変更が見つかりました(つまり、2度目にカウンターが奇数の場合)。ブロックなし、ロックなし、ハンドルなし。アクセスパターンによっては、この戦略が大きなメリットになる場合があります。
多数のクライアントを処理するためのさまざまな戦略が存在します。1つは、ネットワーク関連のマルチプレクサスレッドを1つ(おそらくいくつか)持つことです。そのために、select
、poll
、GetQueuedCompletionStatus
、epoll
、およびkqueue
などの関数が、オペレーティングシステムに応じて思い浮かびます( s)あなたがターゲットにします。
C#が頻繁に(必ずしもそうとは限らないが)Windowsを示唆しているのを見ると、GetQueuedCompletionStatus
は使用したいもののように見えます(おそらくC#には、利用可能な機能の上に構築された、より高いレベルの多重化機能さえあります) 、私は知りません、C#を使用しません)。そのため、通常は1つ以上のスレッドが使用されます(正確にいくつも気にせずに6ダースを実行でき、関数は必要に応じてそれらをブロック/ウェイクします)が、それほど多くのスレッドは必要ありません。おそらくうまくいくでしょう。たとえばepoll
、単一のマルチプレクサスレッドを使用するのは通常のことです(ただし、canは何か別のことをします)。完璧に機能し、パフォーマンスの問題はありません。
最後に、ワーカーのプール(おおよそCPUコアの数)は、追加で実行する必要のある重い処理と、それらとI/Oの間で通信するためのある種の並行キューを実行します。正確に何を使用するか(ロックされたキュー、ロックフリー/ウェイトフリーキュー、ディスラプター、ビジースピンまたはイールド、またはブロックさえも)は、正確な状況によって異なります。それぞれに長所と短所があります。ロックされた並行構造は、あなたが思うほどひどいものではないことに注意してください。非常に混雑している場合を除いて、それらは実際には非常に優れています(そして、ロックフリーのものは悪夢のようですが、うまくいくのは簡単です)。
TCPではなくUDPを使用している場合(ただし、その影響を理解していることを確認してください。特に、すべての信頼性を自分で行う必要があります)、単一の専用スレッドですべての送信を実行できます。シンプルなブロッキング、1つずつの方法です。これが最も簡単な方法であり、非常に簡単で、100%ポータブルであり、イーサネットケーブルを物理的に飽和させるだけで、不要なパケット損失を引き起こしませんスレッドがものをネットワークスタックにプッシュするため同時に。
UDPを使用する場合も、準備/完了を厳密に待つ必要はありません。 1つまたは複数のワーカーからのプレーンな通常のブロッキング読み取りは完全にうまく機能し、いずれの方法でもそれ以上パフォーマンスを押し出すことはほとんどできません(1ダースのデータグラムを取得するrecvmmmsg
のような特別な「チート」関数を使用する場合を除く)行く)。
重い処理が必要なく、待ち時間が最優先されない場合(メッセージが0.5秒遅れることは問題にならない可能性があります)、シングルスレッドの数千の同時接続を処理するサーバーも実行できます。上記の多重化APIには、その処理に問題はなく、ネットワークスタックもありません。