You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何ThreadSafeEnumerator实现不具备线程安全性?

问题分析:ThreadSafeEnumerator线程安全失效原因

定义的F#类型代码

type ThreadSafeEnumerator<'T>(inner: IEnumerator<'T>, readWriteLock: ReaderWriterLockSlim) =
    do readWriteLock.EnterReadLock()

    interface IEnumerator<'T> with
        member __.Current: 'T = inner.Current
        member __.Current: obj = inner.Current
        member __.Dispose() = 
            inner.Dispose()
            readWriteLock.ExitReadLock()
        member __.MoveNext() = inner.MoveNext()
        member __.Reset() = inner.Reset()

type ThreadSafeObject() = 
    let _lock = new ReaderWriterLockSlim()

    member __.Lock = _lock
    member __.EnterReadLock() = _lock.EnterReadLock()
    member __.EnterWriteLock() = _lock.EnterWriteLock()
    member __.ExitReadLock() = _lock.ExitReadLock()
    member __.ExitWriteLock() = _lock.ExitWriteLock()
    member __.EnterUpgradeableReadLock() = _lock.EnterUpgradeableReadLock()
    member __.ExitUpgradeableReadLock() = _lock.ExitUpgradeableReadLock()

    member this.SafeRead reader = this.EnterReadLock(); try reader() finally this.ExitReadLock()
    member this.SafeWrite writer = this.EnterWriteLock(); try writer() finally this.ExitWriteLock()

    member this.SafeReadWrite readerWriter = 
        this.EnterUpgradeableReadLock(); 
        try readerWriter (fun writer -> this.SafeWrite(writer)) 
        finally this.ExitUpgradeableReadLock()

    member this.Dispose() = 
        _lock.Dispose()
        GC.SuppressFinalize(this)

    override this.Finalize() = this.Dispose()

    interface IDisposable with
        member this.Dispose() = this.Dispose()

type ThreadSafeList<'v>([<Optional; DefaultParameterValue(10)>] initialCapacity: int) =
    inherit ThreadSafeObject()
    let _container = ResizeArray<'v>(initialCapacity)

    member this.Clear() = this.SafeWrite(fun() -> _container.Clear())
    member this.Count = this.SafeRead(fun() -> _container.Count)
    member this.Add(value: 'v) = this.SafeWrite(fun() -> _container.Add(value))
    member this.Contains(value: 'v) = this.SafeRead(fun() -> _container.Contains(value))
    member this.Remove(value: 'v) = this.SafeWrite(fun() -> _container.Remove(value) |> ignore)

    member this.Item
        with get(index: int) = this.SafeRead(fun() -> _container.[index])
        and set(index: int) (value: 'v) = this.SafeWrite(fun() -> _container.[index] <- value)
    
    member this.Trim(index: int) = this.SafeWrite(fun() -> _container.Trim(index))

    member this.Iter(action: 'v -> unit) = this.SafeRead(fun() -> _container |> Seq.iter action)
    member this.Map(action: 'v -> 'r) = this.SafeRead(fun() -> _container |> Seq.map action)

    member private this.GetEnumerator() = new ThreadSafeEnumerator<'v>(_container.GetEnumerator(), this.Lock)

    interface Collections.IEnumerable with
        member this.GetEnumerator() = this.GetEnumerator() :> Collections.IEnumerator

    interface IEnumerable<'v> with
        member this.GetEnumerator() = this.GetEnumerator()

问题现象

使用Iter方法遍历ThreadSafeList时线程安全,但通过枚举器(F#中隐式调用Seq.iter或C#中foreach)遍历时,抛出异常:

System.InvalidOperationException : Collection was modified; enumeration operation may not execute.

失效原因分析

1. ResizeArray内置枚举器的版本检查机制

ResizeArray<'v>对应C#的List<T>,它的内置枚举器会维护一个集合版本号:每次集合执行修改操作(Add/Remove/Clear等)时,版本号自动递增。枚举过程中,MoveNext或Current属性会检查当前集合的版本号是否与枚举器创建时的版本号一致,不一致则直接抛出上述异常。

你的ThreadSafeEnumerator虽然加了读锁,但枚举器的创建时机早于读锁的获取——在ThreadSafeList.GetEnumerator()中,先调用_container.GetEnumerator()拿到内置枚举器,之后才在ThreadSafeEnumerator的构造函数里进入读锁。这就导致枚举器创建时,集合可能正被其他线程修改,或者在创建枚举器到加锁的间隙被修改,版本号已经不匹配,后续即使持有读锁,遍历仍会触发版本检查失败。

2. 读锁的时机错误

ThreadSafeEnumerator的构造函数才执行EnterReadLock(),但内置枚举器是在构造函数调用前就已经创建完成。这段时间窗口内,其他线程可以执行写操作修改集合,导致枚举器的版本号与集合当前版本号脱节。等读锁加上后,虽然写操作被阻塞,但枚举器已经是基于旧状态创建的,遍历必然触发异常。

3. Iter方法为何正常

Iter方法是在SafeRead的读锁保护范围内,先获取读锁,再创建枚举器并完成整个遍历。全程持有读锁期间,集合不会被其他线程修改(读锁会阻塞写操作),内置枚举器的版本号与集合版本号始终一致,因此不会触发版本检查异常。

内容的提问来源于stack exchange,提问作者Franco Tiveron

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 04:09:54