1

I have an async method which will load some info from the database via Entity Framework.

In one circumstance I want to call that code synchronously from within a lock.

Do I need two copies of the code, one async, one not, or is there a way of calling the async code synchronously?

For example something like this:

using System;
using System.Threading.Tasks;

public class Program
{
    public static void Main()
    {
        new Test().Go();
    }
}

public class Test
{
    private object someLock = new object();

    public void Go()
    {
        lock(someLock)
        {
            Task<int> task = Task.Run(async () => await DoSomethingAsync()); 
            var result = task.Result;
        }
    }
    
    public async Task<int> DoSomethingAsync()
    {
        // This will make a database call
        return await Task.FromResult(0);
    }
}

Edit: as a number of the comments are saying the same thing, I thought I'd elaborate a little

Background: normally, trying to do this is a bad idea. Lock and async are polar opposites as documented in lots of places, there's no reason to have an async call in a lock.

So why do it here? I can make the database call synchronously but that requires duplicating some methods which isn't ideal. Ideally the language would let you call the same method synchronously or asynchronously

Scenario: this is a Web API. The application starts, a number of Web API calls execute and they all want some info that's in the database that's provided by a service provider dedicated for that purpose (i.e. a call added via AddScoped in the Startup.cs). Without something like a lock they will all try to get the info from the database. EF Core is only relevant in that every other call to the database is async, this one is the exception.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
tony
  • 2,178
  • 2
  • 23
  • 40
  • 1
    Does this answer your question? [Calling async method synchronously](https://stackoverflow.com/questions/22628087/calling-async-method-synchronously) – Troopers Jan 27 '22 at 11:29
  • My advice would be to just not use synchronous code in the first place. Can't you do some refactoring to just do everything properly? – DavidG Jan 27 '22 at 11:34
  • You can't use async code in locks, and it needs a lock – tony Jan 27 '22 at 11:38
  • I didn't know if the lock introduced some subtle gotcha that I would need to be aware of, which makes it slightly different to the other question – tony Jan 27 '22 at 11:39
  • @tony I'm guessing that it doesn't need a lock; it needs *synchronization*, and you're familiar with using `lock` for that - but: it isn't the only option. – Marc Gravell Jan 27 '22 at 11:41
  • @tony how is EF Core involved with this question? It sounds like your real problem is very different. Why would you even need a lock if you can use `await context.SaveChangesAsync` to persist all pending changes in a single transaction? Are you trying to use a singleton DbContext in multiple threads? Did you try to speed up slow operations by "parallelizing" them instead of finding and fixing the actual delays? – Panagiotis Kanavos Jan 27 '22 at 11:45
  • `lock` has no effect on synchronous or asynchronous execution. It only locks a block of code to be executed by one thread at a time – Troopers Jan 27 '22 at 11:47
  • @tony `it needs a lock` why? DbContext doesn't need locks - it's not meant to be used by multiple threads *at all*. That's not an issue for the job it's meant to do though - a Unit of Work for Object to Relational Mapping. It's not a database connection – Panagiotis Kanavos Jan 27 '22 at 11:48
  • It needs a lock because there's a slow stored procedure which needs to be called once at startup, via EFCore, but many handlers called asynchronously need the result of that call – tony Jan 27 '22 at 11:52
  • @tony then a lock won't help at all, and EF Core shouldn't even be used. A lock won't prevent other threads, applications or web requests from calling the same stored procedure. To call a stored procedure all you need is plain old ADO.NET. You could use Dapper to reduce the 10 lines needed by ADO.NET to just `connection.ExecuteAsync("myproc",new {param1=123})` – Panagiotis Kanavos Jan 27 '22 at 11:53
  • Perhaps I wasn't clear. If anything else wants to call that stored procedure they can, that's not the issue. The issue is we need the SP to be called and the result stored, cached, and available to other handlers. – tony Jan 27 '22 at 11:55
  • @tony the question you posted has nothing to do with the real problem. EF Core isn't even relevant, locking won't help. What kind of application are you building? What do you mean by handlers? It matters. If you have a .NET Core app, you could use a Singleton or BackgroundService to execute that stored procedure, ensuring that only the BackgroundService itself could start loading and caching the result. No need to lock in that case. You could also use `Lazy` on a singleton to only load the data once. There are a lot of ways to do this that don't involve locking – Panagiotis Kanavos Jan 27 '22 at 11:59
  • This is a Web.API app. The application starts, a number of web.api calls execute and they all want some info that's in the database that's provided by a service provider dedicated for that purpose (i.e. a call added via AddScoped in the Startup.cs). Without something like a lock they will all try to get the info from the db. EF Core is only relevant in that every other call to the db is async, this one is the exception – tony Jan 27 '22 at 12:07
  • _"Without something like a lock they will all try to get the info from the database."_ and what is the problem here? Isn't it how it is supposed to work? – Guru Stron Jan 27 '22 at 13:21
  • The info is the same for all the calls, it's shared. Multiple reads would be inefficient. Only one is needed – tony Jan 27 '22 at 13:42

1 Answers1

3

You simply cannot use a lock with asynchronous code; the entire point of async/await is to switch away from a strict thread-based model, but lock aka System.Monitor is entirely thread focused. Frankly, you also shouldn't attempt to synchronously call asynchronous code; that is simply not valid, and no "solution" is correct.

SemaphoreSlim makes a good alternative to lock as an asynchronous-aware synchronization primitve. However, you should either acquire/release the semaphore inside the async operation in your Task.Run, or you should make your Go an asynchronous method, i.e. public async Task GoAsync(), and do the same there; of course, at that point it becomes redundant to use Task.Run, so: just execute await DoSomethingAsync() directly:

private readonly SemaphoreSlim someLock = new SemaphoreSlim(1, 1);
public async Task GoAsync()
{
    await someLock.WaitAsync();
    try
    {
        await DoSomethingAsync();
    }
    finally
    {
        someLock.Release();
    }
}

If the try/finally bothers you; perhaps cheat!

public async Task GoAsync()
{
    using (await someLock.LockAsync())
    {
        await DoSomethingAsync();
    }
}

with

internal static class SemaphoreExtensions
{
    public static ValueTask<SemaphoreToken> LockAsync(this SemaphoreSlim semaphore)
    {
        // try to take synchronously
        if (semaphore.Wait(0)) return new(new SemaphoreToken(semaphore));

        return SlowLockAsync(semaphore);

        static async ValueTask<SemaphoreToken> SlowLockAsync(SemaphoreSlim semaphore)
        {
            await semaphore.WaitAsync().ConfigureAwait(false);
            return new(semaphore);
        }
    }
}
internal readonly struct SemaphoreToken : IDisposable
{
    private readonly SemaphoreSlim _semaphore;
    public void Dispose() => _semaphore?.Release();
    internal SemaphoreToken(SemaphoreSlim semaphore) => _semaphore = semaphore;
}
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • This sounds like an XY Problem. If the OP is trying to access DbContext from multiple threads, or trying to "speed up" bulk operations, SemaphoreSlim won't help – Panagiotis Kanavos Jan 27 '22 at 11:49
  • @PanagiotisKanavos indeed; sharing a DB-context (or a connection) already sounds like a bad idea, and usually leads to unbounded memory growth (due to tracking), plus heavily constrained throughput (due to sunchronization) – Marc Gravell Jan 27 '22 at 11:51