我需要一个简单的 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
*/