Parallel.Foreach循环中的实例成员与线程安全性

时间:2016-01-18 14:42:24

标签: c# multithreading parallel-processing thread-safety locking

我会在下面提出问题,这些问题以某种方式相互关联并与下面的代码相关(简化 - 请参阅评论):

public class DataProcessor
{
    private DataUpdater dataUpdater = new DataUpdater(someConnection);

    public void Process()
    {
        Parallel.ForEach(itemCollection, (item, loopState) =>
        {
            ProcessItem(item);
            // Q1. is this thread safe? (as I understand, same DataUpdater instance for all threads)
            // Q4. should a new DataUpdater be created here and passed to ProcessItem as parameter instead?

            // (commented as not relevant to question)
            // if (mustStop()) 
            // {
            //     loopState.Break();
            // }
        });

    }

    private void ProcessItem(Item item)
    {
        // some processing...

        dataUpdater.Update(item); // Q2. should I lock here if 1. is not thread-safe?
    }
}

public class DataUpdater // Q3. could/should this be made a static helper class?
{
    private IDatabaseConnection databaseConnection;

    public DataUpdater(IDatabaseConnection databaseConnection)
    {
        this.databaseConnection = databaseConnection;
    }

    public void Update(item)
    {
        using (var ctx = new DataContext(databaseConnection.ConnectionString))
        {
            // Linq2Sql
            ctx.Item.InsertOnSubmit(item);
            ctx.SubmitChanges();
        }
    }
}

在上面的代码中,数据处理器使用Parallel.Foreach循环来加速ProcessItem方法中的一些繁重处理(与问题无关)。处理完成后,该项目将通过DataUpdater类在数据库中更新。

问题是:

  1. 当使用数据库更新程序作为DataProcessor的实例成员时,ProcessItem调用是否是线程安全的?
  2. 如果没有,在lock(obj)方法的dataUpdater.Update调用周围添加ProcessItem会解决线程安全问题,前提是该方法中的其余处理不会需要是线程安全的吗?
  3. 应该将类DataUpdater(及其方法)设为静态吗?这会消除线程安全问题吗?
  4. 是否应在循环中创建DataUpdater的新实例,并将其作为参数传递给ProcessItem
  5. 修改

    Update方法添加了代码。

1 个答案:

答案 0 :(得分:0)

  

当使用数据库更新程序作为DataProcessor的实例成员时,ProcessItem调用是否是线程安全的?

不能肯定地说,因为您没有准确地告诉我们Update()做了什么,也没有IDatabaseConnection的方法可以安全地同时调用多个线程,但它很可能不是线程安全的。

  

如果没有,在ProcessItem方法中对dataUpdater.Update的调用周围添加锁(obj)是否解决了线程安全问题,前提是该方法中的其余处理不需要是线程安全的?

可能会使其线程安全,但是您需要确保IDatabaseConnection是线程安全的,或者在循环运行时,在此循环之外的程序中的其他任何位置不同时使用。

  

应该将类DataUpdater(及其方法)设为静态吗?这会消除线程安全问题吗?

不,制作一些静态的东西并不能神奇地使某些东西变得安全,而是方法中的代码使其安全,如果它是静态的或没有效果。

  

是否应该在循环中创建一个新的DataUpdater实例并将其作为参数传递给ProcessItem?

是和否,Parallel.ForEach has overloads具有在每个线程的开头和结尾发生的特殊委托,它包含线程本地对象,并且该对象在每个调用的线程内重用。你应该在那里创建新的更新程序。

在下面的示例中,我做了一些小的假设,someConnection不是线程安全的,但提供了.Clone()方法,该方法创建了一个可以与旧连接同时使用的新连接。此外,我假设.Close()上有一个DataUpdater方法,可以调用该方法来关闭传入的连接。

public void Process()
{
    Parallel.ForEach(itemCollection,
        //This is called at the creation of a thread, may be called on many threads at once.
        () =>  
        {
            lock(someConnection)
            {
                var newConnection = someConnection.Clone();
                return new DataUpdater(newConnection)
            }
        };
        (item, loopState, dataUpdater) => //dataUpdater is the thread local copy
        {
            ProcessItem(item, dataUpdater);

            // (commented as not relevant to question)
            // if (mustStop()) 
            // {
            //     loopState.Break();
            // }

            //This will get passed in to the next item or the cleanup method.
            return dataUpdater;
         }
        //This is called at the end of a thread after it has finished processing items.
        , (dataUpdater) => dataUpdater.Close() 
        );
}

private void ProcessItem(Item item, DataUpdater dataUpdater)
{
    // some processing...

    dataUpdater.Update(item);
}

附录A:

ForEach在内部进行的简化示例。

public static ParallelLoopResult ForEach<TSource, TLocal>(IEnumerable<TSource> source, 
                       Func<TLocal> localInit,
                       Func<TSource, ParallelLoopState, TLocal, TLocal> body,
                       Action<TLocal> localFinally)
{
    ParallelLoopState[] loopStates;
    IEnumerable<TSource>[] jobBatches;
    // (SNIP)
    Task.Run(() => WorkerThread(jobBatchs[0], localInit, body, localFinally, loopStates[0]))
    Task.Run(() => WorkerThread(jobBatchs[1], localInit, body, localFinally, loopStates[1]))
    // (SNIP)
}

private static void WorkerThread<TSource, TLocal>(IEnumerable<TSource> jobBatch,
                       Func<TLocal> localInit,
                       Func<TSource, ParallelLoopState, TLocal, TLocal> body,
                       Action<TLocal> localFinally,
                       ParallelLoopState loopState)
{
    TLocal localData = localInit();
    foreach(TSource data in jobBatch)
    {
        if(loopState.ShouldExitCurrentIteration)
            break;
        localData = body(data, loopState, localData);
    }
    localFinally(localData);
}