C#中的Task.FromResult<TResult>有什么用途?

257
在C#和TPL(Task Parallel Library)中, Task 类表示产生类型为T的值的正在进行的工作。
我想知道Task.FromResult方法的必要性是什么?
也就是说,在已经拥有生成的值的情况下,为什么需要将其重新包装到任务中?
唯一想到的是它被用作其他接受Task实例的方法的适配器。

6
这个对你有帮助吗?http://msdn.microsoft.com/en-us/library/hh228607.aspx - Izikon
41
在某种程度上,我同意这个观点,但是创建像这样密集、有用、集中、讨论导向的网页是非常有益的。我几乎总是从一个好的、密集的stackoverflow页面中学到更多,而不是通过在多个地方进行谷歌搜索和研究,所以在这种情况下,我真的很高兴他发布了这篇帖子。 - Alex Edelstein
56
我认为谷歌将我带到了某个网站,那个网站又要我去谷歌查找,这是一个循环引用 :) - gmail user
1
当有人删除已经有答案的评论时,我真的很讨厌。这会导致在尝试理解时浪费大量的脑力... - The incredible Jan
7个回答

357

我发现有两种常见的使用情况:

  1. 当您正在实现允许异步调用者的接口,但您的实现是同步时。
  2. 当您为测试而模拟异步代码时。

9
一个很好的 #1 示例是网络服务。你可以有一个同步服务方法,返回Task.FromResult,并且客户端异步等待网络 I/O。这样,您可以使用 ChannelFactory 共享客户端/服务器之间的相同接口。 - Nelson Rothermel
19
@JohnHenckel: OWIN 从一开始就被设计为 async-friendly。接口和基类通常使用异步签名,因为它只是允许(而不是强制)实现是异步的。所以这类似于 IEnumerable<T> 继承自 IDisposable - 它允许可枚举类型具有可释放资源,而不是强制其这样做。FromResultasyncawait 都不会生成线程。 - Stephen Cleary
5
@StephenCleary,谢谢你的解释。我一开始以为await会开新的线程,但是我试了一下发现它并没有这样做。只有Task.Run才会。因此,x = await Task.FromResult(0);相当于说x = 0;这很令人困惑,但是很好知道! - John Henckel
8
对于I/O操作来说,最好的解决方案是异步实现,但有时你没有这个选择。此外,有时你可以同步实现它(例如,缓存结果并在值未被缓存时回退到异步实现)。更一般地,返回Task的方法意味着“可能是异步的”。因此,有时会给方法一个异步签名,明确知道某些实现将是同步的(例如,NetworkStream应该是异步的,而MemoryStream应该是同步的)。 - Stephen Cleary
2
@StephenCleary 我明白。此外,我认为,当你创建抽象时,你希望它是可等待的,即使该抽象的具体实现尚未运行任何异步操作,但可能在更改具体抽象实现时会调用某些可等待的操作。因此,使用 Task 作为返回类型声明抽象将使您的具体实现足够灵活,可以运行同步和异步操作,而无需更改抽象签名。如果我没错的话,我觉得这是 Task.FromResult 的主要目的之一... - OlegI
显示剩余8条评论

64

已完成的任务,例如从 Task.FromResult 返回的任务,可以被缓存。 - Paulo Morgado
2
@Paulo: 将整个 Task 对象保存在内存中似乎比仅缓存结果更浪费空间。 - Ben Voigt
2
预期“值任务”已经被缓存。我不太记得是哪些,但我认为Task.FromResult(0)Task.FromResult(1)Task.FromResult(false)Task.FromResult(true)都被缓存了。你不应该缓存一个用于网络访问的任务,但从结果中返回一个任务是完全可以的。你更喜欢每次需要返回值时创建一个新的任务吗? - Paulo Morgado
@PauloMorgado 我不认为您回应了Ben的观点。缓存Task而不是仅缓存任务返回的TResult有什么好处? - ToolmakerSteve
5
为了回答我的问题,拥有一个 Tasks 缓存的好处是其中某些任务可以是已完成的任务,而其他任务可能尚未完成。调用方不必关心这些细节:他们进行异步调用,如果任务已经完成,则在等待时立即获得答案,否则稍后再获取。如果没有这些缓存任务,则要么(a)需要两种不同机制,一种同步,一种异步-对调用方来说很麻烦,要么(b)每次调用方请求已经可用的答案时都必须动态创建一个 Task(如果我们只缓存了一个 TResult)。 - ToolmakerSteve
2
我现在明白了。答案的措辞有点令人困惑。任务本身没有内置的“缓存”机制。但是,如果您为...比如...下载文件编写了一个缓存机制,Task<File> GetFileAync( ),您可以使用Task.FromResult(cachedFile)立即返回已经在缓存中的文件,并且等待将同步运行,通过不进行线程切换来节省时间。 - Brain2000

