【问题标题】:Thread safe with Linq and Tasks on a Collection使用 Linq 和集合上的任务实现线程安全
【发布时间】:2015-04-29 01:49:19
【问题描述】:

给定一些这样的代码

public class CustomCollectionClass : Collection<CustomData> {}
public class CustomData
{
    string name;
    bool finished;
    string result;
}

public async Task DoWorkInParallel(CustomCollectionClass collection)
{
    // collection can be retrieved from a DB, may not exist.
    if (collection == null)
    {
        collection = new CustomCollectionClass();
        foreach (var data in myData) 
        { 
            collection.Add(new CustomData()
            {
                name = data.Name;
            });
        }
    }

    // This part doesn't feel safe. Not sure what to do here.
    var processTasks = myData.Select(o => 
        this.DoWorkOnItemInCollection(collection.Single(d => d.name = o.Name))).ToArray();

    await Task.WhenAll(processTasks);

    await SaveModifedCollection(collection);
}

public async Task DoWorkOnItemInCollection(CustomData data)
{
    await DoABunchOfWorkElsewhere();
    // This doesn't feel safe either. Lock here?
    data.finished = true;
    data.result = "Parallel";
}

正如我在几个 cmets inline 中指出的那样,执行上述操作对我来说并不安全,但我不确定。我确实有一个元素集合,我想为每个并行任务分配一个唯一元素,并让这些任务能够根据完成的工作修改集合的单个元素。最终结果是,我想在并行修改不同元素之后保存集合。如果这不是一种安全的方法,我该怎么做?

【问题讨论】:

  • 我假设 DoWorkOnItemInCollection 里面至少有一个 await 你没有显示,对吗?
  • 是的,这很简单。我会添加一些东西来等待。
  • 你的代码不会编译,你不能像DoWorkInParallel那样在非async方法中使用await
  • 这是一个错字,它是异步的。固定。
  • 我假设 DoWorkInParallel 是由单个线程调用的,对吧?

标签: c# .net multithreading task-parallel-library


【解决方案1】:

您的代码是执行此操作的正确方法,假设多次启动 DoABunchOfWorkElsewhere() 本身是安全的。

您无需担心 LINQ 查询,因为它实际上并不并行运行。它所做的只是多次调用DoWorkOnItemInCollection()。这些调用可能并行工作(或不并行工作,取决于您的同步上下文和DoABunchOfWorkElsewhere() 的实现),但您显示的代码是安全的。

【讨论】:

    【解决方案2】:

    您上面的代码应该可以正常工作。您正在将一项传递给每个工作线程。我不太确定 async 属性。您可能只返回一个任务,然后在您的方法中执行:

    public Task DoWorkOnItemInCollection(CustomData data)
    {
        return Task.Run(() => {
            DoABunchOfWorkElsewhere().Wait();
            data.finished = true;
            data.result = "Parallel";
        });
    }
    

    您可能需要小心,如果有大量项目,后台线程可能会超出您的最大线程数。在这种情况下,c# 只是删除您的线程,这在以后可能难以调试。

    我以前做过这个,如果不是将整个集合交给一些神奇的 linq,而是做一个经典的消费者问题,这可能会更容易:

    class ParallelWorker<T>
    {
        private Action<T> Action;
        private Queue<T> Queue = new Queue<T>();
        private object QueueLock = new object();
        private void DoWork() 
        {
            while(true)
            {
                T item;
                lock(this.QueueLock)
                {
                    if(this.Queue.Count == 0) return; //exit thread
                    item = this.Queue.DeQueue();
                }
    
                try { this.Action(item); }
                catch { /*...*/ }
            }
        }
    
        public void DoParallelWork(IEnumerable<T> items, int maxDegreesOfParallelism, Action<T> action)
        {
            this.Action = action;
    
            this.Queue.Clear();
            this.Queue.AddRange(items);
    
            List<Thread> threads = new List<Thread>();
            for(int i = 0; i < items; i++)
            {
                ParameterizedThreadStart threadStart = new ParameterizedThreadStart(DoWork);
                Thread thread = new Thread(threadStart);
                thread.Start();
                threads.Add(thread);
            }
    
            foreach(Thread thread in threads)
            {
                thread.Join();
            }
        }
    }
    

    这是在免费的 IDE 中完成的,因此可能存在拼写错误。

    【讨论】:

    • 当你可以await时使用Wait()没有任何优势。
    • 你不能使用await,如果你的方法没有标记为async,我删除了async,因为我认为linq不支持。
    • 我说的是Task.Run() 中的代码(在这里使用它本身就是一个有问题的选择)。这与 LINQ 无关,当然可以设为 async
    • 我什至没有注意到您明确使用Threads 的第二个建议。那更糟。
    • 我想我关心的是我在一个 Select 中对我的集合调用 SIngle() 来查找与我的数据关联的自定义数据,然后修改自定义数据,这是一个由于这些集合都不是线程安全的(as discussed here),因此比我习惯的并行化程度更高。但也许我想多了?无论哪种方式,感谢您的确认。
    【解决方案3】:

    我将建议您使用 Microsoft 的 Reactive Framework (NuGet "Rx-Main") 来完成此任务。

    代码如下:

    public void DoWorkInParallel(CustomCollectionClass collection)
    {
        var query =
            from x in collection.ToObservable()
            from r in Observable.FromAsync(() => DoWorkOnItemInCollection(x))
            select x;
    
        query.Subscribe(x => { }, ex => { }, async () =>
        {
            await SaveModifedCollection(collection);
        });
    }
    

    完成。就是这样。仅此而已。

    但我不得不说,当我试图让您的代码运行时,它充满了错误和问题。我怀疑您发布的代码不是您的生产代码,而是您专门为这个问题编写的示例。我建议您在发布之前尝试制作一个可运行的可编译示例。

    不过,我的建议只要稍加调整就对你有用。

    它是多线程和线程安全的。完成后它确实会干净地保存修改后的集合。

    【讨论】:

    • 那么,您建议使用“一劳永逸”的方法吗?这对我来说听起来不是一个好的选择。
    • @svick - 为什么这是“一劳永逸”?你这是什么意思?
    • 您设置了 observable,然后设置了它完成时会发生什么,然后您立即返回。所以DoWorkInParallel() 很可能会在DoWorkOnItemInCollection()SaveModifedCollection() 完成之前返回,并且这些方法的任何异常都不会传播给调用者。
    • @svick - 这是一个公平的选择。解决这个问题并不需要太多,但我不想让解决方案过于复杂。我仍然认为 Rx 是做这种事情的更好方法。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-02
    • 2011-05-29
    • 1970-01-01
    • 2010-10-29
    相关资源
    最近更新 更多