简体   繁体   English

如何编写以实体 ID 为条件的 C# 互斥块?

[英]How to code a C# mutex block that is conditional on an entity id?

I'm looking for a C# pattern for coding a synchonized operation including writes to two different databases for a particular entity such that I can avoid race conditions for simultaneous operations on the same entity.我正在寻找用于编码同步操作的 C# 模式,包括为特定实体写入两个不同的数据库,这样我就可以避免在同一实体上同时操作的竞争条件。

Eg Thread 1 and thread 2 are processing an operation on entity X at the same time.例如,线程 1 和线程 2 同时处理实体 X 上的操作。 The operation writes information for X to database A (in my case, an upsert to MongoDB) and database B (an insert to SqlServer).该操作将 X 的信息写入数据库 A(在我的情况下,是对 MongoDB 的 upsert)和数据库 B(对 SqlServer 的插入)。 Thread 3 is processing the same operation on entity Y. The desired behavior is:线程 3 正在处理实体 Y 上的相同操作。所需的行为是:

  • Thread 1 blocks thread 2 while processing writes to A and B for entity X.线程 1 在处理实体 X 对 A 和 B 的写入时阻塞线程 2。
  • Thread 2 waits until thread 1 completes writes to A and B and then makes writes to A and B for entity X.线程 2 一直等到线程 1 完成对 A 和 B 的写入,然后对实体 X 写入 A 和 B。
  • Thread 3 is not blocked and processes writes to A and B for entity Y while thread 1 is processing线程 3 未被阻塞,并且在线程 1 正在处理时处理实体 Y 对 A 和 B 的写入

The behavior I'm trying to avoid is:我试图避免的行为是:

  • Thread 1 writes to A for entity X.线程 1 为实体 X 写入 A。
  • Thread 2 writes to A for entity X.线程 2 为实体 X 写入 A。
  • Thread 2 writes to B for entity X.线程 2 为实体 X 写入 B。
  • Thread 1 writes to B for entity X.线程 1 为实体 X 写入 B。

I could uses a mutex across all threads, but I don't really want to block the operation for a different entity.我可以在所有线程中使用互斥锁,但我真的不想阻止其他实体的操作。

Using the lock statement is insufficient for multiple processes 1 .多进程使用lock语句不够1 Even named/system semaphores are limited to a single-machine and thus insufficient from multiple servers.甚至命名/系统信号量也仅限于单台机器,因此对于多台服务器来说是不够的。

If duplicate processing is OK and a "winner" can be selected, it may be sufficient just to write/update-over or use a flavor of optimistic concurrency .如果重复处理没问题并且可以选择“赢家”,那么只需要写入/更新或使用乐观并发的风格就足够了。 If stronger process-once concurrently guarantees need to be maintained, a global locking mechanism needs to be employed - SQL Server supports such a mechanism via sp_getapplock .如果需要维护更强的 process-once 并发保证,则需要采用全局锁定机制 - SQL 服务器通过sp_getapplock支持这种机制。

Likewise, the model can be updated so that each agent 'requests' the next unit of work such that dispatch can be centrally controlled and that an entity, based on ID etc., is only given to a single agent at a time for processing.同样,可以更新 model,以便每个代理“请求”下一个工作单元,以便可以集中控制调度,并且基于 ID 等的实体一次只提供给单个代理进行处理。 Another option might be to use a Messaging system like RabbitMQ (or Kafka etc., fsvo);另一种选择可能是使用像RabbitMQ (或 Kafka 等,fsvo)这样的消息系统; for RabbitMQ, one might even use Consistent Hashing to ensure (for the most part) that different consumers receive non-overlapping messages.对于 RabbitMQ,甚至可以使用 一致性哈希来确保(在大多数情况下)不同的消费者接收到不重叠的消息。 The details differ based on implementation used.细节因使用的实现而异。

