克服共享对象上的线程问题的最佳解决方案

时间:2013-12-17 16:31:49

标签: c# multithreading task-parallel-library

在.net 4.0中使用任务并行库,我想知道这种情况的最佳解决方案是什么:

我的代码正在启动执行大量长时间运行步骤的任务(步骤需要一个接一个地完成)。 我有一个对象Result,它汇总了每个步骤的结果。 结果对象在任务中被修改(因此在与此任务相关的线程中)。 我还有一个Web服务,我们可以在其中获取当前的Result对象以查看任务的进度。 所以Result对象是任务和我的代码的主线程之间的共享对象。实现这一目标的最佳方法是什么,以确保我没有线程问题和类似的东西?

这是我正在谈论的样本。请注意,_doWork不会像代码中那样是静态的,它将成为层次结构中更高级别的另一个类的成员。

using System.Threading.Tasks;

namespace ConsoleApplication
{
    public class Step1Result
    {
    }

    public class Step2Result
    {
    }

    public class Result
    {
        public Step1Result Step1Result;
        public Step2Result Step2Result;
    }

    class DoWork
    {
        public Result Result;

        public DoWork()
        {
            Result = new Result();
        }

        public void Process()
        {
            // Execute Step 1
            Result.Step1Result = Step1();
            Result.Step2Result = Step2();

            // Other Steps ( long - running )
        }

        public Step1Result Step1()
        {
            // Long running step that can takes minutes

            return new Step1Result();
        }

        public Step2Result Step2()
        {
            // Long running step that can takes minutes

            return new Step2Result();
        }
    }
    class Program
    {
        private static DoWork _doWork;

        static void Main(string[] args)
        {
            _doWork = new DoWork();

            var task = Task.Factory.StartNew(() => _doWork.Process());
            task.Wait();
        }

        // This method will be called from a web service at anytime.
        static Result CalledFromWebService()
        {
            return _doWork.Result;
        }
    }
}

这里遇到的麻烦是从Task和Main线程访问_doWork.Result。对吗?有什么办法可以解决这个问题?

3 个答案:

答案 0 :(得分:0)

我会将DoWork.Result属性更改为GetCurrentResult()方法,并在每次返回当前操作结果的新副本时返回(您可以使用MemberwiseClone复制对象)。我没有看到任何需要共享相同的对象。

附加,我会使用ReadWriteLockSlim。所以DoWork类看起来像这样

class DoWork
{
    private readonly Result _result;
    private readonly ReadWriteLockSlim _lock = new ReadWriteLockSlim();

    public DoWork()
    {
        _result = new Result();
    }

    public void Process()
    {
        // Execute Step 1
        Step1Result st1result = Step1();
        try
        {
             _lock.EnterWriteLock();
             _result.Step1Result = st1result;
        }
        finally
        {
             _lock.ExitWriteLock();
        }

        Step2Result st2result = Step2();
         try
        {
             _lock.EnterWriteLock();
             _result.Step2Result = st2result;
        }
        finally
        {
             _lock.ExitWriteLock();
        }

        // Other Steps ( long - running )
    }

    public Step1Result Step1()
    {
        // Long running step that can takes minutes

        return new Step1Result();
    }

    public Step2Result Step2()
    {
        // Long running step that can takes minutes

        return new Step2Result();
    }

    public Result GetCurrentResult()
    {
        try
        {
             _lock.EnterReadLock();
             return (Result)_result.MemberwiseCopy();
        }
        finally
        {
             _lock.ExitReadLock();
        }
    }

}

答案 1 :(得分:0)

如果我正确理解了问题,则访问Result对象时没有线程安全问题。

正如你所说,这些步骤必须一个接一个地完成,所以你将无法同时运行它们。 因此,在Process()内,您可以在任务中启动Step1,然后在另一个任务中使用Step2启动.Continue等等

因此,您只有一个编写器线程,并且没有并发问题。在这种情况下,如果您有另一个线程访问它并不重要 结果,如果这是一个只读的提取线程

如果从不同的线程访问集合,您只需要像ConcurrentDictionary这样的并发集合来存储结果。

如果步骤不是一个接一个地运行并且你有多个编写器,那么你只需要一个ReadWriteLockSlim

答案 2 :(得分:0)

这里唯一关心的是从Result返回的CalledFromWebService对象的脏读。您可以向Result对象添加布尔属性,并删除对锁的需要:

public class Result
{
    public volatile bool IsStep1Valid;
    public Step1Result Step1Result;

    public volatile bool IsStep2Valid;
    public Step2Result Step2Result;
}

布尔值的赋值是原子的,因此您不必担心脏读和写。然后,您可以在Process方法中使用这些布尔值,如下所示:

public void Process()
{
    // Execute Step 1
    Result.Step1Result = Step1();
    Result.IsStep1Valid = true;


    Result.Step2Result = Step2();
    Result.IsStep2Valid = true;

    // Other Steps ( long - running )
}

请注意,IsStep1Valid的分配是在Step1Result的分配之后确保Step1ResultIsStep1Valid设置为true之前从任务分配给它的值

现在,当您通过调用CalledFromWebService访问主线程中的结果时,您只需执行以下操作:

void MyCode() {
    var result = Program.CalledFromWebService();
    if (result.IsStep1Valid) {
         // do stuff with result.Step1Result
    } else {
        // if need be notify the user that step 1 is not complete yet
    }
    if (result.IsStep2Valid) {
         // do stuff with result.Step2Result
    }
    // etc.
}

在尝试访问IsStep1Valid属性之前检查Step1Result的值可确保您没有弄脏Step1Result属性。

更新:单独的Web服务无法访问Windows服务中的结果对象,因为它们在不同的应用程序域中运行。您需要从Windows服务中公开Web服务,并让Windows服务的主线程加载Web服务并调用您的后台任务。您不必公开公开此Web服务。您仍然可以在IIS中或您最初预期的位置托管Web服务。它只会调用由windows服务托管的Web服务。