【问题标题】:InvalidOperationException Collection was modified, but I did not modify it while iterating it?InvalidOperationException Collection 被修改了,但是我在迭代的时候没有修改?
【发布时间】:2016-08-07 13:51:30
【问题描述】:

我目前正在用 F# 编写一个小型服务器类型,它基本上存储了与它的活动 TCP 连接。在其循环中,它会检查新连接,并使用以下代码检查当前连接是否仍然存在:

type TCPListenerServer(discoveryPort:int) =
    let server = new TcpListener (IPAddress.Loopback, discoveryPort)

    let activeConnections = new List<TcpClient>()
    let cancellationToken = new CancellationTokenSource()

    let connectionIsStillActive (client:TcpClient) =
        let ipProperties = IPGlobalProperties.GetIPGlobalProperties ()
        let allTcpConnections = ipProperties.GetActiveTcpConnections ()
        let relevantTcpConnections = Array.filter (fun (connectionInfo:TcpConnectionInformation) -> 
            (connectionInfo.LocalEndPoint = (client.Client.LocalEndPoint :?> IPEndPoint)) && (connectionInfo.RemoteEndPoint = (client.Client.RemoteEndPoint :?> IPEndPoint))) allTcpConnections

        try
            let stateOfConnection = (Array.get relevantTcpConnections 0).State
            match stateOfConnection with
            | TcpState.Established ->
                true
            | _ ->
                false
        with
        | :? System.IndexOutOfRangeException as ex ->
            false

    let rec loop (pendingConnection:Task<TcpClient>) = async {            
        let newPendingConnection, client =
            match pendingConnection.Status with
            | TaskStatus.Created | TaskStatus.WaitingForActivation | TaskStatus.WaitingToRun 
            | TaskStatus.WaitingForChildrenToComplete | TaskStatus.Running  ->
                (None, None)
            | TaskStatus.Faulted ->
                let result = pendingConnection.Exception
                raise (new System.NotImplementedException())
            | TaskStatus.Canceled ->
                raise (new System.NotImplementedException())
            | TaskStatus.RanToCompletion ->
                let connectionTask = server.AcceptTcpClientAsync ()
                (Some connectionTask, Some pendingConnection.Result)
            | _ -> 
                raise (new System.NotImplementedException())

        // Add the new client to the list
        Option.iter (fun c -> activeConnections.Add c) client

        // Switch the new pending connection if there is one
        let connectionAttempt = defaultArg newPendingConnection pendingConnection

        // Check that the connections are still alive
        let connectionsToRemove = Seq.filter (fun (connection:TcpClient) -> not (connectionIsStillActive connection)) activeConnections
        Seq.iter (fun connection -> activeConnections.Remove connection |> ignore) connectionsToRemove  // <-- Exception happens here


        Async.Sleep 1000 |> Async.RunSynchronously
        return! loop connectionAttempt
    }

    member x.Start () =
        if not cancellationToken.Token.CanBeCanceled
        then
            raise (new System.Exception("Cancellation token cannot be used to cancel server loop task."))

        try
            server.Start ()
            let connectionTask = server.AcceptTcpClientAsync ()
            Async.Start (loop connectionTask, cancellationToken.Token)
        with 
        | :? SocketException as ex ->
            server.Stop ()
            raise ex

    member x.Stop () =
        cancellationToken.Cancel ()
        Async.Sleep 2000 |> Async.RunSynchronously
        server.Stop ()
        activeConnections.Clear ()

    member x.ActiveConnections =
        activeConnections

为了看看这是否有效,我实现了这个简单的单元测试:

[<TestMethod>]
[<TestCategory(Networking)>]
member x.``Start Server, Client Connects, then Disconnects`` () =
    let server = new TCPListenerServer(44000)
    server.Start ()

    let client = createClientAndConnect 44000

    Async.Sleep 5000 |> Async.RunSynchronously

    client.GetStream().Close()
    client.Close()

    Async.Sleep 5000 |> Async.RunSynchronously

    Assert.IsTrue(server.ActiveConnections.Count = 0, "There are still connections in the server's active connections list.")

    cleanupTest server (Some [client])

不幸的是,当我运行这个测试时,一旦我在客户端上调用了Close(),就会得到以下异常:

