C# 异步代码中的标志、循环和锁

C# 异步代码中的标志、循环和锁,c#,locking,async-await,C#,Locking,Async Await,我试图做的是创建一个“侦听器”,它可以同时侦听多个不同的Tcp端口,并将消息通过管道发送给任何观察者 伪ish代码: private bool _Listen = false; public void Start() { _Listen = true; Task.Factory.StartNew(() => Listen(1); Task.Factory.StartNew(() => Listen(2); } public void Stop() {

我试图做的是创建一个“侦听器”,它可以同时侦听多个不同的Tcp端口,并将消息通过管道发送给任何观察者

伪ish代码:

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,这两个while也都检查了标志)

我的问题是:我应该锁定标志吗?如果是的话,如何与异步/等待位结合?

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

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)
     {

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

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

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

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

    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
                }
                ...
            }
        }
    }
您可以在中阅读更多有关取消的信息,包括有关如何轮询、注册取消回调、侦听多个令牌等的建议

try/catch
块存在,因为任务取消时
wait
会引发异常。您可以通过对ReadAsync返回的任务调用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
现在等待一个简单的
任务
,该任务在续集完成时完成

您可能最好始终坚持使用RX,而不是使用任务。下面是我为使用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));
        });
    });
}
public IObservable StreamObserver
(int localPort,TimeSpan?timeout=null)
{
返回Linq.observate.Create(observator=>
{
UdpClient客户端=新的UdpClient(本地端口);
var o=Linq.Observable.Defer(()=>client.ReceiveAsync().ToObservable());
IDisposable订阅=null;
如果((超时!=null)){
subscription=Linq.Observable.Timeout(o.Repeat(),Timeout.Value);
}否则{
订阅=o.Repeat().Subscribe(观察者);
}
返回一次性。创建(()=>
{
client.Close();
subscription.Dispose();
//似乎需要一些时间来关闭插座,所以
//当我们重新订阅时,出现了一个错误
//真的不喜欢这个黑客。看看是否
//这是可以改进的
睡眠(时间跨度从毫秒(200));
});
});
}
我应该锁定标志吗?如果是的话,如何与异步/等待位结合

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

bool compilerGeneratedLocal = _Listen;
while (compilerGeneratedLocal)
{
    // body of the loop
}
这会使你的代码出错

以下是一些解决方法:

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

  • Slim似乎适用于短期事件:如果要在“人类”时间尺度上启动/停止,我应该使用ManualResetEvent吗?@Benjol-不,我会使用Slim。它们之间的主要区别发生在事件未发出信号时,但在上述情况下,事件大部分时间都发出信号。这比
    volatile bool
    好多少?有趣的是,此代码的早期版本使用UDP,我一直使用Rx。如果我这样做,我甚至可以停止使用
    Subject
    。我发现我很少在代码中直接使用Task。任务只是RX的一种退化形式,您只需要返回一个值。通常更容易记住事物的RX版本。例如,等待200毫秒<代码>等待Objistabel.Phimes(TimeSIP.FROM毫秒(200))。(1)< /代码>我确信有一个直接的基于任务的版本,但是我记不起来了。HM,我只是给它一个Go,但是它在中间有点毛茸茸的(嵌套可观察的,使用和尝试捕捉):(张贴你的尝试。有人会帮助:)我会再做一点,然后把它发布到CodeReview上,可能是下周。是的,我会记住它的。也许我对取消代币有不公平的偏见,不知道,感觉很笨重。而我的
    开始
    几乎是“火与忘”…令牌怎么会比标志或事件更糟糕?(更不用说令牌是专门构建的,并且已经在整个框架中使用)。实际上,源代码/令牌设计将取消的发送方与接收方分离“
    async void
    methods cannot cancel”这不是真的,您可以取消
    async void
    method。只是调用方无法知道是否以及何时实际发生了取消。此外,虽然您通常认为应该避免使用
    async void
    方法是正确的,但不管怎样,忽略结果
    Task
    也没有多大区别。(唯一的区别是,
    async void
    中未捕获的异常将使应用程序崩溃,但如果忽略
    任务
    ,它们在
    异步任务
    中不会做任何事情。这使得
    异步任务
    实际上是更糟糕的选择。)@svick,Panagiotis。好吧,好吧,我试试看:)我想我不喜欢的是这个