如何防止方法跨多个线程运行?

Posted

技术标签:

【中文标题】如何防止方法跨多个线程运行?【英文标题】:How to prevent a method from running across multiple threads? 【发布时间】:2014-01-15 14:27:10 【问题描述】:

我正在开发一个 Web 应用程序,其中多个用户可以更新同一条记录。因此,如果用户同时更新同一条记录,为了避免出现问题,我将他们的更改保存在队列中。当每次保存发生时,我想调用一个在另一个线程上处理队列的方法,但我需要确保如果再次调用该方法不能在另一个线程中运行。我已经阅读了有关该主题的几篇文章,但不确定什么最适合我的情况。下面是我现在的代码。这是处理它的正确方法吗?

public static class Queue 
    static volatile bool isProcessing;
    static volatile object locker = new Object();

    public static void Process() 
        lock (locker) 
            if (!isProcessing) 
                isProcessing = true;

                //Process Queue...

                isProcessing = false;
            
        
    

【问题讨论】:

数据库知道如何避免同时更新问题。为什么要重新发明***? 数据存储在 Azure 表中。在原始记录的请求和更新之间,处理可能需要一些时间。如果此后有其他用户更新,存储客户端只会抛出错误。 @user2628438 我已经更新了中止安全的答案。这意味着如果拥有锁的线程被中止(无论如何你都不应该这样做)另一个线程可以进入。我还修复了第二个示例中的错误(如果TryEnter 成功,我必须调用Exit 来完成临界区)。注意:lock 自 C# 4.0 起是中止安全的。 @Theraot,Monitor.TryEnter 示例中的_syncroot 对象是什么?还是应该在那里使用locker 对象? @user2628438 是的,那就是locker。我已经更新了。 syncrootMonitor 中使用的对象的通用名称,因为当微软认为公开它们是个好主意时,这就是它们的名称。您仍然可以找到暴露 SyncRoot 的类,这些类来自 .NET 2.0 或更早版本(它是 ICollection 接口的一部分,不是通用接口)。您应该使用这些类公开的SyncRoot 吗?没有。 【参考方案1】:

新答案

如果您要将这些记录持久化到数据库(或数据文件,或类似的持久性系统),您应该让底层系统处理同步。正如JohnSaunders pointed out 数据库已经处理同步更新。


假设您想保留记录……problem presented by John 是您仅在 Web 应用程序的单个实例中同步对数据的访问。尽管如此,可能有多个实例同时运行(例如在服务器场中,如果您有高流量,这可能是一个好主意)。在这种情况下,使用队列来防止同时写入不够好,因为网页的多个实例之间仍然存在竞争条件。

在这种情况下,当您从不同实例获取同一记录的更新时,底层系统无论如何都必须处理冲突,但由于更新顺序已丢失,它无法可靠地处理冲突.

除了这个问题,如果你使用这个数据结构作为缓存,那么它会提供不正确的数据,因为它不知道发生在另一个实例中的更新。


话虽如此,对于可能值得使用线程安全队列的场景。对于这些情况,您可以使用ConcurrentQueue(正如我在原始答案末尾提到的那样)。

我会保留我原来的答案,因为我认为帮助理解 .NET 中可用的线程同步机制很有价值(我介绍了其中的一些)。


原答案

使用lock足以防止多个线程同时访问一个代码段(这就是互斥)。

这里我把你不需要的东西注释掉了:

public static class Queue 
    // static volatile bool isProcessing;
    static /*volatile*/ object locker = new Object();

    public static void Process() 
        lock (locker) 
            // if (!isProcessing) 
            //  isProcessing = true;

                //Process Queue...

            //  isProcessing = false;
            // 
        
    

lock 不需要volatile 工作。但是,由于此处未包含其他代码,您可能仍需要将变量设为 volatile


话虽如此,所有尝试进入lock 的线程都将在队列中等待。据我了解,这不是您想要的。相反,您希望所有其他线程跳过该块并只留下一个来完成工作。这可以通过Monitor.TryEnter 来完成:

public static class Queue

    static object locker = new Object();

    public static void Process()
    
        bool lockWasTaken = false;
        try
        
            if (Monitor.TryEnter(locker))
            
                lockWasTaken = true;
                //Process Queue…
            
        
        finally
        
            if (lockWasTaken)
            
                Monitor.Exit(locker);
            
        
    


另一个不错的选择是使用Interlocked:

public static class Queue

    static int status = 0;

    public static void Process()
    
        bool lockWasTaken = false;
        try
        
            lockWasTaken = Interlocked.CompareExchange(ref status, 1, 0) == 0;
            if (lockWasTaken)
            
                //Process Queue…
            
        
        finally
        
            if (lockWasTaken)
            
                Volatile.Write(ref status, 0);
                // For .NET Framework under .NET 4.5 use Thread.VolatileWrite instead.
            
        
    


无论如何,您不需要实现自己的线程安全队列。你可以使用ConcurrentQueue。

【讨论】:

-1:在 Web 应用程序中不够好。在回收的情况下,或者在网络农场或网络花园的情况下,可能有多个 AppDomain 副本在运行。 @JohnSaunders 你是在告诉我多个 AppDomain 共享静态字段吗? 不,我告诉你,可能有多个 AppDomains 同时运行相同的代码,所以静态的多个副本,这意味着多个 AppDomains 之间没有同步。 @JohnSaunders 是的,因为它不是相同的数据。尝试在此级别同步它们是没有意义的。如果我们正在处理需要跨 AppDomains 保存的数据,那么 - 正如您所建议的 - 使用数据库是一个更好的主意。我不确定这是否是 user2628438 想要的。 为什么不是相同的数据?它是同一个 Web 应用程序,并且可能是同一组用户。【参考方案2】:

lock 很好,但不适用于async await。如果您尝试在锁中等待方法调用,您将收到以下错误:

CS1996 不能在 lock 语句的主体中等待

在这种情况下,您应该使用 SemaphoreSlim

例子:

public class TestModel : PageModel

    private readonly ILogger<TestModel> _logger;
    private static readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1);

    public TestModel(ILogger<TestModel> logger)
    
        _logger = logger;
    

    public async Task OnGet()
    
        await _semaphoreSlim.WaitAsync();
        try
        
            await Stuff();
        
        finally
        
            _semaphoreSlim.Release();
        
    

重要的是不要在构造函数或其他任何地方新建SemaphoreSlim,因为那样它就不起作用了。

https://***.com/a/18257065/3850405

https://docs.microsoft.com/en-us/dotnet/api/system.threading.semaphoreslim?view=net-5.0

【讨论】:

以上是关于如何防止方法跨多个线程运行?的主要内容,如果未能解决你的问题,请参考以下文章

实体框架:如何防止 dbcontext 被多个线程访问?

防止跨多个服务器重复 POST API 调用

如何在 .net Core API 项目中跨多个线程限制对 HttpClient 的所有传出异步调用

如何获取redis的活动线程数量 java

虚拟机的那个线程是不是需要每个线程有多个实例?

跨线程合并更改时如何防止竞争条件?