Haskell 中与 C# 5 async/await 相当的东西

14
我刚刚阅读了有关使用awaitasync关键字处理C# 5.0中的异步函数的新方法。来自C#参考文档的示例:
private async Task SumPageSizesAsync()
{
    // To use the HttpClient type in desktop apps, you must include a using directive and add a 
    // reference for the System.Net.Http namespace.
    HttpClient client = new HttpClient();
    // . . .
    Task<byte[]> getContentsTask = client.GetByteArrayAsync(url);
    byte[] urlContents = await getContentsTask;

    // Equivalently, now that you see how it works, you can write the same thing in a single line.
    //byte[] urlContents = await client.GetByteArrayAsync(url);
    // . . .
}

Task<byte[]>代表异步任务的未来,该任务将生成类型为byte[]的值。在Task上使用关键字await基本上会将函数的其余部分放在一个续体中,在任务完成时调用该续体。任何使用await的函数必须使用关键字async并具有类型Task<a>,如果它返回类型a

所以这些行

byte[] urlContents = await getContentsTask;
// Do something with urlContents

会翻译成类似的东西

Task newTask = getContentsTask.registerContinuation(
               byte[] urlContents => {
                 // Do something with urlContents
               });
return newTask;

这感觉很像一个Monad(-transformer?)。它感觉应该与CPS Monad有些关系,但也许不是。
这是我尝试编写相应的Haskell类型。
-- The monad that async functions should run in
instance Monad Async
-- The same as the the C# keyword
await         :: Async (Task a) -> Async a
-- Returns the current Task, should wrap what corresponds to
-- a async method in C#.
asyncFunction :: Async a -> Async (Task a)
-- Corresponds to the method Task.Run()
taskRun       :: a -> Task a

并且上面例子的粗略翻译

instance MonadIO Async -- Needed for this example

sumPageSizesAsync :: Async (Task ()) 
sumPageSizesAsync = asyncFunction $ do
    client <- liftIO newHttpClient
    -- client :: HttpClient
    -- ...
    getContentsTask <- getByteArrayAsync client url
    -- getContentsTask :: Task [byte]
    urlContents <- await getContentsTask
    -- urlContents :: [byte]

    -- ...

这是否是Haskell中对应的类型?是否有任何Haskell库可以实现处理异步函数/操作的方式(或类似方式)?
另外:你能使用CPS-transformer构建这个吗?
编辑
是的,Control.Concurrent.Async模块解决了类似的问题(并具有类似的接口),但它采用完全不同的方式。我想Control.Monad.Task更匹配。我正在寻找一种使用Continuation Passing Style幕后进行Futures的单调接口。

3
您可以查看 Haskell 库 async,它应该提供您需要的功能:http://hackage.haskell.org/package/async - MoFu
1
“async”包比C#的功能更好,因为它将真正异步的责任从被调用的函数中移除。我唯一能看到的缺点是性能,因为“Control.Concurrent.Async”(可能)需要比C#中更简单的解决方案更多的簿记工作。轻量级线程和STM与赞美回调相比非常重。 - Hjulle
2个回答

13

这是一个基于async库构建的Task单子:

import Control.Concurrent.Async (async, wait)

newtype Task a = Task { fork :: IO (IO a) }

newTask :: IO a -> Task a
newTask io = Task $ do
    w <- async io
    return (wait w)

instance Monad Task where
    return a = Task $ return (return a)
    m >>= f  = newTask $ do
        aFut <- fork m
        a    <- aFut
        bFut <- fork (f a)
        bFut

请注意,我并没有检查这些单子定律,所以可能不正确。
以下是如何定义在后台运行的原始任务:
import Control.Concurrent (threadDelay)

test1 :: Task Int
test1 = newTask $ do
    threadDelay 1000000  -- Wait 1 second
    putStrLn "Hello,"
    return 1

test2 :: Task Int
test2 = newTask $ do
    threadDelay 1000000
    putStrLn " world!"
    return 2

然后,您可以使用do符号将Task组合起来,从而创建一个新的延迟任务,准备运行:
test3 :: Task Int
test3 = do
    n1 <- test1
    n2 <- test2
    return (n1 + n2)

运行fork test3将会生成任务并返回一个未来的结果,您可以随时调用该结果以要求它,必要时会阻塞直到完成。

为了证明它的有效性,我将进行两个简单的测试。首先,我将分叉 test3 ,不要求其未来的结果,只是为了确保它正确地生成了复合线程:

main = do
    fork test3
    getLine -- wait without demanding the future

这个是正确的:

$ ./task
Hello,
 world!
<Enter>
$

现在我们可以测试当我们要求结果时会发生什么:
main = do
    fut <- fork test3
    n   <- fut  -- block until 'test3' is done
    print n

...也可以使用以下方法:

$ ./task
Hello,
 world!
3
$

这里应该对应什么关键字(Task<a>、await、async等)? - Hjulle
Haskell版本的newTask对应于C#async,而Haskell版本的wait则对应于C#的await。 Haskell版本的Task类型没有完全匹配,因为C#对任务和其产生的未来使用相同的类型,所以有点令人困惑。 同样,C#中没有与Haskell版本的fork相当的东西,因为每次调用C# Task时,C#基本上都会在幕后为您隐式调用fork - Gabriella Gonzalez
据我所知,C# 中的 Task 类型仅是 future,没有其他任何作用。而且 C# 没有隐式地进行 fork,它在 future 值上隐式注册了回调函数(当使用 await 时,即 async 函数的其余部分),这对性能有重大影响。所有的 forking 都由生成原始 Task 的函数显式完成。 - Hjulle
8
尽管我理解您指出的将来类型是单子,但您误解了C#中等待运算符的作用。等待的重点不是“分派一个新线程以并行执行这个计算”!等待的重点是意味着“将当前方法重写为延续传递样式;计算当前返回任务的表达式,在该任务中记录当前延续,并将控制权返回给调用方”。当任务完成时,延续被安排。这与您所写的非常不同。 - Eric Lippert
@EricLippert 是的,我在C#中关于await的描述不够清晰/准确。也许我不应该在问题中尝试描述它,因为似乎这导致了更多的混淆而不是澄清。 - Hjulle
显示剩余2条评论

0

monad-par 库提供了 spawnget 函数,可用于创建类似于 Future 的计算。您可以使用 Par 单子来运行纯代码并行,或者使用带有副作用的代码的 ParIO

特别地,我认为您的代码示例可以翻译成:

import Control.Monad.Par.IO

sumPageSizesAsync :: URL -> IO ByteString
sumPageSizesAsync url = runParIO $ do
  task <- spawn $ liftIO $ do client <- newHttpClient
                              return $ getContents url
  urlContents <- get task

正如您所看到的,spawn 负责创建并行运行的代码,并返回一个 IVar,稍后可以通过 get 查询以检索其答案。我认为这两个函数的行为非常匹配 asyncawait

有关更多信息,建议阅读《Haskell 中的并行和并发编程》一书中的 Par monad 章节


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