从C#调用F#异步函数

6
假设我有以下代码:
namespace Library1

open System.Threading.Tasks
open System.Threading
open System.Runtime.Remoting.Messaging
open System

type public Class1() =

    let printThread (message) = 
        printfn "%s %A" message Thread.CurrentThread.ManagedThreadId

    let bar = 
        printThread ("first bar")
        async { 
            printThread ("first async")
            do! Async.Sleep(1000)
            printThread "last async"
        }

    member this.X() =  bar

我想使用这个类并从C#中调用X。问题在于X返回一个Async<'T>,但是公开F#特定类型是不好的做法。所以最佳实践是返回一个Task。但是Async.StartAsTask有点问题,因为它会导致代码在单独的线程中运行。我想要的是返回一个Task,但是它也应该像Async.StartImmediate一样运行。因此,非异步部分的代码应该在原始主线程中运行。在这里,我假设我从UI线程运行它,以便所有调用都返回相同的线程ID。换句话说,我想要一个Async.StartImmediate,但返回一个任务。 这是可行的吗?
3个回答

3
您可以使用 Async.StartAsTask<'T> 方法将 Async<'T> 转换为 Task<'T>
我通常建议为C#用户简化操作,并使用额外的方法扩展F#实现,该方法返回 Task<'T>。按照惯例,您可以将F#版本称为 AsyncFoo,将适合C#的版本称为 FooAsync
根据您的示例,我会采用以下方式:
type public Class1() =

    let printThread (message) = 
        printfn "%s %A" message Thread.CurrentThread.ManagedThreadId

    let bar = 
        printThread ("first bar")
        async { 
            printThread ("first async")
            do! Async.Sleep(1000)
            printThread "last async"
        }

    member this.AsyncFoo() =  bar

    /// Expose C#-friendly asynchronous method that returns Task
    member this.FooAsync() = Async.StartAsTask(bar)
    /// Expose C#-friendly asynchronous method that returns Task
    /// and takes cancellation token to support cancellation...
    member this.FooAsync(cancellationToken) = 
      Async.StartAsTask(bar, ?cancellationToken=cancellationToken)

2
正如我所述,这不能满足我的问题,会导致非异步代码在不同的线程中运行。 - Onur Gumus

2

这正好符合我的要求(与问题不同,此版本还返回了一个整数,这是加分项):

type public Class1() = 
    let printThread (message) = printfn "%s %A" message Thread.CurrentThread.ManagedThreadId

    let bar = 
        printThread ("first bar")
        async { 
            printThread ("first async")
            do! Async.Sleep(1000)
            printThread "last async"
            return 1232
         }

    member this.convertToTask<'T> (asyn : Async<'T>) = 
       let tcs1 = new TaskCompletionSource<'T>()
       let t1 = tcs1.Task
       Async.StartWithContinuations
        (
          asyn 
          , (fun (k) -> tcs1.SetResult(k)), (fun exn -> tcs1.SetException(exn)), fun exn -> ())
        t1

    member this.X() : Task<int> =  (bar |> this.convertToTask)

如果我运行这个程序,我会得到这样的输出:X 8, first bar 8, first async 8, last async 11,就像在我的(更简单的)答案中一样。 - Mark Seemann
哦,对不起,你可能正在尝试从控制台应用程序中运行它,同时挂起主线程。我从UI线程运行我的代码,所有的打印语句都返回相同的ID。你的代码存在调用RunSynchronously的问题。它要么会创建一个死锁,要么会在不同的线程中运行最后一个语句。 - Onur Gumus
+1 如果我认真阅读了问题,这就是我要写的内容! - Tomas Petricek

0

你觉得这样定义X怎么样?

member this.X() = 
    let t = new Task(fun () -> bar |> Async.StartImmediate)
    t.RunSynchronously()
    t

据我所知,它可以做你想要的事情。至少,这段 C# 代码:
Console.WriteLine("X " + Thread.CurrentThread.ManagedThreadId);
var c = new Class1();            
c.X().Wait();

Thread.Sleep(2000);

输出以下内容:

X 7
first bar 7
first async 7
last async 11

在C#中,X的签名为Task X()

所有线程 ID 必须为 7。我实际上已经找到了答案,将在下面发布。 - Onur Gumus

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