哪种异步实现是最佳实践

时间:2018-04-12 01:02:29

标签: f#

我似乎无法找到足够直接的答案。我无法理解为什么有人会选择" Get"结束" Get2"。

  • 在" Get&#34 ;, async会在我的代码中流失,比如C#。在C#中,这是有充分理由的。 f#也是如此吗? async似乎也不具备功能性。就像C#。
  • 在" Get2"中,我返回了没有包装器的值。来电者不必等待"等待"任何东西。在C#中,如果不调用" GetAwaiter()。GetResult()"则不可能。

除非有技术原因不这样做,否则我总是选择" 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);

2 个答案:

答案 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;已经开始,已经在运行,并且您无能为力关于那个。您可以等待其完成,并且您可以获得其结果,但是您无法阻止它运行,或重新启动它,或其他任何类似的结果。

另一方面,另一方面,F#async计算是一个准备进行的计算,但还没有进行。您可以使用Async.StartAsync.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)

F#中的{p> async和C#中的async/await在某种意义上是梅尔文康威于1958年创造的coroutinehttps://en.wikipedia.org/wiki/Coroutine)一词的实现。

正常函数和C#和F#是subroutines。事情是:

Subroutines are special cases of ... coroutines.
— Donald Knuth

这意味着我们invoke subroutine,然后在return(可能是例外)时运行完成。

coroutine还可以yieldresume

subroutine读取IO时,它需要在等待IO时阻塞。然而,coroutine yield可以让coroutine执行另一个coroutine,并且当IO信号准备就绪时resume然后coroutine。我们的好处是可以共享一个线程来为许多subroutine提供服务,其中每个coroutines必须由一个线程提供服务。

C10k problemcoroutinehttps://en.wikipedia.org/wiki/C10k_problem)中可能会有所帮助,因为我们不需要每个连接一个帖子。相反,我们拥有一个有限的线程池,为10,000 yield提供服务,为IO resumeasync/await提供服务。

C#中subroutine的一个问题是当coroutine(正常函数)启动asynccoroutine函数时)会发生什么? 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函数。

因此,如果我们在上面的问题中查看Get2Get2,那么我想知道Get3member 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的操作是错误的,例如.WaitRunSynchronouslyasync。此操作消除了我们为什么首先assembly-plugin的好处。