3

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

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 

打印的结果是

  • 第二附件.txt
  • 第二附件.txt。

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

然后我尝试执行与下面同步的相同操作。

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


}

结果是

  • 第一个附件.txt

  • 第二附件.txt

显示我想要的正确结果

我该如何解决这些问题?

4

5 回答 5

5

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

information在将其发送到异步方法之前,您应该制作一份副本。

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

于 2013-01-10T08:58:56.323 回答
2

您已修改information.

于 2013-01-10T08:57:18.377 回答
2

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

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

  1. 填充information呼叫 1
  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.
于 2013-01-10T08:59:23.977 回答
1

在第二个 foreach() 的第一行中,您必须制作“信息”的副本并将副本传递给 AddAttachmentAsync。(也就是说,所有数据的副本 - 而不仅仅是对象引用的副本。)

发生的事情是您传递给 AddAttachmentAsync() 的“信息”对象在 AddAttachmentAsync() 返回之前被更改。

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

于 2013-01-10T09:00:35.010 回答
0

除了正确评估您需要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."; });
}

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

于 2013-01-10T09:12:52.853 回答