Search code examples
c#.net-coreconcurrencylockinginterlocked-increment

C#: Is it right to use Interlocked to count the number of calls of a delegate?


I am playing around with Interlocked.Increment and was wondering which of the call counter impl. below (to basically get the number of call of a given delegate) is doing its job properly (ie. thread-safe) in a highly concurrent environment.

public interface ICallCounter<in TInput, out TOutput>
{
    public TOutput Invoke(TInput input);
    public int Count { get; }
}
public class InterlockedPreCallCounter<TInput, TOutput> : ICallCounter<TInput, TOutput>
{
    private readonly Func<TInput, TOutput> _func;
    private int _count;
    public int Count => _count;

    public InterlockedPreCallCounter(Func<TInput, TOutput> func) => _func = func;

    public TOutput Invoke(TInput input)
    {
        Interlocked.Increment(ref _count);
        // What if there is an interruption / crash at some point here?
        return _func(input);
    }

}
public class InterlockedPostCallCounter<TInput, TOutput>
{
    private readonly Func<TInput, TOutput> _func;
    private int _count;
    public int Count => _count;
    public InterlockedPostCallCounter(Func<TInput, TOutput> func) => _func = func;

    public TOutput Invoke(TInput input)
    {
        var result = _func(input);
        Interlocked.Increment(ref _count);
        return result;
    }
}
public class LockCallCounter<TInput, TOutput> : ICallCounter<TInput, TOutput>
{
    private readonly Func<TInput, TOutput> _func;
    public int Count { get; private set; }
    private readonly object _syncRoot = new object();

    public LockCallCounter(Func<TInput, TOutput> func) => _func = func;

    public TOutput Invoke(TInput input)
    {
        lock (_syncRoot)
        {
            var result = _func(input);
            Count++;
            return result;
        }
    }
}

Solution

  • All methods are completely thread-safe, with respect to _count.

    However, this will increment _count irrespective as to whether _func throws an exception:

    Interlocked.Increment(ref _count);
    return _func(input);
    

    This will increment _count only if _func does not throw an exception.

    var result = _func(input);
    Interlocked.Increment(ref _count);
    return result;
    

    And this will do the same as the above, but with poorer performance in a multi-threaded environment, particularly because only one thread will be able to call _func at any one time:

    lock (_syncRoot)
    {
        var result = _func(input);
        Count++;
        return result;
    }
    

    Which you choose depends on what you are trying to measure.