如何防止方法跨多个线程运行?
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
。我已经更新了。 syncroot
是Monitor
中使用的对象的通用名称,因为当微软认为公开它们是个好主意时,这就是它们的名称。您仍然可以找到暴露 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
【讨论】:
以上是关于如何防止方法跨多个线程运行?的主要内容,如果未能解决你的问题,请参考以下文章