在.net的嵌套循环中执行异步方法时出现问题

本文关键字:问题 异步方法 执行 net 嵌套循环 | 更新日期: 2023-09-27 17:51:15

我在嵌套循环中调用了一个异步函数,如下所示

var queue = new Queue<ExchangeEmailInformation>(mailInformation);
var currentQueue = queue.ToList();
foreach (var exchangeEmailInformation in currentQueue)
{
    ExchangeEmailInformation information = exchangeEmailInformation;
    foreach (var queueList in exchangeEmailInformation.Attachment)
    {
        Attachment attachment = queueList;
        information.FileName = attachment.Name;
        var emailId = information.Sender.Split('@');
        information.UserAlias = emailId[0];
        information.FileSize = attachment.Size;
        AddAttachmentAsync(information);
    }
}
private static void AddAttachmentAsync(ExchangeEmailInformation information)
{
    System.Threading.Tasks.Task.Factory.StartNew(
        () =>
        AddAttachment(information.UserAlias, information.EngagementName,
                        information.DocumentTransferId.ToString(), information.FileName,
                        information.FileSize.ToString(), information.ActivityName)).ContinueWith(
                            task => OnComplete(task, information), TaskContinuationOptions.None);
}
static void AddAttachment(string userAlias, string engagementName, string documentTranferId, string fileName, string fileSize,string activityName)
{
    Console.Writeline(fileName);
}
In the exchange information collection has one record. In these collection there is another property called Attachment which type is AttachmentCollection which contain two attachments. After calling the method AddAttachmentAsync like above asynchronously the 

结果打印为

  • SecondAttachment.txt
  • SecondAttachment.txt。

只显示第二个附件(结果不正确)。

然后我尝试像下面一样同步执行。

private static void AddAttachmentAsync(ExchangeEmailInformation information)
{
    AddAttachment(information.UserAlias, information.EngagementName,
                    information.DocumentTransferId.ToString(), information.FileName,
                    information.FileSize.ToString(), information.ActivityName);

}

结果

  • FirstAttachment.txt

  • SecondAttachment.txt

显示我想要的正确结果

如何解决这些问题?

在.net的嵌套循环中执行异步方法时出现问题

information是在嵌套循环外声明的引用类型对象。您正在将此对象传递给AddAttachmentAsync方法,但是在等待它完成(甚至开始处理Task)之前,您已经在下一次迭代中修改了information

您应该在将information发送给异步方法之前复制它。

编辑正如Marc指出的,这应该是一个新对象复制值,而不仅仅是对同一对象的新引用。

您已经修改了information的闭包

我认为这是因为您在单个外部foreach循环迭代中使用相同的ExchangeEmailInformation实例information,用于每个内部foreach循环迭代;在上一个调用有机会读取其值之前,为下一个异步调用更新该实例中的属性。

在异步情况下,事件的顺序是

  1. 为呼叫1填充information
  2. 填充information呼叫2
  3. 执行呼叫1
  4. 执行呼叫2

所以在调用1执行的时候,information已经包含了调用2的数据。在同步的情况下,这不会发生;在调用1执行完毕之前,循环不能继续。

我认为解决这个问题的最好方法是停止更改 information,并将三个曾经被更改的字段作为单独的参数传递。(实际上看起来UserAlias只需要更新一次,所以你不需要单独传递它。还请注意,如果这样做,则不需要获取queueList的副本。

ExchangeEmailInformation information = exchangeEmailInformation;
var emailId = information.Sender.Split('@');
information.UserAlias = emailId[0];
foreach (var queueList in exchangeEmailInformation.Attachment)
{
    AddAttachmentAsync(information, queueList.Name, queueList.Size);
}
// and modify AddAttachmentAsync to use these two parameters too

另一种方法是获取queueList的副本,然后将information 和该副本传递给AddAttachmentAsync,并酌情从两者中提取参数:

ExchangeEmailInformation information = exchangeEmailInformation;
var emailId = information.Sender.Split('@');
information.UserAlias = emailId[0];
foreach (var queueList in exchangeEmailInformation.Attachment)
{
    var attachment = queueList;
    AddAttachmentAsync(information, attachment);
}
// and modify AddAttachmentAsync to pull properties from the right parameter.

在第二个foreach()的第一行,您必须复制'information'并将副本传递给AddAttachmentAsync。(即所有数据的副本,而不仅仅是对象引用的副本)

发生的事情是,你传递给AddAttachmentAsync()的"信息"对象在AddAttachmentAsync()返回之前被改变了。

一般来说,当设计诸如ExchangeEmailInformation之类用于多线程的类时,您应该使它们不可变——这样就不可能发生这种特殊的事情。(在我看来,您应该使所有POD("普通旧数据")类不可变。)

除了正确评估您需要为每个线程创建information实例的其他答案外,我会提出另一个建议。我注意到您已经创建了一个调用AddAttachment,虽然目前它只是使Console.Write,我想象最终您将访问列表并将结果的实例添加到此列表。这将造成不必要的访问共享资源的情况。

比起让每个线程负责将其结果添加到列表中,让主线程创建一个具有引用类型属性的对象实例并将其传递给线程要简单得多。这样,线程已经有一个位置来存储它的结果,因为它不是一个共享资源,你不必担心同步。

这是一个非常基本的例子。显然,你得根据你的需要稍微修改一下。

class Result
{
   public object Data { get; set; }
}
List<Work> WorkToDo = // Some population call
List<Result> Results = new List<Result>();
foreach (Work Item in WorkToDo)
{
    Result Result = new Result();
    Results.Add(Result);
    System.Threading.Tasks.Task.Factory.StartNew(
        () => { Result.Data = "Hello World."; });
}

再次指出,仅仅因为您希望将所有结果编译成某种类型的列表或数组,并不意味着您需要共享对列表的访问权限。让父线程处理所有这些可以大大简化你的算法。