Due to the different nature of a SQL RDBMS and MongoDB (especially if used as "a cache"), it may be sufficient to loosen the restriction and/or design the problem using MongoDB as a read through (which is a good way to use caches).由于 SQL RDBMS 和 MongoDB 的不同性质(特别是如果用作“缓存”),使用 Z206E3718AF092FD1D1D1D12F80CAE771 来放松限制和/或设计问题可能就足够了(这是一种很好的通读方式缓存)。 This can mitigate the paired-write issue, although it does not prevent global concurrent processing of the same items.这可以缓解配对写入问题,尽管它不会阻止对相同项目的全局并发处理。

1 Even though a lock statement is globally insufficient , it can be still be employed locally between threads in a single process to reduce local contention and/or minimize global locking. 1即使锁语句全局不足,它仍然可以在单个进程中的线程之间本地使用,以减少局部争用和/或最小化全局锁定。


The answer below was for the original question, assuming a single process .下面的答案是针对原始问题的,假设是一个 process

The "standard" method of avoiding working on the same object concurrently via multiple threads would be with a lock statement on the specific object.避免通过多个线程同时处理同一 object 的“标准”方法是在特定 object 上使用锁定语句 The lock is acquired on the object itself, such that lock(X) and lock(Y) are independent when ,ReferenceEquals(X,Y) .锁是在 object 本身上获取的,这样lock(X)lock(Y),ReferenceEquals(X,Y)时是独立的。

The lock statement acquires the mutual-exclusion lock for a given object, executes a statement block, and then releases the lock. lock 语句获取给定 object 的互斥锁,执行语句块,然后释放锁。 While a lock is held, the thread that holds the lock can again acquire and release the lock.持有锁时,持有锁的线程可以再次获取和释放锁。 Any other thread is blocked from acquiring the lock and waits until the lock is released .任何其他线程都被阻止获取锁并等待直到锁被释放

lock (objectBeingSaved) {
  // This code execution is mutually-exclusive over a specific object..
  // ..and independent (non-blocking) over different objects.
  Process(objectBeingSaved);
}

A local process lock does not necessarily translate into sufficient guarantees for databases access or when then the access spills across processes.本地进程锁不一定转化为对数据库访问的充分保证,或者当访问跨进程溢出时。 The scope of the lock should also be considered: eg.还应考虑锁的 scope:例如。 should it cover all processing, only saving, or some other work unit?它应该涵盖所有处理,仅保存还是其他一些工作单元?

To control what objects are being locked and reduce the chance of undesired/accidental lock interactions, it's sometimes recommend to add a field of the most specific visibility to the objects explicitly (and only for) the purpose of establishing a lock.为了控制哪些对象被锁定并减少不希望的/意外锁定交互的机会,有时建议显式(并且仅出于)建立锁定的目的向对象添加最具体的可见性字段。 This can also be used to group objects which should lock on each other, if such is a consideration.如果需要考虑的话,这也可以用于对应该相互锁定的对象进行分组。

It's also possible to use a locking pool, although such tends to be a more 'advanced' use-case with only specific applicability.也可以使用锁定池,尽管这往往是一个更“高级”的用例,只有特定的适用性。 Using pools also allows using semaphores (in even more specific use-cases) as well as a simple lock.使用池还允许使用信号量(在更具体的用例中)以及简单的锁。

If there needs to be a lock per external ID , one approach is to integrate the entities being worked on with a pool, establishing locks across entities:如果每个外部 ID都需要一个锁,一种方法是将正在处理的实体与池集成,在实体之间建立锁:

// Some lock pool. Variations of the strategy:
// - Weak-value hash table
// - Explicit acquire/release lock
// - Explicit acquire/release from ctor and finalizer (or Dispose)
var locks = CreateLockPool();
// When object is created, assign a lock object
var entity = CreateEntity();
// Returns same lock object (instance) for the given ID, and a different
// lock object (instance) for a different ID.
etity.Lock = GetLock(locks, entity.ID);

