首页 > 解决方案 > 通过 System.Text.Json.Serialization 将对象序列化为文件似乎会破坏 ReaderWriterLockSlim

问题描述

我需要一个简单的 List<> 在异步环境中使用并且是并发的。该列表确实只有一堆方法,而不是 IList 的完整实现。我已经使用 ReaderWriterLockSlim 类来启用锁定范例并且它运行良好。

最近在进行单元测试时,我收到了一个锁定错误:System.Threading.LockRecursionException:“在此模式下持有的写锁可能无法获取读锁。” 并且属性 IsWriteLockHeld 在异常点为真。这根本不是预期的,代码似乎很好。

因此,我启动了一个小型控制台测试程序,发现使用 System.Text.Json.Serialization 命名空间将我的列表序列化为文件会导致问题。如果我评论了将列表序列化为文件的调用,那么一切都会按预期工作。

单步执行代码时,程序似乎在 Save() 之后的下一个读/写锁上死锁,并且即使属性 IsReadLockHeld 和 IsWriteLockHeld 为假也不会返回。

示例代码中没有抛出异常。

我将不胜感激任何意见或建议。我不知所措。这是信号量或序列化程序中的缺陷,还是我彻底搞砸了我的代码。

带有注释的示例代码如下。

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;

namespace example {

    public static class Serialize {

        public static async Task ToFileAsync<T>(string path, object value) {
            var options = new JsonSerializerOptions(JsonSerializerDefaults.General) {
                WriteIndented = true,
                Converters = { new JsonStringEnumConverter() }
            };

            using (FileStream fs = File.Open(path, FileMode.Create)) {
                await JsonSerializer.SerializeAsync<T>(fs, (T)value, options);
            }
        }

        public static async Task<T> FromFileAsync<T>(string path) where T : new() {
            if (File.Exists(path)) {
                var options = new JsonSerializerOptions(JsonSerializerDefaults.General) {
                    ReadCommentHandling = JsonCommentHandling.Skip,
                    Converters = { new JsonStringEnumConverter() }
                };

                using (FileStream fs = File.Open(path, FileMode.Open)) {
                    return await JsonSerializer.DeserializeAsync<T>(fs, options);
                }
            }

            return new T();
        }
    }

    public class ListCollection<T> {

        private List<T> list;
        private readonly ReaderWriterLockSlim readerWriterLockSlim;
        private bool disposed;

        public ListCollection() {
            this.readerWriterLockSlim = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
            this.list = new List<T>();
        }

        public bool Contains(T value) {
            bool result = false;

            try {
                Console.WriteLine($"Contains, Before EnterReadLock");
                Console.WriteLine($"Contains, IsReadLockHeld: {this.readerWriterLockSlim.IsReadLockHeld}");
                Console.WriteLine($"Contains, IsWriteLockHeld: {this.readerWriterLockSlim.IsWriteLockHeld}");
                
                this.readerWriterLockSlim.EnterReadLock();

                // program appears to have hung (deadlock ?) up at the point of EnterReadLock()
                // even though both IsReadLockHeld and IsWriteLockHeld are false
                // Commenting the await Serialize.ToFileAsync(...) in Save() allows this to run as expected

                // using a TryEnterReadLock fails even though both IsReadLockHeld
                // and IsWriteLockHeld are false;
                // bool locked = this.readerWriterLockSlim.TryEnterReadLock(500);

                Console.WriteLine($"Contains, After EnterReadLock");
                Console.WriteLine($"Contains, IsReadLockHeld: {this.readerWriterLockSlim.IsReadLockHeld}");
                Console.WriteLine($"Contains, IsWriteLockHeld: {this.readerWriterLockSlim.IsWriteLockHeld}");

                result = this.list.Contains(value);
            }
            catch (Exception e) {
                Console.WriteLine(e.Message);
            }
            finally {
                Console.WriteLine($"Contains, Finally");
                Console.WriteLine($"Contains, IsReadLockHeld: {this.readerWriterLockSlim.IsReadLockHeld}");
                Console.WriteLine($"Contains, IsWriteLockHeld: {this.readerWriterLockSlim.IsReadLockHeld}");

                if (this.readerWriterLockSlim.IsReadLockHeld) {
                    this.readerWriterLockSlim.ExitReadLock();
                }
            }

            return result;
        }

