我似乎无法找到足够直接的答案。我无法理解为什么有人会选择" Get"结束" Get2"。
除非有技术原因不这样做,否则我总是选择" Get2"。这样我的代码可以在应用程序的io边缘而不是在任何地方异步。有人可以帮助我理解为什么你会选择一个而不是另一个。还是两者的情况?我理解C#中async / await的线程和执行模型,但不知道如何确保我在f#中做同样的事情。
member this.Get (url : string) : Async<'a> = async {
use client = new HttpClient()
let! response = client.GetAsync(url) |> Async.AwaitTask
response.EnsureSuccessStatusCode() |> ignore
let! content = response.Content.ReadAsStringAsync() |> Async.AwaitTask
return this.serializer.FromJson<'a>(content)
}
member this.Get2 (url : string) : 'a =
use client = new HttpClient()
let response = client.GetAsync(url) |> Async.AwaitTask |> Async.RunSynchronously
response.EnsureSuccessStatusCode() |> ignore
let content = response.Content.ReadAsStringAsync() |> Async.AwaitTask |> Async.RunSynchronously
this.serializer.FromJson<'a>(content);
答案 0 :(得分:17)
如果您正在编写一个可以阻止的程序,例如控制台应用程序,那么一定要阻止。如果你有能力阻止,Get2
就好了。
但事实是,大多数现代节目都无法阻挡。例如,在一个丰富的GUI应用程序中,当您执行长时间运行的事情时,您无法支撑UI线程:UI将冻结并且用户将不满意。同时,您也无法承担在后台执行所有操作,因为最终您需要将结果推送回UI,而这只能在UI线程上完成。因此,您唯一的选择是将逻辑分割为一系列后台调用,其回调结束于UI线程。异步计算(以及C#async
/ await
)可以透明地为您完成。
另一个例子,Web服务器实际上不能为每个活动请求保留一个活动线程:这将严重限制服务器的吞吐量。相反,服务器需要使用完成后回调向操作系统发出长时间运行的请求,这将形成一个链,最终以对客户端的响应结束。
最后一点或许需要特别澄清,因为正如我所发现的那样,保持活动后台线程和发出OS请求之间的区别并不是每个人都能立即明白的。构建大多数操作系统的方式(深度位于最低级别)是沿着这种模式:&#34; 亲爱的操作系统,请让网卡在此插槽上接收一些数据包,并在此时唤醒我#39;完成&#34; (我在这里有点简化)。此操作在.NET API中表示为HttpClient.GetAsync
。关于此操作的重点是,当它正在进行时,没有等待其完成的线程。您的应用程序已经将操作系统踢到操作系统端,并且可以愉快地将其宝贵的线程花在不同的东西上 - 比如接受来自客户或其他任何方面的更多连接。
现在,如果您使用Async.RunSynchronously
明确阻止此操作,这意味着您只是谴责当前线程坐在那里等待操作完成。在操作完成之前,此线程现在不能用于任何其他操作。如果在每个实例中执行此操作,则服务器现在每个活动连接都要花费一个线程。这是一个很大的禁忌。线程并不便宜。他们有一个限制。您基本上构建了一个玩具服务器。
最重要的是,C#async
/ await
和F#async
并不是为了满足语言设计师的需要而发明的。痒:他们非常需要。如果您正在构建严肃的软件,那么在某个时刻您必然会发现您无法阻止任何地方,并且您的代码变成了一堆乱七八糟的回调。这就是几年前大部分代码的状态。
我没有回答声称&#34; async似乎没有好好组成&#34;,因为该声明没有事实根据。如果你要澄清你在编写异步计算时遇到的具体困难,我很乐意修改我的答案。
F#async
和C#Task
不完全类似。有一个微妙的区别:F#async
是我们所说的&#34;冷&#34;,而C#Task
就是我们所说的&#34; hot&#34; (参见例如cold vs. hot observables)。简单来说,区别在于Task
已经在运行&#34;,而async
只是准备好运行。
如果手中有Task
个对象,则意味着该对象所代表的任何计算已经在飞行中#34;已经开始,已经在运行,并且您无能为力关于那个。您可以等待其完成,并且您可以获得其结果,但是您无法阻止它运行,或重新启动它,或其他任何类似的结果。
async
计算是一个准备进行的计算,但还没有进行。您可以使用Async.Start
或Async.RunSynchronously
或其他任何内容启动它,但除非您这样做,否则它无法执行任何操作。这样做的一个重要推论是你可以多次启动它,那些将是独立的,不同的,完全独立的执行。
例如,考虑这个F#代码:
let delay = async {
do! Task.Delay(500) |> Async.AwaitTask
return ()
}
let f = async {
do! delay
do! delay
do! delay
}
和这个(不完全)等效的C#代码:
var delay = Task.Delay(500);
var f = new Func<Task>( async () => {
await delay;
await delay;
await delay;
});
f().Wait();
F#版本需要1500毫秒,而C#版本则需要500毫秒。这是因为在C#中delay
是一个运行500毫秒并停止的单个计算,而这就是它;但在F#delay
中,只有在do!
中使用它才会运行,即使这样,每个do!
也会启动delay
的新实例。
提炼上述所有内容的一种方法是:F#async
相当于C#Task
,而不是Func<Task>
- 也就是说,它不是计算本身,但是一种开始新计算的方法。
那么,对于记录,async {}表达式具体是与C#中的async / await等效的功能正确吗?
有点但不完全正确。见上面的解释。
与&#34; AwaitTask&#34;耦合电话(如上所述)是......正常吗?
有点但不完全正确。作为一种使用C#中心.NET API的方法 - 是的,使用AwaitTask
是正常的。但是,如果您完全使用F#编写程序并仅使用F#库,那么任务根本不应该真正进入图片。
它的异步&amp;让!这是async / await的直接等价物。我是对的吗?
再次,有点但不完全正确。 let!
和do!
确实与await
完全相同,但F#async
与C#async
并不完全相同 - 请参阅上述说明。还有return!
,它在C#语法中没有直接的等价物。
RunSync ...适用于可以接受阻塞的特定环境。
是。通常,RunSynchronously
用于边界。它习惯于转换&#34;异步计算到同步计算。它相当于Task.GetAwaiter().GetResult()
。
如果我想将该函数公开给C#,则必须将其返回给Task。是&#34; StartAsTask&#34;这样做的方法?
再次:有点但不完全正确。请注意,Task
已经正在运行&#34;计算,因此您必须小心谨慎地将async
变为Task
。考虑这个例子:
let f = async { ... whatever ... } |> Async.StartAsTask
let g () = async { ... whatever ... } |> Async.StartAsTask
这里,f
的主体将在初始化时执行,任务将在程序开始运行时立即开始运行,每当有人获取f
的值时,它将是总是一样的任务。可能不是你直觉所期望的。
另一方面,g
每次调用时都会创建一个新任务。为什么?因为它有一个单位参数()
,所以它的主体不会执行,直到有人用该参数调用它。每次新身体执行时,都会调用Async.StartAsTask
,因此需要新Task
。如果您仍然对空括号()
正在做的事情感到困惑,请查看this question。
[StartAsTask]是否立即显式启动新线程?
是的,确实如此。但猜猜怎么了?这实际上正是C#所做的事情!如果您有一个方法public async void f() { ... }
,那么每次调用它就像f()
一样,该调用实际上会立即启动一个新线程。好吧,更确切地说,它会立即启动一个新的计算 - 这可能并不总是会导致新的线程。 Async.StartAsTask
完全相同。
是否必须为这种互操作做出妥协?
是的,这是一种必须采用这种互操作方式的方法,但我不明白为什么它会成为&#34;妥协&#34;。
答案 1 :(得分:2)
async
和C#中的async/await
在某种意义上是梅尔文康威于1958年创造的coroutine
(https://en.wikipedia.org/wiki/Coroutine)一词的实现。
正常函数和C#和F#是subroutines
。事情是:
Subroutines are special cases of ... coroutines.
— Donald Knuth
这意味着我们invoke
subroutine
,然后在return
(可能是例外)时运行完成。
coroutine
还可以yield
和resume
。
当subroutine
读取IO时,它需要在等待IO时阻塞。然而,coroutine
yield
可以让coroutine
执行另一个coroutine
,并且当IO信号准备就绪时resume
然后coroutine
。我们的好处是可以共享一个线程来为许多subroutine
提供服务,其中每个coroutines
必须由一个线程提供服务。
C10k problem
在coroutine
(https://en.wikipedia.org/wiki/C10k_problem)中可能会有所帮助,因为我们不需要每个连接一个帖子。相反,我们拥有一个有限的线程池,为10,000 yield
提供服务,为IO resume
和async/await
提供服务。
C#中subroutine
的一个问题是当coroutine
(正常函数)启动async
(coroutine
函数时)会发生什么? yield
可能希望.Value
,但这是.NET中的常规功能不支持的操作。当我们在async
任务上调用var task = MyAsyncFunction();
var result = task.Value; // Hmm but what if the MyAsyncFunction needs to yield.
时会发生这种情况。
.Value
所以在coroutines
&#34;有趣&#34;事情可能会发生。我们可能有死锁,或者我们可能没有。无论我们失去了.Value
的好处,因为我们的帖子现在在async
上阻止。
这是一些人争辩说应该一直使用.Value
并且永远不依赖.Wait()
或async
的原因之一。
async
函数可以调用async
函数和普通函数。普通函数无法调用或至少阻止Get
函数。
因此,如果我们在上面的问题中查看Get2
和Get2
,那么我想知道Get3
比member this.Get3 (url : string) : 'a =
use client = new HttpClient()
let response = client.Get(url)
response.EnsureSuccessStatusCode() |> ignore
let content = response.Content.ReadAsString()
this.serializer.FromJson<'a>(content);
有什么好处:
Get2
在我看来Get1
更复杂,然后一直使用普通功能,并没有带来任何好处。
coroutine
提供的好处是coroutine
与其他coroutine
共享该帖子。
PS。恕我直言,包含阻止API中.Value
的操作是错误的,例如.Wait
,RunSynchronously
或async
。此操作消除了我们为什么首先assembly-plugin
的好处。