47

当您想要创建一个可等待的方法,但不想使用 async 关键字时,可以使用它。我找到了这个例子:

public class TextResult : IHttpActionResult
{
    string _value;
    HttpRequestMessage _request;

    public TextResult(string value, HttpRequestMessage request)
    {
        _value = value;
        _request = request;
    }
    public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        var response = new HttpResponseMessage()
        {
            Content = new StringContent(_value),
            RequestMessage = _request
        };
        return Task.FromResult(response);
    }
}

在这里,您正在创建自己的IHttpActionResult接口实现,以在Web Api动作中使用。 ExecuteAsync方法应该是异步的,但您不必使用async关键字使其异步和可等待。由于您已经拥有结果并且不需要等待任何内容,因此最好使用Task.FromResult。


27

6

在您的示例中,结果不是同步获取的,所有操作都是异步的,Task.FromResult 用于获取先前缓存的异步结果。 - Rodrigo Reis

-2

Task.Run() 创建一个 lambda 线程,不需要 async 并返回一个类型对象。在我的示例中,我有多个任务同时运行等待它们的完成。一旦所有任务都完成了,我就可以循环遍历它们的结果。Task.FromResult 用于推送由 Task.Run() 未生成的任务结果。

在这种情况下,Task.FromResult 推送一个类型对象,即 Result 类中的 RecordStruct 类。我创建了两个调用函数 getData 的任务。Task.WaitAll 处理每个任务并将结果推送到类型为 RecordStruct 的 result 对象数组中。然后,我作为结果访问 RecordStruct 类的属性元素。

    public class RecordStruct
    {
        public RecordStruct(string _element) {
            element = _element;
        }
        public string element { get;set; }
    }

public class TaskCustom
    {
        public Task<RecordStruct> getData(string phrase)
        {
            if (phrase == "hello boise")
            {
                return Task.FromResult(new RecordStruct("Boise is a great place to live"));
            }

            return Task.Run(() =>
            {
                return new RecordStruct(phrase);
            });
        }
    } 

[Fact]
        public async Task TestFactory()
        {
            TaskCustom obj = new TaskCustom();
            List<Task<RecordStruct>> tasks = new List<Task<RecordStruct>>();
            tasks.Add(obj.getData("hello world"));
            tasks.Add(obj.getData("hello boise"));

            Task.WaitAll(tasks.ToArray());

            for(int ctr = 0; ctr < tasks.Count; ctr++) {
                if (tasks[ctr].Status == TaskStatus.Faulted)
                    output.WriteLine(" Task fault occurred");
                else
                {
                    output.WriteLine("test sent {0}",
                                      tasks[ctr].Result.element);
                    Assert.True(true);
                }
            }
        }

-2
我认为,对于那些需要较长时间才能完成的同步方法,你可以使用 Task.FromResult,并在代码中执行其他独立的工作。虽然我更倾向于将这些方法改为异步调用,但是想象一下这样一种情况:当你无法控制所调用的代码时,你希望实现隐式并行处理。

1
不确定为什么SO排序将此答案放在列表顶部,但它是不正确的。通过调用await Task.FromResult(...)没有隐式的并行处理(请参见此问题进行解释)。相反,它是同步运行的。如果您希望将长时间运行的同步方法转换为可以与另一个方法并行调用的方法,则应改用await Task.Run(() => LongRunningTask()); - nicholas
@nicholas,“convert”是什么意思?你不能在同步方法中使用“await”。你的“LongRunningTask”是指同步方法吗? - The incredible Jan
@TheincredibleJan - 有趣的是,现在重新阅读这条评论,我不确定当时我在想什么。我相信通过“转换”,我指的是“假装一个长时间运行的阻塞调用是异步的”。回想起来,我的评论有一些值得提及的注意事项。在Task.Run(...)调用中运行阻塞操作通常会将长时间操作推送到线程池线程中。如果这是ASP.NET Core处理程序,则已经在线程池上,因此除了不必要的上下文切换外,这没有任何影响。此外,如果滥用此模式,可能会导致线程池饥饿。简而言之,不要这样做。 - nicholas

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