使用async / await从多个WebSockets读取

时间:2018-03-25 03:06:07

标签: c# async-await .net-core console-application

我正在编写一个需要不断从多个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上的各种工作线程上工作和执行。然而,这绝对不像任何典型的异步/等待代码,这让我觉得必须有更好的方法。

有更合适/更优雅的方式吗?

3 个答案:

答案 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;它让你掌控一切。