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

本文关键字:最佳 解决方案 问题 线程 共享 对象 克服 | 更新日期: 2024-10-18 06:08:16

使用.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。是吗?可以做些什么来克服这个问题?

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

我会将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();
        }
    }
}

如果我正确理解这个问题,那么访问Result对象就不会出现线程安全问题。

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

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

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

如果步骤不是一个接一个地运行,并且您有多个写入程序,则只需要ReadWriteLockSlim

您唯一关心的是从CalledFromWebService返回的Result对象的脏读取。您可以将布尔属性添加到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的分配之后,这确保了在IsStep1Valid设置为true之前,Step1Result具有从Task分配给它的值。

现在,当您通过调用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.
}

在尝试访问Step1Result属性之前检查IsStep1Valid的值可以确保不会对Step1Result属性进行脏读取。

更新:单独的web服务将无法访问windows服务中的结果对象,因为它们在单独的应用程序域中运行。您需要从windows服务内部公开一个web服务,并让windows服务的主线程加载web服务并调用后台任务。您不必公开此web服务。您仍然可以在IIS中或您最初打算的位置托管web服务。它将简单地调用由windows服务托管的web服务。