从 Parallel.ForEachAsync() 线程中调用接口方法是否安全?

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

提问人:RobC 提问时间:10/9/2023 最后编辑:TylerHRobC 更新时间:10/10/2023 访问量:62

问:

这里有一些代码,希望能清楚地说明我所说的情况:

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;
    }
}

我认为仅仅因为接口(隐藏实现)的性质,从并行循环中调用任何方法都是一个坏主意:实现可能具有非线程安全的方法。

如果是这样,如何调用接口上的方法?我已经做了相当多的测试,包括标准的foreach循环和标准foreach循环,我得到了相同的结果,但我不确定这是我可以指望的。不过,使用 Parallel 循环和 6 度并行度运行所需的时间要少得多。Parallel.ForEachAsync()

C# 异步 任务并行库 parallel.foreachasync

评论

2赞 Marc Gravell 10/9/2023
我不认为有 100% 正确或错误的答案——它是上下文的。然而,在一般情况下,当你不知道抽象背后可能发生了什么,或者事物是如何交互的时,是的:我同意引入并发性是一个坏主意——这可能会导致可通过多条路径访问的对象以不可预测的方式被两个线程接触,或者它可能只是具有奇怪的不可预测的性能;但是,如果您知道这些操作是隔离的:是的,并发操作非常有意义
1赞 Panagiotis Kanavos 10/9/2023
重要的是你做了什么,而不是你是否通过接口去做。例如,使用并行执行来加速错误的数据库查询将导致更多延迟。假设服务器/服务可以处理额外的负载,则发出 6 个并发 HTTP 请求会更快。DOP 过高可能会让您受到限制。
0赞 Panagiotis Kanavos 10/9/2023
例如,执行 6 次查询将导致 6 次全表扫描,对整个表进行共享锁,阻止修改,并可能导致死锁。 只会这样做一次。如果字段已编制索引,则仍然更好,因为 6 个连接的开销可能高于查询本身的成本。SELECT * FROM Person where UnindexedID=@idSELECT ... WHERE unindexedid in (@id1, @id2,...,@id6)IDIN
0赞 Theodor Zoulias 10/9/2023
您的问题更侧重于调用在接口上定义的 API 而不是具体类的线程安全方面,还是在性能方面?
1赞 RobC 10/10/2023
@TheodorZoulias:我的意思是让我的问题更多地集中在使用接口的线程安全方面,而不是性能方面。我提到性能信息只是为了避免任何“你真的从并行性中获得任何好处吗?”的问题。

答:

3赞 Guru Stron 10/9/2023 #1

接口只是抽象合约的一种方式,因为任何抽象都可能变得漏洞百出,因此您可能需要更深入地研究实现。

在这种特殊情况下,它们与任何功能封装都没有太大区别——无论你是调用在接口上定义的方法,还是在某个类中定义的方法,你仍然需要了解它的作用和工作原理,或者至少,如果你想在潜在的多线程上下文中使用它,它提供了什么并发保证(即 在这种情况下)。此外(假设您希望获得一些性能提升),您肯定需要知道实际实现是如何工作的,以了解从并行化中可以获得多少收益。Parallel.ForEachAsync

一个不太关心接口实现内部工作的选项是为每个处理程序创建一个 DI 范围(假设您正在使用它)——例如,通过注入 IServiceScopeFactory 并用于创建范围和解析依赖项(也可以封装到一些“迭代处理程序”中),尽管通常仍然建议了解实现的作用。

附言

  • ConcurrentBag 可能不是在这里使用的最佳选择

  • 我已经做了相当多的测试,包括 Parallel.ForEachAsync() 和标准的 foreach 循环,并得到了相同的结果......

    TBH:我预计设置为 3 的性能增益“均匀”,但如果没有看到实际实现,就很难说。MaxDegreeOfParallelism