克服共享对象上线程问题的最佳解决方案
本文关键字:最佳 解决方案 问题 线程 共享 对象 克服 | 更新日期: 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服务。