Search code examples
c#multithreadingasp.net-coreasynchronousnito.asyncex

How to check if another thread is within a locked section, using AsyncEx?


I've just started using Nito.AsyncEx package and AsyncLock instead of a normal lock() { ... } section where I have async calls within the locked section (since you can't use lock() in such cases for good reasons I've just read about). This is within a job that I'm running from Hangfire. Let's call this the 'worker' thread.

In another thread, from an ASP.NET controller, I'd like to check if there's a thread that's currently executing within the locked section. If there's no thread in the locked section then I'll schedule a background job via Hangfire. If there's already a thread in the locked section then I don't want to schedule another one. (Yes, this might sound a little weird, but that's another story).

Is there a way to check this using the Nito.AsyncEx objects, or should I just set a flag at the start of the locked section and unset it at the end?

e.g. I'd like to this:

public async Task DoAJobInTheBackground(string queueName, int someParam)
{ 
    // do other stuff...

    // Ensure I'm the only job in this section
    using (await _asyncLock.LockAsync())
    {           
        await _aService.CallSomethingAsync());           
    }

    // do other stuff...
}

and from a service called by a controller use my imaginary method IsSomeoneInThereNow():

public void ScheduleAJobUnlessOneIsRunning(string queueName, int someParam)
{ 

    if (!_asyncLock.IsSomeoneInThereNow())
    {
        _backgroundJobClient.Enqueue<MyJob>(x => 
            x.DoAJobInTheBackground(queueName, someParam));
    }

}

but so far I can only see how to do this with a separate variable (imagining _isAnybodyInHere is a thread-safe bool or I used Interlocked instead):

public async Task DoAJobInTheBackground(string queueName, int someParam)
{ 
    // do other stuff...

    // Ensure I'm the only job in this section
    using (await _asyncLock.LockAsync())
    {
        try
        {
            _isAnybodyInHere = true; 
            await _aService.CallSomethingAsync());
        }
        finally
        {
            _isAnybodyInHere = false; 
        }
    }

    // do other stuff...
}

and from a service called by a controller:

public void ScheduleAJobUnlessOneIsRunning(string queueName, int someParam)
{ 

    if (!_isAnybodyInHere)
    {
        _backgroundJobClient.Enqueue<MyJob>(x => 
            x.DoAJobInTheBackground(queueName, someParam));
    }

}

Really it feels like there should be a better way. The AsyncLock doc says:

You can call Lock or LockAsync with an already-cancelled CancellationToken to attempt to acquire the AsyncLock immediately without actually entering the wait queue.

but I don't understand how to do that, at least using the synchronous Lock method.


Solution

  • I don't understand how to do that

    You can create a new CancellationToken and pass true to create one that is already canceled:

    using (_asyncLock.Lock(new CancellationToken(canceled: true)))
    {
      ...
    }
    

    The call to Lock will throw if the lock is already held.

    That said, I don't think this is a good solution to your problem. There's always the possibility that the background job is just about to finish, the controller checks the lock and determines it's held, and then the background job releases the lock. In that case, the controller will not trigger a background job.