System.InvalidOperationException:集合已修改;枚举操作可能无法执行。 在 System.ThrowHelper.ThrowInvalidOperationException(ExceptionResource 资源) 在 System.Collections.Generic.List1.Enumerator.MoveNextRare() at System.Collections.Generic.List1.Enumerator.MoveNext() 在 Microsoft.FSharp.Collections.IEnumerator.next@224[T](FSharpFunc2 f, IEnumerator1 e, FSharpRef1 started, Unit unitVar0) at Microsoft.FSharp.Collections.IEnumerator.filter@219.System-Collections-IEnumerator-MoveNext() at Microsoft.FSharp.Collections.SeqModule.Iterate[T](FSharpFunc2 action, IEnumerable1 source) at TCPListenerServer.clo@35.Invoke(Unit unitVar) in E:\Documents\Source Control Projects\WiDroid\Core.Networking\TCPListenerServer.fs:line 61 at Microsoft.FSharp.Control.AsyncBuilderImpl.callA@851.Invoke(AsyncParams1 args)

我了解当您在迭代集合时修改集合时会发生此异常。但从我所见,我并没有直接在我的代码中这样做。我想知道为什么我会收到此异常?

【问题讨论】:

  • 我不知道 F#,但我猜是非活动套接字清理代码:let connectionsToRemove = Seq.filter (fun (connection:TcpClient) -&gt; not (connectionIsStillActive connection)) activeConnections Seq.iter (fun connection -&gt; activeConnections.Remove connection |&gt; ignore) connectionsToRemove。我认为迭代 connectionsToRemove 实际上是迭代 activeConnections,当您在迭代期间从 activeConnections 中删除一个项目时会导致问题。
  • @wablab 异常发生在那个地方:Seq.iter (fun connection -&gt; activeConnections.Remove connection |&gt; ignore) connectionsToRemove 确实如此。我看不出这两个列表是如何连接的,为什么迭代一个会迭代另一个?
  • 您可能需要考虑使用二进制搜索来将您的问题缩减为Minimal, Complete, and Verifiable example。通常,根据我的经验,仅仅这样做就可以让您自己解决问题。
  • 我相信connectionsToRemove 不是一个真实的对象列表;相反,它表示一个延迟执行的表达式,在您迭代它之前不会做任何事情,此时它开始迭代底层的activeConnections 列表。请参阅msdn.microsoft.com/en-us/visualfsharpdocs/conceptual/… 了解更多信息。

标签: .net exception collections f#


【解决方案1】:
let connectionsToRemove = Seq.filter (fun (connection:TcpClient) -> not (connectionIsStillActive connection)) activeConnections
Seq.iter (fun connection -> activeConnections.Remove connection |> ignore) connectionsToRemove

我认为问题出在此处,Seq.filter 返回一个 seq(IEnumerable&lt;T&gt; 的别名)。 它所代表的不是某种数据容器(如数组或列表),而是“将在某个时候给出项目的计算”。

这意味着当您执行Seq.iter 时,它必须“计算”connectionsToRemove 有效地迭代activeConnections。但是在同一个 Seq.iter 中,您要求从迭代集合中删除项目,因此会出现错误。

如果你熟悉 C#,这里的代码可以看一下,然后你就会清楚地看到问题

foreach (TcpClient connection in activeConnections)
{
    if (!connectionIsStillActive connection)
    {
        activeConnections.Remove (connection);
    }
}

您可以将其替换为致电 List&lt;T&gt;.RemoveAll

//doesn't seems to work directly (can't convert (TcpClient -> bool) to Predicate<TcpClient>)
//activeConnections.RemoveAll (not << connectionIsStillActive)

//this would work though
//activeConnections.RemoveAll (Predicate (not << connectionIsStillActive))

activeConnections.RemoveAll (fun connection -> not <| connectionIsStillActive connection)
|> ignore

【讨论】:

  • 很高兴知道!我没有看到那个潜在的逻辑。不过,您的解决方案给了我以下错误:This expression was expected to have type System.Predicate&lt;TcpClient&gt; but here has type bool。似乎无法弄清楚为什么?
  • 以下“长”的写法虽然有效:activeConnections.RemoveAll (fun connection -&gt; not (connectionIsStillActive connection)) |&gt; ignore
  • 哦,似乎 F# 函数不能直接转换为 .Net 委托(或者至少在“未明确定义”时不能)以及类似的东西 ^^ 你可以像你纠正的那样保持它,我会编辑答案
猜你喜欢
  • 1970-01-01
  • 2018-01-23
  • 1970-01-01
  • 2013-02-13
  • 1970-01-01
  • 2014-02-23
  • 1970-01-01
  • 2013-05-13
  • 2014-09-17
相关资源
最近更新 更多