내가 동기 apis와 스레드 풀을 사용하여 이런 식으로 보이는 TCP 서버에서 일하고 :
TcpListener listener;
void Serve(){
while(true){
var client = listener.AcceptTcpClient();
ThreadPool.QueueUserWorkItem(this.HandleConnection, client);
//Or alternatively new Thread(HandleConnection).Start(client)
}
}
내 목표는 리소스 사용량이 가장 적은 동시 연결을 가능한 한 많이 처리하는 것이라고 가정 할 때 사용 가능한 스레드 수에 따라 재빨리 제한되는 것 같습니다. Non-blocking Task API를 사용하면 더 적은 자원으로 더 많은 것을 처리 할 수있을 것으로 생각됩니다.
저의 첫인상은 다음과 같습니다.
async Task Serve(){
while(true){
var client = await listener.AcceptTcpClientAsync();
HandleConnectionAsync(client); //fire and forget?
}
}
그러나 이것은 병목 현상을 일으킬 수 있다고 생각합니다. 아마도 HandleConnectionAsync는 첫 번째 기다리기에 비정상적으로 긴 시간이 걸릴 것이고 주 수락 루프가 진행되지 않게 할 것입니다. 이것은 오직 하나의 쓰레드만을 사용합니까, 아니면 런타임에 마술처럼 여러 쓰레드에서 일을 실행합니까?
이 두 가지 접근 방식을 결합하여 서버가 실제로 실행중인 작업의 수만큼 필요한 스레드 수를 사용하지만 IO 작업에서 불필요하게 스레드를 차단하지 않도록 할 수 있습니까?
이와 같은 상황에서 처리량을 극대화하는 관용적 인 방법이 있습니까?
프로파일 링 테스트에서 필자가 필요로하지 않는다면 프레임 워크가 스레딩을 관리하게하고 추가 스레드를 생성하지 않을 것입니다. 특히, 내부 통화HandleConnectionAsync
대부분 IO 기반입니다.
어쨌든, 시작 부분에 호출 스레드 (디스패처)를 해제하려면HandleConnectionAsync
, 매우 쉬운 해결책이 있습니다.에서 새 스레드에 이동할 수 있습니다.ThreadPool
와await Yield()
.이것은 서버가 일반적으로 TCP 서버의 경우 인 초기 스레드 (콘솔 응용 프로그램, WCF 서비스)에 설치된 동기화 컨텍스트가없는 실행 환경에서 실행되는 경우 작동합니다.
[편집 됨]다음은 이것을 설명합니다 (코드는 원래이리). 참고로, 메인while
루프는 명시 적으로 스레드를 생성하지 않습니다.
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
class Program
{
object _lock = new Object(); // sync lock
List<Task> _connections = new List<Task>(); // pending connections
// The core server task
private async Task StartListener()
{
var tcpListener = TcpListener.Create(8000);
tcpListener.Start();
while (true)
{
var tcpClient = await tcpListener.AcceptTcpClientAsync();
Console.WriteLine("[Server] Client has connected");
var task = StartHandleConnectionAsync(tcpClient);
// if already faulted, re-throw any error on the calling context
if (task.IsFaulted)
task.Wait();
}
}
// Register and handle the connection
private async Task StartHandleConnectionAsync(TcpClient tcpClient)
{
// start the new connection task
var connectionTask = HandleConnectionAsync(tcpClient);
// add it to the list of pending task
lock (_lock)
_connections.Add(connectionTask);
// catch all errors of HandleConnectionAsync
try
{
await connectionTask;
// we may be on another thread after "await"
}
catch (Exception ex)
{
// log the error
Console.WriteLine(ex.ToString());
}
finally
{
// remove pending task
lock (_lock)
_connections.Remove(connectionTask);
}
}
// Handle new connection
private async Task HandleConnectionAsync(TcpClient tcpClient)
{
await Task.Yield();
// continue asynchronously on another threads
using (var networkStream = tcpClient.GetStream())
{
var buffer = new byte[4096];
Console.WriteLine("[Server] Reading from client");
var byteCount = await networkStream.ReadAsync(buffer, 0, buffer.Length);
var request = Encoding.UTF8.GetString(buffer, 0, byteCount);
Console.WriteLine("[Server] Client wrote {0}", request);
var serverResponseBytes = Encoding.UTF8.GetBytes("Hello from server");
await networkStream.WriteAsync(serverResponseBytes, 0, serverResponseBytes.Length);
Console.WriteLine("[Server] Response has been written");
}
}
// The entry point of the console app
static void Main(string[] args)
{
Console.WriteLine("Hit Ctrl-C to exit.");
new Program().StartListener().Wait();
}
}
또는 코드는 다음과 같을 수 있습니다.await Task.Yield()
. 참고, 나는 패스한다.~async
람다Task.Run
내가 아직도하고 싶어서.내부의 비동기 API 활용HandleConnectionAsync
사용await
거기에:
// Handle new connection
private static Task HandleConnectionAsync(TcpClient tcpClient)
{
return Task.Run(async () =>
{
using (var networkStream = tcpClient.GetStream())
{
var buffer = new byte[4096];
Console.WriteLine("[Server] Reading from client");
var byteCount = await networkStream.ReadAsync(buffer, 0, buffer.Length);
var request = Encoding.UTF8.GetString(buffer, 0, byteCount);
Console.WriteLine("[Server] Client wrote {0}", request);
var serverResponseBytes = Encoding.UTF8.GetBytes("Hello from server");
await networkStream.WriteAsync(serverResponseBytes, 0, serverResponseBytes.Length);
Console.WriteLine("[Server] Response has been written");
}
});
}
[최신 정보]주석에 기반하여 : 이것이 라이브러리 코드가 될 것이라면, 실행 환경은 실제로 알려지지 않았고 기본이 아닌 동기화 컨텍스트를 가질 수 있습니다. 이 경우에는 (모든 동기화 컨텍스트가없는) 풀 스레드에서 주 서버 루프를 실행하는 것이 좋습니다.
private static Task StartListener()
{
return Task.Run(async () =>
{
var tcpListener = TcpListener.Create(8000);
tcpListener.Start();
while (true)
{
var tcpClient = await tcpListener.AcceptTcpClientAsync();
Console.WriteLine("[Server] Client has connected");
var task = StartHandleConnectionAsync(tcpClient);
// if already faulted, re-throw any error on the calling context
if (task.IsFaulted)
task.Wait();
}
});
}
이렇게하면 내부에서 생성 된 모든 하위 작업StartListener
클라이언트 코드의 동기화 컨텍스트에 영향을받지 않습니다. 그래서 나는 전화 할 필요가 없다.Task.ConfigureAwait(false)
어디서나 명시 적으로.
await Task.Yeild()
). 그 이유는 syn가 있기 때문입니다. UI 스레드의 컨텍스트는 다음을 사용합니다.PostMessage
이 깊은 내부는 마우스 및 키보드와 같은 다른 사용자 입력 메시지를 인계하고 UI를 차단할 수 있습니다. 이 모든 것은 상황이없는 환경에는 적용되지 않습니다.Task.Yield()
단지 사용ThreadPool.QueueUserWorkItem
편리한 지름길입니다. 몇 가지 추가 정보 :stackoverflow.com/q/20319769/1768303 - noseratioTask.Run
코드 안에 있습니까? 내 이해로서 이것은 주로 I / O 작업으로TCPClient
데이터를 수신합니다. 왜 새로운 것이 있어야 하는가?ThreadPool
쓰레드 파이어 링while (true)
고리? - Yuval Itzchakovawait tcpListener.AcceptTcpClientAsync().ConfigureAwait(false)
. - noseratio
기존 답변을 올바르게 사용하도록 제안했습니다.Task.Run(() => HandleConnection(client));
그러나 이유는 설명하지 않았습니다.
이유는 다음과 같습니다.HandleConnectionAsync
첫 번째를 기다리는 데 약간의 시간이 걸릴 수 있습니다. 비동기 IO를 사용하는 경우 (이 경우처럼) 이는HandleConnectionAsync
차단하지 않고 CPU 바운드 작업을 수행하고 있습니다. 이것은 스레드 풀에 대한 완벽한 경우입니다. 짧은 논 블로킹 CPU 작업을 실행하도록 만들어졌습니다.
그리고 여러분은 맞습니다. accept 루프는HandleConnectionAsync
돌아 오기 전에 오랜 시간이 걸릴 것입니다 (어쩌면 CPU에 상당한 작업이 있기 때문일 수도 있습니다). 새로운 연결 빈도가 높아야하는 경우에는이를 피하십시오.
루프를 조절하는 중요한 작업이 없다고 확신하는 경우 추가 스레드 풀을 저장할 수 있습니다Task
하지 마라.
또는 동시에 여러 수락을 실행할 수 있습니다. 바꾸다await Serve();
(예를 들어) :
var serverTasks =
Enumerable.Range(0, Environment.ProcessorCount)
.Select(_ => Serve());
await Task.WhenAll(serverTasks);
이렇게하면 확장 성 문제가 해결됩니다.주목해라.await
여기에 하나의 오류를 제외한 모든 것을 삼킨다.
시험
TcpListener listener;
void Serve(){
while(true){
var client = listener.AcceptTcpClient();
Task.Run(() => this.HandleConnection(client));
//Or alternatively new Thread(HandleConnection).Start(client)
}
}
Microsoft에 따르면http://msdn.microsoft.com/en-AU/library/hh524395.aspx#BKMK_VoidReturnType예외를 캐치 할 수 없기 때문에 void 반환 형식을 사용하면 안됩니다. 필자가 지적했듯이 "불과 잊기"작업이 필요하므로 내 결론은 작업 (Microsoft가 말했듯이)을 항상 반환해야한다는 것입니다. 그러나 다음을 사용하여 오류를 잡아야합니다.
TaskInstance.ContinueWith(i => { /* exception handler */ }, TaskContinuationOptions.OnlyOnFaulted);
증거로 사용한 예가 아래에 있습니다.
public static void Main()
{
Awaitable()
.ContinueWith(
i =>
{
foreach (var exception in i.Exception.InnerExceptions)
{
Console.WriteLine(exception.Message);
}
},
TaskContinuationOptions.OnlyOnFaulted);
Console.WriteLine("This needs to come out before my exception");
Console.ReadLine();
}
public static async Task Awaitable()
{
await Task.Delay(3000);
throw new Exception("Hey I can catch these pesky things");
}
비동기 연결을 수락해야하는 이유가 있습니까? 내 말은, 어떤 클라이언트 연결을 기다리는 것이 당신에게 어떤 가치를 부여 할까? 이 작업을 수행하는 유일한 이유는 연결을 기다리는 동안 서버에서 진행중인 다른 작업이 있기 때문입니다. 만약 당신이 아마 이런 일을 할 수있다 :
public async void Serve()
{
while (true)
{
var client = await _listener.AcceptTcpClientAsync();
Task.Factory.StartNew(() => HandleClient(client), TaskCreationOptions.LongRunning);
}
}
이 방법은 accepting은 다른 것들을위한 현재의 thread leave 옵션을 해제 할 것이고, 핸들링은 새로운 쓰레드에서 수행 될 것이다. 유일한 오버 헤드는 새로운 연결을 받아들이 기 직전에 클라이언트를 처리하기위한 새로운 스레드를 생성하는 것입니다.
편집하다: 당신이 쓴 코드와 거의 같은 코드라는 것을 깨달았습니다. 내가 실제로 묻는 내용을 더 잘 이해하려면 질문을 다시 읽어야한다고 생각합니다. S
편집 2 :
이 두 가지 접근 방식을 결합하여 내 서버에서 활발히 실행되는 작업의 수에 대해 필요한 스레드의 수는 IO 작업에 불필요하게 스레드를 차단하지 않습니까?
내 솔루션이 실제로이 질문에 대답한다고 생각하십시오. 그래도 정말 필요한가요?
편집 3 : Task.Factory.StartNew ()는 실제로 새 스레드를 만듭니다.