vladek
vladek

Reputation: 617

Convert async computation to .NET task without creating a new thread

I am learning F# through building an ASP.NET Core app. I need to implement an interface Microsoft.AspNetCore.Identity.IUserStore<'TUser>; this interface features methods that return Task like:

member this.CreateAsync (user : User, cancellationToken : CancellationToken) =
    async {
        let connectionString = configuration.GetConnectionString("SocialScoreApp")
        let! rowCountResult = user |> createAsync connectionString
        match rowCountResult with
        | Ok _ -> return IdentityResult.Success
        | Error e ->
        let identityError = new IdentityError()
        identityError.Description <- e
        return IdentityResult.Failed(identityError)
    } |> Async.StartAsTask

But, according to the documentation, Async.StartAsTask "executes a computation on the thread pool". But this is an I/O bound operation so I don't want any new threads involved. Is there any way to achieve that?

Edit: This is the createAsync code for reference:

let createAsync (connectionString : string) (user:User) =
    let sql = "..."
    let args = dict [
        // ...
    ]
        
    async {
        use connection = new SqlConnection(connectionString)
        do! connection.OpenAsync() |> Async.AwaitTask

        try
            let! rowCount = connection.ExecuteAsync(sql, args) |> Async.AwaitTask
            return Ok rowCount
        with
        | e -> return Error e.Message
    }

Upvotes: 2

Views: 162

Answers (1)

Tomas Petricek
Tomas Petricek

Reputation: 243041

If you look at how things work, the StartAsTask (source) operation creates a TaskCompletionSource, which creates a task that is returned to the caller. This is later used to report the result of the computation. In the meantime, the computation is started using QueueAsync, which invokes QueueWorkItemWithTrampoline (source) and this uses standard .NET ThreadPool.QueueUserWorkItem.

This means that when you run StartAsTask, the async workflow is added to the thread pool. However, the key thing for IO-bound operations is that you are using let! in your snippet:

let connectionString = configuration.GetConnectionString("SocialScoreApp")
let! rowCountResult = user |> createAsync connectionString

The work item added to the thread pool will only run GetConnectionString. It will then call createAsync, which presumably contains some non-blocking IO operation. As soon as the evaluation gets to this point, the work in the thread pool completes and it schedules a callback to be called when the IO operation completes - the callback is invoked later and adds the rest of the computation to thread pool as a new work item. So, you are not blocking the thread pool while the IO operation is running.

Upvotes: 4

Related Questions