Monitor.TryEnter用于多个资源

时间:2012-11-21 17:44:03

标签: c# thread-safety

我尝试搜索此内容,但未找到最适合我所面临问题的建议。

我的问题是我们有可用资源的列表/堆栈(计算引擎)。这些资源用于执行某些计算。

执行计算的请求是从外部进程触发的。因此,当进行计算请求时,我需要检查是否有任何可用资源当前没有执行其他计算,如果是,请等待一段时间再检查一下。

我想知道实现这个的最佳方法是什么。我有以下代码,但不确定它是否非常安全。

如果您有任何进一步的建议,那就太棒了:

void Process(int retries = 0) {
    CalcEngineConnection connection = null;
    bool securedConnection = false;
    foreach (var calcEngineConnection in _connections) {
        securedConnection = Monitor.TryEnter(calcEngineConnection);
        if (securedConnection) {
            connection = calcEngineConnection;
            break;
        }
    }
    if (securedConnection) {
        //Dequeue the next request
        var calcEnginePool = _pendingPool.Dequeue();

        //Perform the operation and exit.
        connection.RunCalc(calcEnginePool);
        Monitor.Exit(connection);
    }
    else {
        if (retries < 10)
            retries += 1;
        Thread.Sleep(200);
        Process(retries);
    }
}

2 个答案:

答案 0 :(得分:2)

我不确定在这里使用Monitor是最好的方法,但是如果你决定走这条路线,我会将上面的代码重构为:

bool TryProcessWithRetries(int retries) {
    for (int attempt = 0; attempt < retries; attempt++) {
        if (TryProcess()) {
            return true;
        }
        Thread.Sleep(200);
    }
    // Throw an exception here instead?
    return false;
}

bool TryProcess() {
    foreach (var connection in _connections) {
        if (TryProcess(connection)) {
            return true;
        }
    }
    return false;
}

bool TryProcess(CalcEngineConnection connection) {
    if (!Monitor.TryEnter(connection)) {
        return false;
    }
    try {
        var calcEnginePool = _pendingPool.Dequeue();
        connection.RunCalc(calcEnginePool);
    } finally {
        Monitor.Exit(connection);
    }
    return true;
}

这分解了三个逻辑:

  • 多次重试
  • 尝试集合中的每个连接
  • 尝试单一连接

它还避免了使用递归,并将Monitor.Exit调用放入finally块,绝对应该在其中。

您可以使用以下方法替换中间方法实现:

return _connections.Any(TryProcess);

......但这对于自己的利益来说可能有点过于“聪明”。

就我个人而言,我很想将TryProcess移到CalcEngineConnection本身 - 这样这个代码不需要知道连接是否能够处理某事 - 这取决于对象本身。这意味着您可以避免公开可见的锁定,并且如果某些资源可以(比如说)在将来一次处理两个请求,它也会很灵活。

答案 1 :(得分:1)

可能会出现多个问题,但我们首先要简化您的代码:

void Process(int retries = 0) 
{
    foreach (var connection in _connections) 
    {
        if(Monitor.TryEnter(connection))
        {
            try
            {
                //Dequeue the next request
                var calcEnginePool = _pendingPool.Dequeue();

                //Perform the operation and exit.
                connection.RunCalc(calcEnginePool);
            }
            finally
            {
                // Release the lock
                Monitor.Exit(connection);
            }
            return;
        }
    }

    if (retries < 10)
    {
        Thread.Sleep(200);
        Process(retries+1);
    }
}

这将正确保护您的连接,但请注意,此处的一个假设是您的_connections列表是安全的,并且不会被其他线程修改。

此外,您可能希望为_connections使用线程安全队列,因为在某些负载级别,您最终可能只使用前几个连接(不确定这是否会产生影响)。为了相对均匀地使用所有连接,我会将它们放入队列并将它们出列。这也将保证没有两个线程使用相同的连接,您不必使用Monitor.TryEnter()