异步代码中的标志,循环和锁定

时间:2013-08-23 07:12:31

标签: c# locking async-await

我要做的是创建一个“倾听者”'它一次监听几个不同的Tcp端口,并将消息传递给任何观察者。

伪代码:

private bool _Listen = false;
public void Start()
{
    _Listen = true;
    Task.Factory.StartNew(() => Listen(1);
    Task.Factory.StartNew(() => Listen(2);
}

public void Stop()
{
    _Listen = false;
}

private async void Listen(int port)
{
     var tcp = new TcpClient();
     while(_Listen)
     {
          await tcp.ConnectAsync(ip, port);
          using (/*networkStream, BinaryReader, etc*/)
          {
               while(_Listen)
               {
                   //Read from binary reader and OnNext to IObservable
               }
          }
     }
}

(为简洁起见,我省略了两个while内的try / catch,两者都检查了标志)

我的问题是:我是否应该锁定标志,如果是,那么该如何与async / await位绑定?

4 个答案:

答案 0 :(得分:4)

首先,您应该将返回类型更改为Task,而不是void。 async void方法基本上是即发即忘,无法等待或取消。它们的存在主要是为了允许创建异步事件处理程序或类似事件的代码。它们永远不应该用于正常的异步操作。

协作取消/中止/停止异步操作的TPL方法是使用CancellationToken。您可以检查令牌的IsCancellationRequested属性,看看是否需要取消操作并停止。

更好的是,框架提供的大多数异步方法都接受CancellationToken,因此您可以立即停止它们而无需等待它们返回。您可以使用NetworkStream的ReadAsync(Byte[], Int32, Int32, CancellationToken)来读取数据,并在有人调用您的Stop方法时立即取消。

您可以将代码更改为以下内容:

    CancellationTokenSource _source;

    public void Start()
    {
        _source = new CancellationTokenSource();            
        Task.Factory.StartNew(() => Listen(1, _source.Token),_source.Token);
        Task.Factory.StartNew(() => Listen(2, _source.Token), _source.Token);
    }

    public void Stop()
    {
        _source.Cancel();
    }


    private async Task Listen(int port,CancellationToken token)
    {
        var tcp = new TcpClient();
        while(!token.IsCancellationRequested)
        {
            await tcp.ConnectAsync(ip, port);
            using (var stream=tcp.GetStream())
            {
                ...
                try
                {
                    await stream.ReadAsync(buffer, offset, count, token);
                }
                catch (OperationCanceledException ex)
                {
                    //Handle Cancellation
                }
                ...
            }
        }
    }

您可以在Cancellation in Managed Threads中阅读更多关于取消的内容,包括如何轮询,注册取消以取消,收听多个令牌等建议。

存在try/catch块,因为如果取消任务,await会抛出异常。您可以通过在ReadAsync返回的Task上调用ContinueWith并检查IsCanceled标志来避免这种情况:

    private async Task Listen(int port,CancellationToken token)
    {
        var tcp = new TcpClient();
        while(!token.IsCancellationRequested)
        {
            await tcp.ConnectAsync(ip, port);
            using (var stream=tcp.GetStream())
            {
                ///...
                await stream.ReadAsync(buffer, offset, count, token)
                    .ContinueWith(t =>
                    {
                        if (t.IsCanceled)
                        {
                            //Do some cleanup?
                        }
                        else
                        {
                            //Process the buffer and send notifications
                        }
                    });
                ///...
            }
        }
    }

await现在等待一个简单的Task,在继续完成时结束

答案 1 :(得分:1)

当您可能处理多个线程时,某种形式的事件(例如ManualResetEventSlim)将是一个更明显的选择。

private ManualResetEventSlim _Listen;
public void Start()
{
    _Listen = new ManualResetEventSlim(true);
    Task.Factory.StartNew(() => Listen(1);
    Task.Factory.StartNew(() => Listen(2);
}

public void Stop()
{
    _Listen.Reset();
}

private async void Listen(int port)
{
     var tcp = new TcpClient();
     while(_Listen.IsSet)
     {

答案 2 :(得分:1)

你可能会更好地坚持使用RX而不是使用Task。这是我为连接到带有RX的UDP套接字而编写的一些代码。

public IObservable<UdpReceiveResult> StreamObserver
(int localPort, TimeSpan? timeout = null)
{


    return Linq.Observable.Create<UdpReceiveResult>(observer =>
    {
        UdpClient client = new UdpClient(localPort);

        var o = Linq.Observable.Defer(() => client.ReceiveAsync().ToObservable());
        IDisposable subscription = null;
        if ((timeout != null)) {
            subscription = Linq.Observable.Timeout(o.Repeat(), timeout.Value).Subscribe(observer);
        } else {
            subscription = o.Repeat().Subscribe(observer);
        }

        return Disposable.Create(() =>
        {
            client.Close();
            subscription.Dispose();
            // Seems to take some time to close a socket so
            // when we resubscribe there is an error. I
            // really do NOT like this hack. TODO see if
            // this can be improved
            Thread.Sleep(TimeSpan.FromMilliseconds(200));
        });
    });
}

答案 3 :(得分:1)

  

我应该锁定标志,如果是,那与async / await位的搭配方式是什么?

您需要以某种方式同步对标志的访问。如果不这样做,则允许编译器进行以下优化:

bool compilerGeneratedLocal = _Listen;
while (compilerGeneratedLocal)
{
    // body of the loop
}

哪会使你的代码出错。

一些选项如何解决这个问题:

  1. 标记bool标记volatile。这将确保始终读取标志的当前值。
  2. 使用CancellationToken(由Panagiotis Kanavos建议)。这将确保以线程安全的方式访问基础标志。它还有许多异步方法支持CancellationToken的优点,因此您也可以取消它们。