【问题标题】:Threading: allow one thread to access data while blocking others, and then stop blocked threads from executing the same code线程:允许一个线程访问数据同时阻塞其他线程,然后阻止被阻塞的线程执行相同的代码
【发布时间】:2011-01-07 16:07:35
【问题描述】:

想象一下带有一些内存缓存的最简单的数据库访问代码 -

if exists in cache  
    return object  
else  
    get from DB  
    add to cache  
    return object  

现在,如果数据库访问需要一秒钟,并且我有 5 个 ASP.Net 请求/线程在那一秒钟内访问相同的代码,我如何确保数据库只调用第一个?我在它周围有一个简单的线程锁,但这只是将它们以有序的方式排队,允许每个人依次调用数据库。我的数据存储库基本上一次读取整个表,所以我们不是在谈论 Get by Id 数据请求。

关于如何做到这一点的任何想法?线程等待句柄听起来几乎是我所追求的,但我不知道如何编码。

这肯定是常见的情况?

现有伪代码:

lock (threadLock)  
{  
    get collection of entities using Fluent NHib  
    add collection to cache  
}  

谢谢, 列

【问题讨论】:

    标签: c# multithreading


    【解决方案1】:

    您基本上已经回答了自己的问题。 “lock()”很好,它可以防止其他线程在任何其他线程在那里时继续进入该代码。然后,在锁内执行您的第一个伪代码。检查它是否已经缓存,如果没有,检索值并缓存它。然后下一个线程会进来,检查缓存,发现它可用并使用它。

    【讨论】:

    • 感谢罗杰。我已经想到了,但它稍微复杂一点,因为我还运行了一个后台缓存刷新机制,它将抢先刷新它。因此,如果我要检查它是否在缓存中,当被后台缓存刷新线程命中时,它总是会出现在缓存中。是的,我可以添加一个 boolean forceRefresh 参数(我已经尝试过了),但它看起来很乱......
    • 如果你想变得更复杂,你可以使用更高级的同步对象,例如互斥锁,它一次只允许一个东西拥有互斥锁,并允许你控制对变量的访问。您希望确保您持有锁的时间尽可能短,具体取决于您如何实现它,后台刷新线程可能会阻止对缓存值的访问。你总是可以有这样的逻辑,1)在缓存中吗?如果是,则从缓存中获取,否则 2) 锁定并再次检查缓存以查看自上次检查后是否有人将其放在那里,然后从缓存中获取或检索。
    • 抱歉,没有正确阅读您上次评论中的场景。是的,如果您想在后台线程需要强制更新它时跳过此代码中的缓存检查,您可以使用“forceRefresh”标志,或者您可以只执行 lock(threadLock)(这非常类似于使用 Mutex ) 在不同的函数中专门用于更新缓存。
    • 你要锁定什么为“threadLock”?整个实例? -> 糟糕的设计。使用信号量...
    • 感谢 Roger、Jaster、Joe 和 9dan 的回复,并对回复缓慢表示歉意。在执行数据库访问之前,我最终使用了我的原始解决方案,即有条件地重新检查缓存或 forceRefresh。我现在发现了 2 或 3 个其他相关的错误,它们是我最初困惑的原因。
    【解决方案2】:

    这肯定是常见的情况?

    不一定像你想象的那么普遍。

    在很多类似的缓存场景中:

    • 您描述的竞争条件不会经常发生(当缓存冷时它需要多个请求到达)

    • 数据库返回的数据是只读的,多个请求返回的数据本质上是可以互换的。

    • 检索数据库的成本并没有那么高,这很重要。

    但如果在某些情况下您绝对需要防止这种竞争条件,那么请按照 Roger Perkins 的建议使用锁。

    【讨论】:

    • 好吧,显式锁定会更好。否则,您很容易在黄金时段使 DB I/O 饱和,此时有效地禁用整个 Web 应用程序。锁定的成本一点也不高,因为正如这个答案所指出的那样,它不会经常发生。
    【解决方案3】:

    我会使用 Monitor/Mutext over lock。使用锁你需要指定一个资源(也可以使用this-pointer,不推荐)。 请尝试以下方法:

    Mutext myMutex = new Mutex();
    // if u want it systemwide use a named mutex
    // Mutext myMutex = new Mutex("SomeUniqueName");
    
    mutex.WaitOne();
    // or 
    //if(mutex.WaitOne(<ms>))
    //{
    // //thread has access
    //} 
    //else 
    //{
    // //thread has no access
    //}
    <INSERT CODE HERE>
    
    mutex.ReleaseMutex();
    

    【讨论】:

    • 但是您还刚刚创建了一个新资源来执行此操作(您可以轻松地创建一些“对象”类型的东西来使用锁,并且您将拥有一个健壮的临界区 - 这不是真的是争论互斥锁和关键部分之间区别的地方)。至少使用 lock() 将处理锁内抛出的任何异常(并自动释放它),在手动执行时您需要小心正确处理它。
    【解决方案4】:

    我不知道是否存在通用解决方案或已建立的算法。

    我个人使用下面的代码模式来解决这样的问题。

    1) 定义一个可以被所有线程访问的整型变量。

    int accessTicket = 0;  
    

    2) 修改代码块

    int myTicket = accessTicket;
    
    lock (threadLock)
    {
        if (myTicket == accessTicket)
        {
            ++accessTicket;
            //get collection of entities using Fluent NHib
            //add collection to cache
        }
    }
    

    更新

    此代码的目的不是防止重复缓存的多个 DB 访问。我们可以用普通的线程锁来做到这一点。

    通过使用这样的访问票,我们可以防止其他线程再次执行已经完成的工作。

    更新#2

    看有lock (threadLock)

    评论前看。

    投反对票前请仔细查看。

    【讨论】:

    • @9dan 您假设两个线程同时锁定锁,因此具有相同的 myTicket 值。如果第二个线程稍微落后,那么它可以在锁定之前获取 accessTicket 的更新值。
    • @9dan 说真的。它不是线程安全的。假设数据库查询需要 5 秒。这是从增加 accessTicket 点开始的 5 秒,随之而来的任何其他线程都将获得更新的值并因此通过“myTicket == accessTicket”检查。您查询锁()“外部”的值。它可以由任何其他线程在“任何”其他时间完成,即使其他线程正在执行数据库查询。
    • @9dan 其他人请跳进来告诉我我错了吗? accessTicket 停止第二个线程“仅”再次执行该工作,前提是它与最终完成工作的线程几乎同时通过(并且为 myTicket 分配了相同的值)。在我们处理此代码的任何其他时间,accessTicket 具有更大的值,因此 myTicket 也是如此,因此检查通过,我们再次查询数据库。不知道有多少种说法。
    • 来吧...使用“锁定标志”进行线程处理?这是您的第一个应用程序吗?就像使用轮询作为事件策略一样。这个不用讨论了,错了。要么使用信号量,要么远离线程!
    • 在我看来,accessTicket 的增量应该在查询之后而不是之前,因为其他线程在第一个线程的查询期间比在第一个线程获取锁并递增accessTicket
    猜你喜欢
    • 2013-07-11
    • 1970-01-01
    • 2017-08-13
    • 2012-01-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-03-27
    相关资源
    最近更新 更多