Muz
Muz

Reputation: 349

Serializing an object to a file via System.Text.Json.Serialization appears to break ReaderWriterLockSlim

I have a requirement for a simple List<> to be used in an async environment and be concurrent. The list does only has a hand full of methods and is not a full implementation of IList. I have used the ReaderWriterLockSlim class to enable a locking paradigm and it was working well.

Recently whilst unit testing I received a locking error: System.Threading.LockRecursionException: 'A read lock may not be acquired with the write lock held in this mode.' and the property IsWriteLockHeld was true at the point of the exception. This was not expected at all and the code appeared to be fine.

So, I whipped up a small console test program and discovered that serializing my list to a file using System.Text.Json.Serialization namespace was causing an issue. If I commented the call to serialize the list to file everything works as expected.

Stepping through the code the program appears to deadlock on the next read/write lock after Save() and does not return even though the properties IsReadLockHeld and IsWriteLockHeld are false.

No exceptions are thrown in the example code.

I would appreciate any comments or suggestions. I'm at a loss. Is this a defect in the semaphore or serializer or have I royally messed up my code.

Sample code with comments follows.

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
  
*/

Upvotes: 0

Views: 34

Answers (0)

Related Questions