我正在编写一个需要不断从多个WebSocket读取数据的.NET Core Console应用程序。我目前的方法是为每个WebSocket创建一个新的Task(通过Task.Run),它运行一个无限循环并阻塞,直到它从套接字读取数据。但是,由于数据以相当低的频率被推送,因此线程只会阻塞大部分时间,这似乎效率很低。
根据我的理解,async / await模式应该是阻止I / O操作的理想选择。但是,我不确定如何将它应用于我的情况,或者即使async / await可以以任何方式改进它 - 特别是因为它是一个控制台应用程序。
我已经将概念证明放在一起(为了简单起见,执行HTTP GET而不是从WebSocket读取)。我能够实现这一目标的唯一方法是实际上没有等待。代码:
static void Main(string[] args)
{
Console.WriteLine($"ThreadId={ThreadId}: Main");
Task task = Task.Run(() => Process("https://duckduckgo.com", "https://stackoverflow.com/"));
// Do main work.
task.Wait();
}
private static void Process(params string[] urls)
{
Dictionary<string, Task<string>> tasks = urls.ToDictionary(x => x, x => (Task<string>)null);
HttpClient client = new HttpClient();
while (true)
{
foreach (string url in urls)
{
Task<string> task = tasks[url];
if (task == null || task.IsCompleted)
{
if (task != null)
{
string result = task.Result;
Console.WriteLine($"ThreadId={ThreadId}: Length={result.Length}");
}
tasks[url] = ReadString(client, url);
}
}
Thread.Yield();
}
}
private static async Task<string> ReadString(HttpClient client, string url)
{
var response = await client.GetAsync(url);
Console.WriteLine($"ThreadId={ThreadId}: Url={url}");
return await response.Content.ReadAsStringAsync();
}
private static int ThreadId => Thread.CurrentThread.ManagedThreadId;
这似乎在ThreadPool上的各种工作线程上工作和执行。然而,这绝对不像任何典型的异步/等待代码,这让我觉得必须有更好的方法。
有更合适/更优雅的方式吗?
答案 0 :(得分:1)
您基本上编写了Task.WhenAny
版本,它使用CPU循环来检查已完成的任务,而不是......框架方法在幕后使用的任何魔法。
更惯用的版本可能如下所示。 (虽然它可能没有 - 我觉得应该有一个更简单的方法来重新运行完成的任务&#34;而不是我在这里使用的反向词典。)
static void Main(string[] args)
{
Console.WriteLine($"ThreadId={ThreadId}: Main");
// No need for Task.Run here.
var task = Process("https://duckduckgo.com", "https://stackoverflow.com/");
task.Wait();
}
private static async Task Process(params string[] urls)
{
// Set up initial dictionary mapping task (per URL) to the URL used.
HttpClient client = new HttpClient();
var tasks = urls.ToDictionary(u => client.GetAsync(u), u => u);
while (true)
{
// Wait for any task to complete, get its URL and remove it from the current tasks.
var firstCompletedTask = await Task.WhenAny(tasks.Keys);
var firstCompletedUrl = tasks[firstCompletedTask];
tasks.Remove(firstCompletedTask);
// Do work with completed task.
try
{
Console.WriteLine($"ThreadId={ThreadId}: URL={firstCompletedUrl}");
using (var response = await firstCompletedTask)
{
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine($"ThreadId={ThreadId}: Length={content.Length}");
}
}
catch (Exception ex)
{
Console.WriteLine($"ThreadId={ThreadId}: Ex={ex}");
}
// Queue the task again.
tasks.Add(client.GetAsync(firstCompletedUrl), firstCompletedUrl);
}
}
private static int ThreadId => Thread.CurrentThread.ManagedThreadId;
答案 1 :(得分:0)
我接受了Rawling的答案 - 我认为这对我所描述的确切情况是正确的。然而,通过一些倒置的逻辑,我最终得到了一些更简单的东西 - 如果有人需要这样的话,请留下它:
static void Main(string[] args)
{
string[] urls = { "https://duckduckgo.com", "https://stackoverflow.com/" };
HttpClient client = new HttpClient();
var tasks = urls.Select(async url =>
{
while (true) await ReadString(client, url);
});
Task.WhenAll(tasks).Wait();
}
private static async Task<string> ReadString(HttpClient client, string url)
{
var response = await client.GetAsync(url);
string data = await response.Content.ReadAsStringAsync();
Console.WriteLine($"Fetched data from url={url}. Length={data.Length}");
return data;
}
答案 2 :(得分:-1)
也许更好的问题是:在这种情况下你真的需要每个插槽的线程吗?您应该将线程视为系统范围的资源,并且在生成它们时应该考虑到这一点,特别是如果您还不知道应用程序将使用的线程数。这是一个很好的阅读:What's the maximum number of threads in Windows Server 2003?
几年前.NET团队推出了异步套接字。
...客户端是用异步套接字构建的,所以执行 服务器返回时,客户端应用程序不会被挂起 响应。应用程序将一个字符串发送到服务器然后 显示服务器在控制台上返回的字符串。
Asynchronous Client Socket Example
有很多例子展示了这种方法。虽然它有点复杂和低水平&#34;它让你掌控一切。