lock (entity.Lock) {
  // Mutually exclusive per whatever rules are to select the lock
  Process(entity);
}

Another variation is a localized pool, instead of carrying around a lock object per entity itself.另一个变体是本地化池,而不是每个实体本身都携带一个锁 object。 It is conceptually the same model as above, just flipped outside-in.它在概念上与上面的 model 相同,只是从外向内翻转。 Here is a gist.这是一个要点。 YMMV. YMMV。

private sealed class Locker { public int Count; }

IDictionary<int, Locker> _locks = new Dictionary<int, Locker>();

void WithLockOnId(int id, Action action) {
  Locker locker;
  lock (_locks) {
     // The _locks might have lots of contention; the work
     // done inside is expected to be FAST in comparison to action().
     if (!_locks.TryGetValue(id, out locker)
        locker = _locks[id] = new Locker();
     ++locker.Count;
  }
  lock (locker) {
     // Runs mutually-exclusive by ID, as established per creation of
     // distinct lock objects.
     action();
  }
  lock (_locks) {
     // Don't forget to take out the garbage..
     // This would be better with try/finally, which is left as an exercise
     // to the reader, along with fixing any other minor errors.
     if (--_locks[id].Count == 0)
       _locks.Remove(id);
  }
}

// And then..
WithLockOnId(x.ID, () => Process(x));

Taking a sideways step, another approach is to 'shard' entities across thread/processing units.另一种方法是跨线程/处理单元“分片”实体。 Thus each thread is guaranteed to never be processing the same entity as another thread: X,Y,Z always go to #1 and P,D,Q always to #2.因此,保证每个线程永远不会与另一个线程处理相同的实体:X,Y,Z 总是 go 到 #1 和 P,D,Q 总是到 #2。 (It's a little bit more complicated to optimize throughput..) (优化吞吐量有点复杂..)

var threadIndex = entity.ID % NumThreads;
QueueWorkOnThread(threadIndex, entity); // eg. add to List<ConcurrentQueue>

I would suggest using simple lock (if it is in one area of the code) As it would be processing different objects (meaning .net objects) but having the same value (as it is the same entity) I would rather go with some form of code for entities.我建议使用简单的锁(如果它在代码的一个区域中)因为它将处理不同的对象(意思是 .net 对象)但具有相同的值(因为它是同一个实体)我宁愿 go 具有某种形式实体的代码。 If the entity has some form of code I would use it - for example:如果实体有某种形式的代码,我会使用它 - 例如:

But of course, you have to watch out for deadlocks.但是,当然,您必须注意死锁。 And String.Intern is tricky, as it Interns the string for as long as application runs.并且 String.Intern 很棘手,因为只要应用程序运行,它就会对字符串进行实习。

lock(String.Intern(myEntity.Code))
{
   SaveToDatabaseA(myEntity);
   SaveToDatabaseB(myEntity);
}

But it looks like you want to have some kind of replication mechanism.但看起来你想要某种复制机制。 Then I would rather do it on database level (not on code level)然后我宁愿在数据库级别(而不是代码级别)

[UPDATE] [更新]

You updated the question with information, that it is being done on multiple servers.您用信息更新了问题,它正在多台服务器上完成。 And this information is kind a crucial here:) Normal lock wont work.这个信息在这里很重要:) 普通锁不起作用。

Of course, you can play with synchronizing the locks across different servers, but is like with distributed transactions.当然,你可以在不同的服务器上同步锁,但就像分布式事务一样。 Theoretically speaking you can do it, but most of the persons just avoid it as long as they can, and they play with the architecture of the solution to simplify the process.理论上你可以做到,但大多数人只是尽可能避免它,他们使用解决方案的架构来简化流程。

[UPDATE 2] [更新 2]

You may also find this interesting: Distributed locking in .NET您可能还会发现这很有趣: 分布式锁定在 .NET

:) :)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM