哪种异步实现是最佳实践

TBo*_*one 4 f#

我似乎无法找到足够直接的答案.我无法理解为什么有人会选择"Get"而不是"Get2".

  • 在"获取"中,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);
Run Code Online (Sandbox Code Playgroud)

Fyo*_*kin 18

如果您正在编写一个可以阻止的程序,例如控制台应用程序,那么无论如何都要阻止.Get2如果你能负担得起,那就好了.

但事实是,大多数现代节目都无法阻挡.例如,在丰富的GUI应用程序中,在执行长时间运行的事情时,您无法承受UI线程:UI将冻结,用户将不满意.同时,您也无法承担在后台执行所有操作,因为最终您需要将结果推送回UI,而这只能在UI线程上完成.因此,您唯一的选择是将逻辑分割为一系列后台调用,其回调结束于UI线程.异步计算(以及C#async/ await)为您透明地执行此操作.

再举一个例子,Web服务器实际上无法为每个活动请求保留一个活动线程:这会严重限制服务器的吞吐量.相反,服务器需要使用完成后回调向操作系统发出长时间运行的请求,这将形成最终以对客户端的响应结束的链.

最后一点或许需要特别澄清,因为正如我所发现的那样,保持活动后台线程和发出操作系统请求之间的区别并不是每个人都能立即明白的.构建大多数操作系统的方式(深层次处于最低级别)是沿着这种模式:" 亲爱的操作系统,请让网卡在这个插槽上接收一些数据包,并在完成后唤醒我 "(我是这里有点简化).此操作在.NET API中表示为HttpClient.GetAsync.关于这个操作的重点是,在它正在进行中时,没有线程在等待它的完成.您的应用程序已经将操作系统踢到操作系统端,并且可以愉快地将其宝贵的线程花在不同的东西上 - 比如接受来自客户端或其他任何方面的更多连接.

现在,如果您明确阻止此操作Async.RunSynchronously,这意味着您只是谴责当前线程坐在那里等待操作完成.在操作完成之前,此线程现在不能用于任何其他操作.如果在每个实例中执行此操作,则服务器现在每个活动连接都要花费一个线程.这是一个很大的禁忌.线程并不便宜.他们有一个限制.你基本上建立了自己的玩具服务器.

最重要的是,C#async/ await和F#async并不是为了满足语言设计者的需求而发明的:他们非常需要.如果你正在构建严肃的软件,那么在某个时刻你必然会发现你不能只是在任何地方阻塞,你的代码变成了一堆乱七八糟的回调.这正是几年前大部分代码的状态.


我没有回答"异步似乎不能很好地构成"的说法,因为该声明没有得到证实.如果你要澄清你在编写异步计算时遇到的具体困难,我很乐意修改我的答案.


编辑:回应评论

F#async和C#Task完全类似.有一个微妙的区别:F#async是我们所说的"冷",而C#Task就是我们所说的"热"(参见例如冷与热观察).简单来说,区别在于a Task"已在运行",而a async只是准备运行.

如果Task手中有一个对象,则意味着该对象所代表的任何计算已经"在飞行中",已经开始,已经在运行,并且您无法做任何事情.您可以等待它的完成,并且您可以获得其结果,但是您无法阻止它运行,或重新启动它或其他任何类似的结果.

async另一方面,F#计算是"准备好了"的计算,但还没有进行.你可以用Async.Start或者Async.RunSynchronously或者其他什么来开始它,但是在你做之前它没有做任何事情.这样做的一个重要推论是你可以多次启动它,那些将是独立的,不同的,完全独立的执行.

例如,考虑这个F#代码:

let delay = async { 
    do! Task.Delay(500) |> Async.AwaitTask 
    return () 
}

let f = async {
    do! delay
    do! delay
    do! delay
}
Run Code Online (Sandbox Code Playgroud)

和这个(不完全)等效的C#代码:

var delay = Task.Delay(500);

var f = new Func<Task>( async () => {
    await delay;
    await delay;
    await delay;
});

f().Wait();
Run Code Online (Sandbox Code Playgroud)

F#版本只需要1500毫秒,而C#版本则需要500毫秒.这是因为在C#中delay是一个运行500ms并停止的单个计算,就是这样; 但是在F#中delay它没有运行,直到它在a中使用do!,即便如此,每个都do!启动一个新的实例delay.

提炼上述所有内容的一种方法是:F#async不等同于C#Task,而是等于Func<Task>- 也就是说,它不是计算本身,而是开始新计算的一种方式.

现在,有了这样的背景,让我们来看看你个人的小问题:

那么,对于记录,async {}表达式具体是与C#中的async/await等效的功能正确吗?

有点但不完全正确.见上面的解释.

与"AwaitTask"调用(如上所述)耦合是......正常吗?

有点但不完全正确.作为一种使用C#中心.NET API的方法 - 是的,使用它是正常的AwaitTask.但是如果你完全用F#编写你的程序并只使用F#库,那么任务就不应该真正进入图片.

这是异步和让!这是async/await的直接等价物.我对吗?

再一次,有点但不完全正确.let!并且do!确实是直接等价的await,但F#async与C#并不完全相同async- 请参阅上面的解释.还有return!,在C#语法中没有直接的等价物.

RunSync ...适用于可以接受阻塞的特定上下文.

是.通常,RunSynchronously用于边界.它用于将异步计算"转换"为同步计算.它相当于Task.GetAwaiter().GetResult().

如果我想将该函数公开给C#,则必须将其返回到Task中."StartAsTask"是这样做的吗?

再说一次:有点但不完全正确.请记住,a Task是一个"已经在运行"的计算,所以你必须要小心你是如何将你async变成一个Task.考虑这个例子:

let f = async { ... whatever ... } |> Async.StartAsTask

let g () = async { ... whatever ... } |> Async.StartAsTask
Run Code Online (Sandbox Code Playgroud)

这里,主体f将在初始化时执行,任务将在程序开始运行时立即开始运行,并且每当有人获取值时f,它将始终是相同的任务.可能不是你直觉所期望的.

另一方面,g每次调用时都会创建一个新任务.为什么?因为它有一个单元参数(),所以它的主体不会执行,直到有人用该参数调用它.每次它将成为一个新的身体执行,一个新的呼叫Async.StartAsTask,因此,一个新的Task.如果你仍然对空括号的作用感到困惑(),请看看这个问题.

[StartAsTask]是否立即显式启动新线程?

是的,它确实.但猜猜怎么了?这实际上正是C#所做的事情!如果你有一个方法public async void f() { ... },每次调用它时f(),该调用确实会立即启动一个新线程.好吧,更确切地说,它立即开始一个新的计算 - 这可能并不总是导致新的线程.Async.StartAsTask做同样的事情.

是否必须为这种互操作做出妥协?

是的,这是一种必须采取这种互操作的方法,但我不明白为什么它是"妥协".