        public bool Add(T value) {
            bool result = false;

            try {
                Console.WriteLine($"Add, Before EnterWriteLock");
                Console.WriteLine($"Add, IsReadLockHeld: {this.readerWriterLockSlim.IsReadLockHeld}");
                Console.WriteLine($"Add, IsWriteLockHeld: {this.readerWriterLockSlim.IsWriteLockHeld}");

                this.readerWriterLockSlim.EnterWriteLock();

                Console.WriteLine($"Add, After EnterWriteLock");
                Console.WriteLine($"Add, IsReadLockHeld: {this.readerWriterLockSlim.IsReadLockHeld}");
                Console.WriteLine($"Add, IsWriteLockHeld: {this.readerWriterLockSlim.IsWriteLockHeld}");

                if (value != null) {
                    if (!this.list.Contains(value)) {
                        this.list.Add(value);
                        result = true;
                    }
                }
            }
            catch (Exception e) {
                Console.WriteLine(e.Message);
            }
            finally {
                Console.WriteLine($"Add, Finally");
                Console.WriteLine($"Add, IsReadLockHeld: {this.readerWriterLockSlim.IsReadLockHeld}");
                Console.WriteLine($"Add, IsWriteLockHeld: {this.readerWriterLockSlim.IsReadLockHeld}");

                if (this.readerWriterLockSlim.IsWriteLockHeld) {
                    this.readerWriterLockSlim.ExitWriteLock();
                }
            }

            return result;
        }

        public bool Remove(T value) {
            bool result = false;

            try {
                Console.WriteLine($"Remove, Before EnterWriteLock");
                Console.WriteLine($"Remove, IsReadLockHeld: {this.readerWriterLockSlim.IsReadLockHeld}");
                Console.WriteLine($"Remove, IsWriteLockHeld: {this.readerWriterLockSlim.IsWriteLockHeld}");

                this.readerWriterLockSlim.EnterWriteLock();

                Console.WriteLine($"Remove, After EnterWriteLock");
                Console.WriteLine($"Remove, IsReadLockHeld: {this.readerWriterLockSlim.IsReadLockHeld}");
                Console.WriteLine($"Remove, IsWriteLockHeld: {this.readerWriterLockSlim.IsWriteLockHeld}");

                if (value != null) {
                    if (this.list.Contains(value)) {
                        this.list.Remove(value);
                        result = true;
                    }
                }
            }
            catch (Exception e) {
                Console.WriteLine(e.Message);
            }
            finally {
                Console.WriteLine($"Add, Finally");
                Console.WriteLine($"Add, IsReadLockHeld: {this.readerWriterLockSlim.IsReadLockHeld}");
                Console.WriteLine($"Add, IsWriteLockHeld: {this.readerWriterLockSlim.IsReadLockHeld}");

                if (this.readerWriterLockSlim.IsWriteLockHeld) {
                    this.readerWriterLockSlim.ExitWriteLock();
                }
            }

            return result;
        }

