任务.ContinueWith的执行顺序

5

显然,我不太理解如何使用ContinueWith方法。我的目标是执行一个任务,并在完成时返回一条消息。

这是我的代码:

    public string UploadFile()
    {
        if (Request.Content.IsMimeMultipartContent())
        {
            //Save file
            MultipartFormDataStreamProvider provider = new MultipartFormDataStreamProvider(HttpContext.Current.Server.MapPath("~/Files"));
            Task<IEnumerable<HttpContent>> task = Request.Content.ReadAsMultipartAsync(provider);

            string filename = "Not set";

            task.ContinueWith(o =>
            {
                //File name
                filename = provider.BodyPartFileNames.First().Value;
            }, TaskScheduler.FromCurrentSynchronizationContext()); 

            return filename;
        }
        else
        {
            return "Invalid.";
        }
    }

变量“filename”始终返回“未设置”。似乎在ContinueWith方法中的代码从未被调用。(如果我在VS中逐行调试它,它会被调用。)
这个方法是在我的ASP.NET Web API控制器 / Ajax POST中被调用。
我做错了什么?

2
这是因为你正在执行异步操作。 - Daniel A. White
除了任务是异步的,我认为它们甚至还没有开始。 - Cristian Lupascu
4个回答

7
如果您正在使用异步操作,最好的方法是将您的操作也变成异步的,否则您将失去所做的异步调用带来的优势。请尝试按照以下方式重写您的方法:
public Task<string> UploadFile()
{
    if (Request.Content.IsMimeMultipartContent())
    {
        //Save file
        MultipartFormDataStreamProvider provider = new MultipartFormDataStreamProvider(HttpContext.Current.Server.MapPath("~/Files"));
        Task<IEnumerable<HttpContent>> task = Request.Content.ReadAsMultipartAsync(provider);

        return task.ContinueWith<string>(contents =>
        {
            return provider.BodyPartFileNames.First().Value;
        }, TaskScheduler.FromCurrentSynchronizationContext()); 
    }
    else
    {
        // For returning non-async stuff, use a TaskCompletionSource to avoid thread switches
        TaskCompletionSource<string> tcs = new TaskCompletionSource<string>();
        tcs.SetResult("Invalid.");
        return tcs.Task;
    }
}

哇,谢谢!这对我有用。也感谢其他所有人 - 感激你们的帮助。 - Rivka
我有一个关于TaskScheduler.FromCurrentSynchronizationContext的问题 - 我的理解是这是一个UI方法(与UI线程同步),但我想知道是否有任何原因可以在webapi方法中使用它。有吗? - AlexGad
同步上下文还可以用于存储一些线程本地变量,并确保在控制权返回到继续时重新填充它们。一个例子就是Thread.CurrentPrincipal。如果我没记错,ASP.NET运行时也为这种情况定义了一个同步上下文。 - carlosfigueira
如果您正在使用 .Net 4.0,请参阅此线程: https://dev59.com/P2Up5IYBdhLWcg3wj4Hu - Daniel Leiszen

2
您的变量未设置的原因可能是:
  • 任务已实例化,但尚未运行。
  • 即使任务运行了,函数也很可能在它们完成运行之前返回,因此仍将返回“未设置”。解决方法是等待最后一个任务(设置fileName的任务)完成。
您的代码可以像这样修复:
public string UploadFile()
{
    if (Request.Content.IsMimeMultipartContent())
    {
        //Save file
        MultipartFormDataStreamProvider provider = new MultipartFormDataStreamProvider(HttpContext.Current.Server.MapPath("~/Files"));
        Task<IEnumerable<HttpContent>> task = Request.Content.ReadAsMultipartAsync(provider);

        string filename = "Not set";

        var finalTask = task.ContinueWith(o =>
            {
                //File name
                filename = provider.BodyPartFileNames.First().Value;
            }, TaskScheduler.FromCurrentSynchronizationContext()); 

        task.Start();

        finalTask.Wait();

        return filename;
    }
    else
    {
        return "Invalid.";
    }
}

以下是新增内容:
  • task.ContinueWith 的返回值分配给名为 finalTask 的变量。我们需要这个任务,因为我们需要等待它完成。
  • 启动任务(task.Start(); 行)
  • 在返回之前等待最终任务完成(finalTask.Wait();
如果可能的话,请考虑不要异步实现,因为最终它是同步的(你正在等待它完成),当前的实现增加了复杂性,可能可以避免。
请考虑按照以下方式进行操作(如果可能):
public string UploadFile()
{
    if (Request.Content.IsMimeMultipartContent())
    {
        //Save file
        MultipartFormDataStreamProvider provider = new MultipartFormDataStreamProvider(HttpContext.Current.Server.MapPath("~/Files"));

        Request.Content.ReadAsMultipart(provider); // don't know if this is really valid.

        return provider.BodyPartFileNames.First().Value;
    }
    else
    {
        return "Invalid.";
    }
}

免责声明:我实际上没有执行上面的代码;我只是编写它来说明应该做什么。


2
你不应该调用 Wait() 函数,因为它会导致线程阻塞。相反,UploadFile() 函数本身应该是异步的。 - marcind
我尝试了你的第一个建议,但在start方法上出现了错误:不能在没有空操作的任务上调用Start。关于非异步 - 我不确定。我认为没有叫做ReadAsMultipart的方法(AFAIK)。 - Rivka
@marcind 你说得对,这是一个笨拙的结构。这就是为什么我建议如果可能的话采用同步替代方案。因为我们有两个任务按顺序执行,当它们完成时我们需要控制权回来 - 我认为这里没有任何异步操作。 - Cristian Lupascu
@Rivka 是的,我没说它能工作,我只是建议你尝试寻找一个更简单的替代方案。在这种情况下不需要异步代码,异步代码只会增加复杂性。 - Cristian Lupascu

1

你应该从这个方法中返回类型为Task<T>,在这种情况下它将是一个Task<string>


我尝试了这个,但是我遇到了同样的问题 - "filename" 没有被设置。 - Rivka

0

您正在使用异步操作。如果您想等待其完成,您必须使用Wait方法,否则您的任务:

task.ContinueWith(o =>
        {
            //File name
            filename = provider.BodyPartFileNames.First().Value;
        ).Wait();

return filename;

编辑: 一些异步方法在创建时立即启动任务,而其他方法要求您明确启动它们。您必须查阅每个方法的文档以确保。在这种情况下,似乎任务会自动启动。


尝试过了,使用Wait()似乎没有返回任何东西(甚至不是“未设置”)。 - Rivka
那么这意味着任务在创建时并没有运行。我已经相应地编辑了我的答案。 - Falanwe
获取错误:无法在具有空操作的任务上调用Start。任务的状态为“RanToCompletion”。 - Rivka
这意味着任务已经开始(并完成了...)。继续应该立即开始。如果没有,可能使用的调度程序无法安排继续。我建议尝试从ContinueWith调用中删除调度程序。 - Falanwe
在这种情况下,等待将阻塞线程,因此后续代码不会执行。 - Daniel Leiszen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接