为何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

