【问题标题】:Async Read Synchronization异步读取同步
【发布时间】:2014-10-11 01:42:03
【问题描述】:

我似乎找不到一个优雅的解决方案来同步下面的代码。我记得曾经做过类似的事情,但现在我的大脑一片空白。

我需要保护没有两个线程会从套接字读取,始终只有一个线程等待 BeginRead。当我以这种方式使用 Mutex 时,会出现 SyncronizationError。

class Program
{
    TcpClient client = new TcpClient();
    public readonly object loker = new object();

    public void BeginRead()
    {
        if (!Mutex.TryEnter(loker))
        {
            return;
        }
        client.GetStream().BeginRead(..., Read_Callback);
    }

    private void Read_Callback(IAsyncResult ar) 
    {
        client.GetStream().EndRead(ar);
        Mutex.Exit(loker);
        BeginRead();
    }


    static void Main(string[] args)
    {
        BeginRead();
    }
}

【问题讨论】:

  • 这将有助于了解哪个对象引发了异常,以及它在您的代码中被抛出的位置。
  • 您使用的是哪个 .NET 框架版本?
  • 我认为您不需要锁定机制。 BeginRead 和 CallBack 可以在不同的线程中运行,但不能同时运行。他们一个接一个地跑。

标签: c# multithreading asynchronous synchronization


【解决方案1】:

您需要的是为异步而不是同步的关键部分设计的同步方法。您希望能够在代码能够取出锁定之后异步运行一些代码,而不是让您的异步方法使用同步锁定机制。这些同步机制通常也期望锁从被取出的同一个线程中释放;对于没有意义的异步操作。 SemaphoreSlim 专门设计用于支持异步同步。

private TcpClient client = new TcpClient();
private SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);

public void BeginRead()
{
    semaphore.WaitAsync()
        .ContinueWith(t =>
            client.GetStream()
            .BeginRead(null, 0, 0, Read_Callback, null));
}

private void Read_Callback(IAsyncResult ar)
{
    client.GetStream().EndRead(ar);

    semaphore.Release();
    BeginRead();
}

您还可以选择使用 TPL 相当显着地重构此程序。

为此,您需要创建一个方法,该方法根据您感兴趣的异步操作生成Task,而不是另一种异步风格:

public static Task<int> WhenRead(
    this NetworkStream stream,
    byte[] buffer,
    int offset,
    int size)
{
    var tcs = new TaskCompletionSource<int>();
    stream.BeginRead(buffer, offset, size, result =>
    {
        tcs.TrySetResult(stream.EndRead(result));
    }, null);
    return tcs.Task;
}

但是一旦你有了这个程序,它就会变得更容易使用:

public async void BeginRead()
{
    while (true)
    {
        await semaphore.WaitAsync();
        int result = await client.GetStream().WhenRead(null, 0, 0);
        semaphore.Release();
        DoStuffWithResult(result);
    }
}

【讨论】:

  • 这确实是一个优雅的代码。不幸的是我不能使用它,因为我不想过多地动摇代码和我的经理
【解决方案2】:

另一个解决方案是使用 ReaderWriterLockSlim。它有 IsReadLockHeld 方法。

【讨论】:

    【解决方案3】:

    首先,您将Monitor 类与Mutex 类混合在一起。 Mutex 没有这些静态方法。

    现在,您正在经历的事实是,您在回调中使用了Monitor.Exit,这可能是从不同的线程调用的。这不起作用,因为必须从拥有锁对象的线程调用Monitor.Exit。

    相反,您可以使用没有此要求的其他锁定原语,例如 Semaphore:

    TcpClient client = new TcpClient();
    private static readonly Semaphore Semaphore = new Semaphore(1, 1);
    
    public void BeginRead()
    {
        if (!Semaphore.WaitOne(0))
        {
            return;
        }
        client.GetStream().BeginRead(..., Read_Callback);
    }
    
    private void Read_Callback(IAsyncResult ar)
    {
        client.GetStream().EndRead(ar);
        Semaphore.Release();
        BeginRead();
    }
    

    【讨论】:

    • 谢谢!这就是我一直在寻找的!
    • 很高兴我能帮上忙! :)
    • 这是从程序中删除所有异步,因为您在执行异步操作之前执行阻塞等待。
    • @Servy OP 要求一个可以处理他的情况的锁定原语。这不会从操作中删除异步,它会删除同时发出多个异步请求的选项。
    • @Servy 和 Yuval Itzchakov 你们都是对的。你看到的代码是我不允许重写的敏捷设计代码的一小部分。需要 Yuval 解决方案来修补糟糕设计中的错误,即使是糟糕的代码也是如此。
    【解决方案4】:

    不能在一个线程上获取Mutex 并在另一个线程上释放。它必须由获取它的线程释放。互斥锁所有者是一个线程。

    我强烈建议不要做任何你试图做的事情(在异步调用期间保持锁定)。一般模式是在发布调用和完成期间锁定:

    lock(something) {
       ... 
       BeginIO(...)
    }
    
    
    completion:
    
       result = endIO;
       lock(something)
       {
         ...
       }
    

    【讨论】:

    • 是的,我知道 Mutex 的所有者是一个线程。我需要其他可以完成这项工作的课程。
    • 在您的示例中,可能会有 2 个线程在同一个套接字中等待,我的目标是避免这种情况。
    • 在更高级别同步。我不知道你的程序做了什么,但不要同步 socket。套接字完成获取位、组装帧、分派接收到的数据。调度通常是进行同步的理想场所。线程在套接字上“等待”的概念是错误的。这意味着您错误地使用了异步 IO。反应灵敏,事件驱动。
    猜你喜欢
    • 2011-12-27
    • 2017-01-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-08-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多