Is calling interface method from within Parallel.ForEachAsync() thread safe?

137 Views Asked by At

Here's some code just to hopefully make clear the situation I'm talking about:

public class Processor
{
    private readonly IRepository _repo; 
    private readonly IApiSrevice _apiService
    private readonly _mapper;
    
    public Processor(IRepository repo, IApiSrevice apiService, IMapper mapper)
    {
        _repo = repo;
        _apiService = apiService
        _mapper = mapper;
    }

    public async Task<IEnumerable<Thing>> ProcessStuff(IEnumerable<MyDto> dtos)
    {
        var people = await _apiService.GetPeople();
        
        ConcurrentBag<Location> things = new();
        var options =  new ParallelOptions { MaxDegreeOfParallelism = 3 };
        await Parallel.ForEachAsync(people, options, async(person, token ) =>
        {
            var locations = await _apiService.GetLocations(person.Id);
            
            IEnumerable<Thing> newThings = _mapper.Map(locations);      
            
            // maybe there's a repo call in here somewhere
            // _repo.AddThings(newThings);
            

            foreach(var thing in newThings)
            {
                things.Add(thing)
            }
            
        });
        
        return things;
    }
}

I think that just because of the nature of interfaces (hidden implementations) calling any method on one from within a Parallel loop is a bad idea: implementations might have methods that aren't thread-safe.

If so, how can I call out to methods on interfaces? I've done quite a bit of testing, both with Parallel.ForEachAsync() and a standard foreach loop, and I get identical results, but I'm not sure this is something I can count on. Running with the Parallel loop and 6 degrees of parallelism takes significantly less time, though.

1

There are 1 best solutions below

0
Guru Stron On

Interfaces are just a way to abstract a contract, as any abstraction it can become leaky so you might need to dig deeper into the implementation.

In this particular case they are not that different from any functionality encapsulation - it does not matter if you are calling a method defined on interface or in some class you still need to understand what it does and how it works or at least what concurrency guarantees it provides if you want to use it in potentially multithreaded context (i.e. Parallel.ForEachAsync in this case). Also (assuming you want to have some perfromance gains) you definitely will need to know how the actual implementation works to understand how much can be gained from parallelization.

One option to care a bit less about internal workings of interface implementation is to create a DI scope (assuming you are using that) per handler - for example by injecting IServiceScopeFactory and using to create scope and resolve dependencies (also can be encapsulated into some "iteration handler"), though in general it is still recommended to understand what the implementation does.

P.S.

  • ConcurrentBag might not be the best option to use here.

  • I've done quite a bit of testing, both with Parallel.ForEachAsync() and a standard foreach loop and get identical results...

    TBH I would expect perfromance gains "even" for MaxDegreeOfParallelism set to 3 but without seeing actual implementation it is hard to tell.