只允许一个线程获得结果

时间:2014-04-28 09:40:33

标签: c# multithreading race-condition

我在并行编程方面有点新,并试图为下一个问题找到解决方案: 让我们有一个简单的功能:

private void doRefreshData()
{
  items = getUpdatedData();
}

getUpdatedData()是一个真正的时间comsumpting函数和返回字典。 问题是,函数doRefreshData()可能会立即从多个线程调用。我想实现,只有第一个线程会运行函数,其他线程只是等待结果。简单的lock()不好,因为其他线程也会运行getUpdatedData(),即使没有必要 - 他们已经完成了其他线程的工作。

所以我需要这样的东西:

private void doRefreshData()
{
  if (isAlreadyRunning) {
    waitForResult();
    return;
  }
}

  items = getUpdatedData();
}

我知道,有一种解决方法,但我确信,已经有解决此问题的方法(Mutex?,Auto / ManualResetEvent?,SomethingElseWithFancyName?)

更新:第一个线程完成工作后,其他人也离开了,有必要允许新线程运行此功能 - 即使几秒钟后,也可以请求刷新新数据。

4 个答案:

答案 0 :(得分:4)

您可以使用任务和async / await轻松实现所需的逻辑。

doRefreshData方法应返回相应类型的Task<TResult>。这是一个虚拟方法,它在两秒钟内不执行任何操作,然后返回“结果”:

async Task<object> RealGetData()
{
    await Task.Delay(2000);
    return 42;
}

由于您希望多个方法同时请求数据并使其请求满足相同的结果,因此您需要以某种方式记住已经存在“获取数据”操作。您还需要某种线程同步来消除竞争条件,因此:

private Task<object> currentCalculation;
private object lockTarget = new object(); // just for lock()

现在编写一个async方法是非常简单的,如果没有待处理的话,它会启动一个新的计算,或者将你挂钩以接收待处理的结果:

async Task<object> GetData()
{
    lock (lockTarget)
    {
        if (currentCalculation != null && !currentCalculation.IsCompleted)
        {
            return currentCalculation;
        }

        return currentCalculation = Task.Run<object>(RealGetData);
    }
}

使用它非常简单:只要您想要访问“新鲜”数据,请写await GetData(),然后您将始终获得新结果;代码将自动排队以获取当前计算的结果或为您开始新的计算。例如,这将从单个计算开始,并满足所有10个请求及其结果:

for (var i = 0; i < 10; ++i) {
    Console.WriteLine(await GetData());
}

答案 1 :(得分:3)

您可以考虑使用lock语句实际生成的代码变体,使用Monitor.TryEnter()代替Monitor.Enter

if (Monitor.TryEnter(_myLock))
{
    try
    {
        // Your code
    }
    finally
    {
        Monitor.Exit(_myLock);
    }
}
else
{
    // Do something else
}

然后,如果其他线程已经获得锁定,则可以改为执行其他操作。

答案 2 :(得分:2)

如果只需要开一次,我建议使用Lazy<T>。实际上,我会使用Task<T>和一些Interlocked来电。类似的东西:

private Task<DataValue> _pendingRefresh;

private void doRefreshData()
{
    var tcs = new TaskCompletionSource<DataValue>();
    //See if there's one in flight
    var task = Interlocked.CompareExchange(ref _pendingRefresh, tcs.Task, null);
    if (task != null)
    {
        task.Wait(); //Or async, etc
        var items = task.Result;
    }
    else
    {
      try{
        var items = getUpdatedData();
        tcs.SetResult(items); //?
      } catch (Exception ex) {
          tcs.SetException(ex);
          throw;
      } finally {
        //Allow a new call to run 
        Interlocked.Exchange(ref _pendingRefresh, null); 
      }
    }
}

基本上,只有一位来电者可以将_pendingRefreshnull更改为null。然后它将完成工作,并在工作完成后发出Task信号。其他来电者会进入if的上半部分,并等待Task被标记为完整。

一旦被允许进行工作的&#34;来电者&#34;已完成并发出任务信号,它将_pendingRefresh设置回null。允许新呼叫者完成工作并拨打新电话。


请注意,我还添加了一些更好的错误处理功能,以便在getUpdatedData抛出异常时,您不会因为等待任务完成该任务而导致大量呼叫者陷入困境永不。他们会收到错误,但至少你会在所有受影响的线程上了解它。

答案 3 :(得分:0)

我的建议是在这里使用一个名为Mutex。在这里命名的原因是为了更容易设置共享同步点。仅在跨线程共享互斥锁时,不命名互斥锁。你可以在这里做到这一点,但我发现命名它们更容易。

// Create a new named mutex and try to obtain a lock on it immediately.
// The WaitOne operation below ensures that you will have to wait when
// another thread has the lock.
Mutex mutex = new Mutex(true,"SynchronisationPoint");

// Wait for the mutex to become available.
// If you get beyond this point it means that you are the only 
// one performing this operation.
mutex.WaitOne();

// Check if the operation was already completed.
// When it was, do nothing here.
if(isCompleted) {
  return;
}

// Perform the operation.
items = getUpdatedData();