        public async Task Save(string path) {
            try {
                Console.WriteLine($"Save, Before EnterWriteLock");
                Console.WriteLine($"Save, IsReadLockHeld: {this.readerWriterLockSlim.IsReadLockHeld}");
                Console.WriteLine($"Save, IsWriteLockHeld: {this.readerWriterLockSlim.IsWriteLockHeld}");

                this.readerWriterLockSlim.EnterWriteLock();

                Console.WriteLine($"Save, After EnterWriteLock");
                Console.WriteLine($"Save, IsReadLockHeld: {this.readerWriterLockSlim.IsReadLockHeld}");
                Console.WriteLine($"Save, IsWriteLockHeld: {this.readerWriterLockSlim.IsWriteLockHeld}");

                await Serialize.ToFileAsync<List<T>>(path, this.list).ConfigureAwait(false);

                Console.WriteLine($"Save, After Serialization");
                Console.WriteLine($"Save, IsReadLockHeld: {this.readerWriterLockSlim.IsReadLockHeld}");
                Console.WriteLine($"Save, IsWriteLockHeld: {this.readerWriterLockSlim.IsWriteLockHeld}");

                // I expected the IsIsWriteLockHeld to be true however it's false!
                // Commenting the await Serialize.ToFileAsync(...) above leaves IsWriteLockHeld as true
            }
            catch (Exception e) {
                Console.WriteLine(e.Message);
            }
            finally {
                Console.WriteLine($"Save, Finally");
                Console.WriteLine($"Save, IsReadLockHeld: {this.readerWriterLockSlim.IsReadLockHeld}");
                Console.WriteLine($"Save, IsWriteLockHeld: {this.readerWriterLockSlim.IsReadLockHeld}");
                
                if (this.readerWriterLockSlim.IsWriteLockHeld) {
                    this.readerWriterLockSlim.ExitWriteLock();
                }
            }

            // stop the warnings if no await's
            await Task.CompletedTask;
        }

        public async Task Load(string path) {
            try {
                Console.WriteLine($"Load, Before EnterWriteLock");
                Console.WriteLine($"Load, IsReadLockHeld: {this.readerWriterLockSlim.IsReadLockHeld}");
                Console.WriteLine($"Load, IsWriteLockHeld: {this.readerWriterLockSlim.IsWriteLockHeld}");

                this.readerWriterLockSlim.EnterWriteLock();

                Console.WriteLine($"Load, After EnterWriteLock");
                Console.WriteLine($"Load, IsReadLockHeld: {this.readerWriterLockSlim.IsReadLockHeld}");
                Console.WriteLine($"Load, IsWriteLockHeld: {this.readerWriterLockSlim.IsWriteLockHeld}");

                this.list = await Serialize.FromFileAsync<List<T>>(path).ConfigureAwait(false);
            }
            catch (Exception e) {
                Console.WriteLine(e.Message);
            }
            finally {
                Console.WriteLine($"Load, Finally");
                Console.WriteLine($"Load, IsReadLockHeld: {this.readerWriterLockSlim.IsReadLockHeld}");
                Console.WriteLine($"Load, IsWriteLockHeld: {this.readerWriterLockSlim.IsReadLockHeld}");

                if (this.readerWriterLockSlim.IsWriteLockHeld) {
                    this.readerWriterLockSlim.ExitWriteLock();
                }
            }
        }

        protected virtual void Dispose(bool disposing) {
            if (!this.disposed) {
                if (disposing) {
                    this.readerWriterLockSlim.Dispose();
                }
                this.disposed = true;
            }
        }

        public void Dispose() {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

    }

    public class Program {
        static async Task Main() {
            var lc = new ListCollection<string>();

            lc.Add("Item 1");

            await lc.Save("example.json");

            bool result = lc.Contains("Item 1");

            Console.WriteLine();
            Console.WriteLine("Finished, hit <enter> to quit");
            Console.WriteLine();
            Console.ReadLine();
        }

    }
}

/*
 Program output fom Save() method:

 Save, Before EnterWriteLock
 Save, IsReadLockHeld: False
 Save, IsWriteLockHeld: False
 Save, After EnterWriteLock
 Save, IsReadLockHeld: False
 Save, IsWriteLockHeld: True  < expected result
 Save, After Serialization
 Save, IsReadLockHeld: False
 Save, IsWriteLockHeld: False < unexpected result
 Save, Finally
 Save, IsReadLockHeld: False
 Save, IsWriteLockHeld: False
  
*/

标签: c#listasynchronousconcurrencyreaderwriterlockslim

解决方案


推荐阅读