【问题标题】:Multiple tasks returns incorrect result多个任务返回不正确的结果
【发布时间】:2018-11-06 17:56:42
【问题描述】:

我需要做什么

我需要使用异步方法在同步上下文中启动一个类的不同实例。

应用结构

在我的console 应用程序中,我声明了一个List<Bot> 类:

private List<Bot> _bots = new List<Bot>(new Bot[10]);

Bot 类包含一些从 Internet 获取数据的方法,因此需要等待这些方法。方法结构如下:

public class Bot
{
    Competition Comp { get; set; }

    public async Task StartAsync(int instance) 
    {
         string url = "";

         //based on the instance I take the data from different source.
         switch(instance)
         {
             case 0:
                url = "www.google.com";
                break;
             case 1:
                url = "www.bing.com";
                break;
         }

         //Comp property contains different groups.
         Comp.Groups = await GetCompetitionAsync(Comp, url);

         if(Comp.Groups.Count > 0)
         {
             foreach(var gp in group)
             {
                //add data inside database.
             }
         }
     }
 }

Competition 类具有以下设计:

public class Competition 
{
    public string Name { get; set; }
    public List<string> Groups { get; set; } 
}

我使用以下代码启动 Bot 类的所有实例:

for(int i = 0; i < _bots.Count - 1; i++)
{
   _bots[i].StartAsync(i);
}

这段代码将调用Bot类的不同时间StartAsync,这样,我可以管理机器人的每个实例,并且我最终可以在单独的方法中停止或启动特定实例。

问题

方法GetCompetitionAsync创建一个List&lt;string&gt;

public async Task<List<string>> GetCompetitionAsync(Competition comp, string url)
{
     if(comp == null)
        comp = new Competition();

     List<string> groups = new List<string();

     using (var httpResonse = await httpClient.GetAsync(url))
     {
        string content = await httpResponse.Content.ReadAsStringAsync();
        //fill list groups
     }

     return groups;
}

基本上,此方法将填充Comp 中可用的List&lt;string&gt;。现在,如果我执行StartAsync 的单个实例一切正常,但是当我运行多个实例(如上)时,Comp 对象(包含Competition)具有NULL 的所有属性。

似乎当我有多个Task 运行synchronous 上下文时,不会等待async 上下文,在这种情况下填充List&lt;string&gt;。 当代码到达这一行时:if(Competition.Groups.Count &gt; 0) 我得到一个NULL 异常,因为Groups 为空,而其他Comp 属性为NULL

我该如何处理这种情况?

更新

经过其他尝试,我想创建一个List&lt;Task&gt; 而不是List&lt;Bot&gt;

List<Task> tasks = new List<Task>(new Task[10]);

然后代替:

for(int i = 0; i < _bots.Count - 1; i++)
{
   _bots[i].StartAsync(i);
}

我做到了:

