【问题标题】:best solution to overcome threading issue on shared object克服共享对象上的线程问题的最佳解决方案
【发布时间】:2014-01-05 12:52:07
【问题描述】:

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

这里的麻烦是从任务和主线程访问 _doWork.Result。真的 ?可以做些什么来克服这个问题?

【问题讨论】:

  • 我已经编辑了你的标题。请参阅“Should questions include “tags” in their titles?”,其中的共识是“不,他们不应该”。
  • 您真正的问题是您没有正确建模服务请求。一旦你真正实现它,你会发现请求已经在一个工作线程上运行。无需启动自己的线程,无需使用全局变量。只需调用 Process 并返回 Result。
  • Hans - 需要明确的是,流程部分不会在 Web 服务调用中完成。 Web 服务调用将只检查当前结果值并将其返回给调用。当您说 Web 服务调用将在自己的线程中运行时,您是正确的。 Process 部分将在 Windows 服务的“主任务”内完成。

标签: c# multithreading task-parallel-library


【解决方案1】:

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

}

【讨论】:

    【解决方案2】:

    如果我正确理解了这个问题,那么您在访问 Result 对象时不会遇到线程安全问题。

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

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

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

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

    【讨论】:

    • TonyS - 你似乎正确理解了我的担忧。在 Process() 方法中,我不会启动任何新任务,我只会一个接一个地调用每个步骤(带有更多的业务逻辑)。所以所有步骤都将在同一个任务中运行(在这种情况下是在 main 中创建的那个)。所以你的解释说如果我只有一个作家(这是我的情况)和一个读者,我不需要任何线程安全机制?我理解正确吗?我必须承认,您的答案与下面的 Garf1eld 答案形成鲜明对比。
    • 我想是的。您的问题不需要作者和读者之间的同步。阅读器何时执行并不重要,因为它不会改变状态。你也正在获取,所以它会在之后很快执行,所以它确实似乎不需要同步。您唯一的问题可能是知道 stepresult 是否有效,为此我可以理解,如果 Step1Result 为 null 则 step1 尚未完成
    【解决方案3】:

    您唯一关心的是从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 之后,这确保Step1ResultIsStep1Valid 设置为true 之前从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 服务。

    【讨论】:

    • 您需要将“有效”布尔值标记为volatile,否则编译器可能会“优化”代码并使true 的分配在结果填充之前发生。
    • 我可能必须在实际结束 Step1 之前创建 Step1Result(以显示该步骤的进度将是该步骤正在执行的工作)。所以我可以在 Result 的构造函数中新建 Step1Result 成员,以使其始终存在。可能会向 Step1Result 添加一些成员,例如将从 step 方法更新的进度指示器。好吗?
    • Charles - 另外,需要明确的是,CalledFromWebService 不会从我的主线程调用,而是会从我的 Web 服务的实现中调用。
    • @Hockeymtl - Web 服务会成为后台线程的所有者吗?
    • @Charles Web 服务将不是后台线程的所有者。这将在 Windows 服务中运行。 Windows 服务启动第一个长时间运行的任务,该任务将触发我们在我的 main 中看到的代码。
    猜你喜欢
    • 2015-05-31
    • 2011-01-05
    • 2020-04-12
    • 1970-01-01
    • 2019-09-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-13
    相关资源
    最近更新 更多