1

昨天我发现我们使用的一个简单缓存对象存在多线程问题:

 If Dictionary.Contains(lsKey.ToLower) Then 'if rate cached, then return value
      lvResult = Dictionary.Item(lsKey.ToLower)

  Else 'else retrieve from database, store, and return value
      lvResult = GetRateFromDB(voADO,
                               veRateType,
                               vdEffDate)
      Dictionary.Add(lsKey.ToLower, lvResult)

  End If

我们在我们的 asp.net 网站上发现了这个问题。消息中的错误信息类似于“您试图向已经存在的哈希表添加值。从上面的代码中可以看出,这种情况发生的可能性肯定会退出。我对等待句柄有些熟悉,并认为它们会解决问题. 所以我在班级级别声明了我的等待句柄:

private Shared _waitHandle as new AutoResetEvent(True)

然后在有问题的特定代码部分:

_waitHandle.Wait()
If Dictionary.Contains(lsKey.ToLower) Then 'if rate cached, then return value
    lvResult = Dictionary.Item(lsKey.ToLower)

Else 'else retrieve from database, store, and return value
    lvResult = GetRateFromDB(voADO,
                             veRateType,
                             vdEffDate)
     Dictionary.Add(lsKey.ToLower, lvResult)
End If
_waitHandle.Set()

由于某种原因,上面的以下代码总是被阻止。即使是第一个线程也访问了代码。我玩了一段时间,甚至尝试在构造函数中将等待句柄设置为发出信号,但我永远无法让它工作。

我最终改用以下方法,效果很好:

SyncLock loLock
    If Dictionary.Contains(lsKey.ToLower) Then 'if rate cached, then return value
        lvResult = Dictionary.Item(lsKey.ToLower)

    Else 'else retrieve from database, store, and return value
        lvResult = GetRateFromDB(voADO,
                                 veRateType,
                                 vdEffDate)
        Dictionary.Add(lsKey.ToLower, lvResult)

    End If
End SyncLock

所以我有两个问题:

  1. 为什么等待句柄解决方案不起作用?
  2. SynLock 是在这种情况下使用的正确/优化的锁类型吗?
4

2 回答 2

1

1 个等待句柄阻塞,直到发出信号。您需要在某处发出信号来指示该等待句柄,以使其不会阻塞第一个线程以获取访问权限。我相信如果您在创建等待句柄的构造函数中发出句柄信号,它将起作用。考虑它就像在等待句柄中有一个信号槽一样,任何调用等待的线程都会等待,直到它可以在离开等待调用之前消耗一个信号。

2 在这种情况下,它可能不是最好的锁,如果两个线程试图读取一个已经在缓存中的值,那么一个将被阻塞,直到另一个完成。我会改用读写器锁。这样多个线程可以同时读取缓存,并且您可以在必要时升级为写入。

如果您不介意将相同值多次加载到缓存中,则可以使用并发字典。任何需要尚未加载的值的线程都会加载它,然后调用 tryadd。在多个线程尝试同时访问相同的卸载值的情况下,所有线程都将执行调用 GetRateFromDB 的工作。

于 2013-08-21T15:07:33.363 回答
1

Windows 条款:

  • 事件”允许一个线程(或进程)向另一个线程(或进程)发出信号。
  • 信号量”类似于事件,但它可用于通知(唤醒)特定数量的线程。
  • 临界区”用于防止同时访问代码块。
  • Slim Lock ”类似于临界区,但通常不允许拥有锁的线程多次进入它(允许使用临界区)。
  • Reader Writer ”锁使您可以灵活地将对象锁定为独占模式(类似于关键部分)或共享模式,这将允许多个线程执行同一块。
  • 为了完整起见,还有一个“互斥锁”,它与临界区非常相似,但可以跨进程共享。

所以使用AutoResetEvent上面的绝对不是你想要的。当一个线程调用其中一个Wait方法时,它将阻塞,直到它接收到来自另一个线程的信号。没有人向你发出信号,所以你永远等待。

SyncLock语句在幕后使用了一个关键部分,并防止任何线程同时进入相同的代码块。这将为您提供所需的保护。但是因为您需要保护对 Dictionary 对象的所有访问以避免损坏,所以您需要在使用 Dictionary 对象的任何地方都使用锁。

如前所述,ConcurrentDictionary在这种情况下使用它是一件好事,因为它本质上内置了一个高度调整的 Reader Writer 锁。因此,您不必在整个代码库中添加一堆锁。但正如我在评论中指出的那样,使用ConcurrentDictionary时您仍然可以有竞争条件。

于 2013-08-21T15:45:54.653 回答