for (int i = 0; i < tasks.Count - 1; i++)
{
    Console.WriteLine("Starting " + i);

    if (tasks[i] == null)
        tasks[i] = new Task(async () => await new Bot().StartAsync(i));

显然一切运行良好,我没有出错。问题是:为什么?我想像deadlock 这样的东西,我什至无法使用ConfigureAwait(false); 解决。

最后一个解决方案也不允许我访问Bot 方法,因为现在是Task

更新 2

好吧,也许我遇到了问题。本质上,异步方法StartAsync中的await试图在主线程上恢复,同时主线程正忙于等待任务完成,这将创建一个deadlock

这就是为什么将StartAsync() 移动到List&lt;Task&gt; 中起作用的原因,因为现在async 调用现在在线程池线程上运行,它不会尝试返回到主线程,一切看起来工作。但由于上述原因,我无法使用此解决方案。

【问题讨论】:

  • 向 Competition 类添加一个不接受参数的构造函数,并在该构造函数中将您的 List&lt;string&gt; 属性初始化为 new List&lt;string&gt;,这应该可以消除 null 异常。
  • @ryanwilson 是的,但这并不能解决问题。
  • @RyanWilson 这无论如何都无法解决问题,如果我启动一个实例,Groups 会正确填充,如果我有多个实例正在运行,我会收到错误,所以你的解决方案是一个修复,但没有真正的修复
  • 我写一个好问题的所有努力都白费了,我真的不知道为什么我得到了反对票,至少解释一下
  • 我认为您的示例中有错误。 async Task GetCompetitionAsync 不会编译。它不应该返回Task&lt;List&lt;string&gt;&gt;吗?

标签: c# multithreading console task


【解决方案1】:

我更喜欢使用线程而不是任务。恕我直言,线程更易于理解。 注意:您的代码中的属性 Bot.Comp 似乎未初始化!我解决了这个问题。 我的代码版本:

public class Bot
{
    Competition Comp { get; set; }
    System.Thread _thread;
    private int _instance;

    public Bot()
    {
        Comp = new Competition ();
    }
    public void Start(int instance) 
    {
        _instance = instance;
        _thread = new Thread(StartAsync);
        _thread.Start();
    }

    private void StartAsync() 
    {
         string url = "";

         //based on the instance I take the data from different source.
         switch(_instance)
         {
             case 0:
                url = "www.google.com";
                break;
             case 1:
                url = "www.bing.com";
                break;
         }

         //Comp property contains different groups.
         GetCompetitionAsync(Comp, url);

         if(Comp.Groups.Count > 0)
         {
             foreach(var gp in group)
             {
                //add data inside database.
             }
         }
     }

     public List<string> GetCompetitionAsync(Competition comp, string url)
     {
          if(comp.groups == null)  comp.groups = new List<string>();

          using (var httpResonse = httpClient.GetAsync(url))
          {
             string content = await httpResponse.Content.ReadAsStringAsync();
             //fill list groups
          }
          return groups;
     }
}

然后我们运行线程:

for(int i = 0; i < _bots.Count - 1; i++)
{
   _bots[i].Start(i);
}

每个 Bot 实例在它自己的线程中启动方法 private void StartAsync()。

注意方法 Bot.Start() 的实现:

public void Start(int instance) 
{
    _instance = instance;
    _thread = new Thread(StartAsync); //At this line: set method Bot.StartAsync as entry point for new thread.
    _thread.Start();//At this line: call of _thread.Start() starts new thread and returns **immediately**.
}

【讨论】:

  • 感谢您的解决方案。我什么都不懂:您的代码运行Bot? 的单个实例?我应该为机器人的“n”个实例创建不同的线程,例如:9 - 10。
  • 我试过你的代码,似乎是我最后的解决方案。我必须做其他测试才能确定,但​​与此同时 +1
【解决方案2】:

如果您从列表和“纯”函数(接受输入并返回输出的函数)的角度来考虑,这种事情要简单得多。不要传递一些东西让他们填充或变异。

例如,此函数接受一个字符串并返回组:

List<string> ExtractGroups(string content)
{
    var list = new List<string>();
    //Populate list
    return  list;
}

此函数接受 URL 并返回其组。

async Task<List<string>> GetCompetitionAsync(string url)
{
    using (var httpResponse = await httpClient.GetAsync(url))
    {
        string content = await httpResponse.Content.ReadAsStringAsync();
        return ExtractGroups(content);
    }
 }

此函数接受一个 URL 列表并将所有组作为一个列表返回。

async Task<List<string>> GetAllGroups(string[] urls)
{
    var tasks = urls.Select( u => GetCompetitionAsync(u) );
    await Task.WhenAll(tasks);
    return tasks.SelectMany( t => t.Result );
}

然后您可以按照您的计划将数据填充到数据库中。

var groups = GetAllGroups( new string[] { "www.google.com", "www.bing.com" }  );        
foreach(var gp in groups)
{
    //add data inside database.
}

看看用这种方式分解它有多简单?

【讨论】:

  • 感谢您的回答,所以您建议编辑填充组的方法?如果是这样:为什么当我执行单个实例时一切正常,而多个实例我有空问题?我不太确定这是一个“方法结构”问题,但我可能是错的
  • 是的,我愿意。这是我们从函数式编程范式中学到的东西:当您使用可以变异的变量时,您最终可能会遇到并发问题,而当您消除它们时,它们就完全消失了。另一种方法是使用锁和 ConcurrentBag 或类似的东西,它的性能不会那么好,而且代码也不是那么漂亮。如果可行,我强烈建议您改变方法并采用这种思维方式。
  • 你能检查我的更新吗?我想我发现了真正的问题
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-01-23
  • 2012-04-11
  • 2016-02-06
  • 2017-05-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多