【问题标题】:Using a Monitor lock in async or sync operations [duplicate]在异步或同步操作中使用监视器锁 [重复]
【发布时间】:2019-05-20 17:54:43
【问题描述】:

我正在尝试提供一种锁定全局资源的解决方案,以强制锁定访问并且可用于同步或异步操作。

我创建了一个通用类State:

public class State<T>
{
  public class LockedState : IDisposable {
    public LockedState(State<T> state) { this.state = state; Monitor.Enter(this.state.theLock); } // would be great if this could be private
    public void Dispose() => Monitor.Exit(this.state.theLock);
    public ref T Val { get { return ref state.t; } }

    readonly State<T> state;
  };

  public State() { }
  public State(T t) { this.t = t; }
  public LockedState Locked { get { return new LockedState(this); } }
  readonly object theLock = new object();
  T t;
}

这里的想法是我可以在 Program 类中拥有一个全局“存储”:

public class Program {
    static public readonly State<ImmutableList<int>> TheList = new State<ImmutableList<int>>(ImmutableList<int>.Empty);
    static public readonly State<SomeType> SomeType = new State<SomeType>(SomeType());
}

然后唯一访问状态的方法是像这样获取锁:

using (var lTheList = Program.TheList.Locked) {
   lTheList.Val = lTheList.Val.Add(5));
}

在我的情况下,它比普通锁效果更好,因为它在获取/设置之前强制锁定(你不能忘记锁定)

(旁注这是一个好策略吗?)

问题是我不能在异步代码中使用上述内容:

using (var someType = Program.SomeType.Lock()) {
   var x = await someType.Val.SomeAsyncOp();
}

我得到一个例外:Object synchronization method was called from an unsynchronized block of code

我发现这篇帖子 How to protect resources that may be used in a multi-threaded or async environment? 有一个 AsyncLock 类但是我不明白如何将像 AsyncLock 这样的类放入我的 StateLock 类中......

State.Lock() 能否返回可用于同步和异步调用者的锁定状态?这真的是我要找的!

如果不是,我最好的前进方式是什么?使用SemaphoreSlim 并拥有State.Lock()State.AsyncLock()

谢谢!

【问题讨论】:

  • 为什么你不使用lock-statement?见docs.microsoft.com/en-us/dotnet/csharp/language-reference/…
  • 不完全确定 using 语句(因此 try/finally 阻止它)是否阻止了这一点,但我认为问题如下:在等待之前执行代码的线程(获取锁)可以是不同的线程,然后线程在等待(释放锁)之后执行代码。由于 Monitor 是可重入的,我认为它会跟踪哪个线程拥有锁,我可以想象当另一个线程然后拥有的线程尝试释放 Monitor 时会引发此错误。
  • knoop 是的,这就是问题所在......我怎样才能最好地解决它?
  • 抱歉,您还不清楚为什么会发生此错误。正如您链接信号量的线程中提到的那样,这似乎是要走的路:stackoverflow.com/questions/11001760/…

标签: c# .net async-await c#-7.0


【解决方案1】:

您的代码令人担忧,因为任何恶意程序员都可以在 Program.TheList.Lock() 上使用 lock 而无需调用 .Dispose(),从而导致任何未来的调用者卡在构造函数中。构造函数应始终快速且干净地执行以避免损坏运行时。

你的代码有各种代码味道。

最好将 lambdas 传递到您的代码中并锁定它。

试试这个代码:

public class State<T>
{
    public State() { }
    public State(T t) { this.t = t; }

    private T t = default(T);
    private readonly object theLock = new object();

    public void With(Action<T> action)
    {
        lock (this.theLock)
        {
            action(this.t);
        }
    }

    public void Update(Func<T, T> action)
    {
        lock (this.theLock)
        {
            this.t = action(this.t);
        }
    }

    public R Select<R>(Func<T, R> action)
    {
        lock (this.theLock)
        {
            return action(this.t);
        }
    }
}

现在,连同您的 Program 课程,您可以这样做:

Program.TheList.Update(t => t.Add(5));

代码更少,更简单。

你也可以这样做:

async void Main()
{
    var wc = new State<WebClient>(new WebClient());

    string source = wc.Select<string>(x => x.DownloadString(new Uri("http://www.microsoft.com")));

    Console.WriteLine(source);
}

我希望这会有所帮助。

【讨论】:

  • async 的上下文中,这不是真的或没有帮助;例如,您的 SelectAsync 没有 await (您不能使用,有充分的理由):不会锁定正确的时间间隔 - 它只会保持锁定直到第一个不完整的等待操作.
  • '更少的代码和更简单'在上述情况下是的,但当使用(或你的'with')内部的代码做很多它自己对状态的访问时,你会结束加上一个非常深的 lambda 嵌套 ...
  • @kofifus - 这是 LINQ 采用的方法,但最终不会导致深度 lambda 嵌套。
  • @MarcGravell - 对不起,我用异步代码过度了。我已经删除了。
  • 我不明白 Linq 与它有什么关系.. 使用您的方法,您必须非常小心地使用或更新您没有使用过时的值,而 IMO 更是如此比忘记锁定更容易出错..反正
【解决方案2】:

您不能在此处使用Monitor,因为Monitor(又名lock)本质上是基于线程的,并且要求您从获取的同一线程中“退出”锁锁 - 这正是您在async 代码中很少期望的情况。相反,请考虑SemaphoreSlim,您可以从中获取和释放令牌,但它不是线程绑定的。

伪代码:

// usually a field somewhere
SemaphoreSlim obj = new SemaphoreSlim(1);

// acquire lock:
obj.Wait(); // or await obj.WaitAsync() if want async acquire

// release lock:
obj.Release(); // often in a "finally"

【讨论】:

  • 好的,这是否意味着 SemaphoreSlim 不可重入?这会让我无法使用...
  • @kofifus 我试过说“试试ReaderWriterLockSlim - 它有递归支持”,但考虑到这也可能是线程绑定的
  • 好的,所以在同一个线程和来自该线程的所有异步操作中没有可重入的异步锁?
  • @kofifus 不是 AFAIK,不;你可能需要使用异步调用内容来设计一些东西,但是......这并不有趣;消除重新进入的需要会更容易
  • @kofifus 好吧,如果您认为监视器使用密钥(在这种情况下,密钥始终是拥有锁的线程的 ID),那么应该有可能在您的位置创建一些东西可以传递钥匙,如果您使用匹配的钥匙调用锁,您将能够重新输入它。这样,您就不必依赖程序员检查他们是否已经拥有锁,但这意味着您在使用此锁的代码中到处传递密钥。所以这当然应该是可能的,问题是它是否可取:-)
猜你喜欢
  • 2013-12-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-11-28
  • 1970-01-01
  • 2023-03-16
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多