Pergunta

Eu tenho jogado com coleções e threading e deparei com os métodos de extensão bacana as pessoas têm criado para facilitar o uso de ReaderWriterLockSlim, permitindo que o padrão IDisposable.

No entanto, eu acredito que eu vim a perceber que algo na implementação é um assassino de desempenho. Eu percebo que os métodos de extensão não é suposto desempenho muito impacto, por isso estou deixou assumindo que algo na implementação é a causa ... a quantidade de descartáveis ??estruturas criadas / coletadas?

Aqui está um código de teste:

using System;
using System.Collections.Generic;
using System.Threading;
using System.Diagnostics;

namespace LockPlay {

    static class RWLSExtension {
        struct Disposable : IDisposable {
            readonly Action _action;
            public Disposable(Action action) {
                _action = action;
            }
            public void Dispose() {
                _action();
            }
        } // end struct
        public static IDisposable ReadLock(this ReaderWriterLockSlim rwls) {
            rwls.EnterReadLock();
            return new Disposable(rwls.ExitReadLock);
        }
        public static IDisposable UpgradableReadLock(this ReaderWriterLockSlim rwls) {
            rwls.EnterUpgradeableReadLock();
            return new Disposable(rwls.ExitUpgradeableReadLock);
        }
        public static IDisposable WriteLock(this ReaderWriterLockSlim rwls) {
            rwls.EnterWriteLock();
            return new Disposable(rwls.ExitWriteLock);
        }
    } // end class

    class Program {

        class MonitorList<T> : List<T>, IList<T> {
            object _syncLock = new object();
            public MonitorList(IEnumerable<T> collection) : base(collection) { }
            T IList<T>.this[int index] {
                get {
                    lock(_syncLock)
                        return base[index];
                }
                set {
                    lock(_syncLock)
                        base[index] = value;
                }
            }
        } // end class

        class RWLSList<T> : List<T>, IList<T> {
            ReaderWriterLockSlim _rwls = new ReaderWriterLockSlim();
            public RWLSList(IEnumerable<T> collection) : base(collection) { }
            T IList<T>.this[int index] {
                get {
                    try {
                        _rwls.EnterReadLock();
                        return base[index];
                    } finally {
                        _rwls.ExitReadLock();
                    }
                }
                set {
                    try {
                        _rwls.EnterWriteLock();
                        base[index] = value;
                    } finally {
                        _rwls.ExitWriteLock();
                    }
                }
            }
        } // end class

        class RWLSExtList<T> : List<T>, IList<T> {
            ReaderWriterLockSlim _rwls = new ReaderWriterLockSlim();
            public RWLSExtList(IEnumerable<T> collection) : base(collection) { }
            T IList<T>.this[int index] {
                get {
                    using(_rwls.ReadLock())
                        return base[index];
                }
                set {
                    using(_rwls.WriteLock())
                        base[index] = value;
                }
            }
        } // end class

        static void Main(string[] args) {
            const int ITERATIONS = 100;
            const int WORK = 10000;
            const int WRITE_THREADS = 4;
            const int READ_THREADS = WRITE_THREADS * 3;

            // create data - first List is for comparison only... not thread safe
            int[] copy = new int[WORK];
            IList<int>[] l = { new List<int>(copy), new MonitorList<int>(copy), new RWLSList<int>(copy), new RWLSExtList<int>(copy) };

            // test each list
            Thread[] writeThreads = new Thread[WRITE_THREADS];
            Thread[] readThreads = new Thread[READ_THREADS];
            foreach(var list in l) {
                Stopwatch sw = Stopwatch.StartNew();
                for(int k=0; k < ITERATIONS; k++) {
                    for(int i = 0; i < writeThreads.Length; i++) {
                        writeThreads[i] = new Thread(p => {
                            IList<int> il = p as IList<int>;
                            int c = il.Count;
                            for(int j = 0; j < c; j++) {
                                il[j] = j;
                            }
                        });
                        writeThreads[i].Start(list);
                    }
                    for(int i = 0; i < readThreads.Length; i++) {
                        readThreads[i] = new Thread(p => {
                            IList<int> il = p as IList<int>;
                            int c = il.Count;
                            for(int j = 0; j < c; j++) {
                                int temp = il[j];
                            }
                        });
                        readThreads[i].Start(list);
                    }
                    for(int i = 0; i < readThreads.Length; i++)
                        readThreads[i].Join();
                    for(int i = 0; i < writeThreads.Length; i++)
                        writeThreads[i].Join();
                };
                sw.Stop();
                Console.WriteLine("time: {0} class: {1}", sw.Elapsed, list.GetType());
            }
            Console.WriteLine("DONE");
            Console.ReadLine();
        }
    } // end class
} // end namespace

Aqui está um resultado típico:

time: 00:00:03.0965242 class: System.Collections.Generic.List`1[System.Int32]
time: 00:00:11.9194573 class: LockPlay.Program+MonitorList`1[System.Int32]
time: 00:00:08.9510258 class: LockPlay.Program+RWLSList`1[System.Int32]
time: 00:00:16.9888435 class: LockPlay.Program+RWLSExtList`1[System.Int32]
DONE

Como você pode ver, usando as extensões realmente faz com que o desempenho PIOR do que apenas usando lock (monitor).

Foi útil?

Solução

Parece que o seu preço de instanciar milhões de estruturas e a pouco mais de invocações.

Eu iria tão longe a ponto de dizer que o ReaderWriterLockSlim está sendo mal utilizado neste exemplo, um bloqueio é bom o suficiente, neste caso, ea vantagem de desempenho que você começa com o ReaderWriterLockSlim é insignificante em comparação com o preço de explicar esses conceitos para juniores devs.

Você ganha um enorme vantagem com fechaduras de estilo leitor escritor quando é preciso uma quantidade não negligenciável de tempo para realizar leituras e gravações. O aumento será maior quando você tem um sistema predominantemente ler base.

Tente inserir um Thread.Sleep (1), enquanto os bloqueios são adquiridos para ver o quão grande a diferença que faz.

Veja este benchmark:

Time for Test.SynchronizedList`1[System.Int32] Time Elapsed 12310 ms
Time for Test.ReaderWriterLockedList`1[System.Int32] Time Elapsed 547 ms
Time for Test.ManualReaderWriterLockedList`1[System.Int32] Time Elapsed 566 ms

Na minha aferição Eu realmente não notar muita diferença entre os dois estilos, eu me sentiria confortável com ele desde que tinha alguma proteção finalizador no caso as pessoas se esquecem de dispor ....

using System.Threading;
using System.Diagnostics;
using System.Collections.Generic;
using System;
using System.Linq;

namespace Test {

    static class RWLSExtension {
        struct Disposable : IDisposable {
            readonly Action _action;
            public Disposable(Action action) {
                _action = action;
            }
            public void Dispose() {
                _action();
            }
        }

        public static IDisposable ReadLock(this ReaderWriterLockSlim rwls) {
            rwls.EnterReadLock();
            return new Disposable(rwls.ExitReadLock);
        }
        public static IDisposable UpgradableReadLock(this ReaderWriterLockSlim rwls) {
            rwls.EnterUpgradeableReadLock();
            return new Disposable(rwls.ExitUpgradeableReadLock);
        }
        public static IDisposable WriteLock(this ReaderWriterLockSlim rwls) {
            rwls.EnterWriteLock();
            return new Disposable(rwls.ExitWriteLock);
        }
    }

    class SlowList<T> {

        List<T> baseList = new List<T>();

        public void AddRange(IEnumerable<T> items) {
            baseList.AddRange(items);
        }

        public virtual T this[int index] {
            get {
                Thread.Sleep(1);
                return baseList[index];
            }
            set {
                baseList[index] = value;
                Thread.Sleep(1);
            }
        }
    }

    class SynchronizedList<T> : SlowList<T> {

        object sync = new object();

        public override T this[int index] {
            get {
                lock (sync) {
                    return base[index];
                }

            }
            set {
                lock (sync) {
                    base[index] = value;
                }
            }
        }
    }


    class ManualReaderWriterLockedList<T> : SlowList<T> {

        ReaderWriterLockSlim slimLock = new ReaderWriterLockSlim();

        public override T this[int index] {
            get {
                T item;
                try {
                    slimLock.EnterReadLock();
                    item = base[index];
                } finally {
                    slimLock.ExitReadLock();
                }
                return item;

            }
            set {
                try {
                    slimLock.EnterWriteLock();
                    base[index] = value;
                } finally {
                    slimLock.ExitWriteLock();
                }
            }
        }
    }

    class ReaderWriterLockedList<T> : SlowList<T> {

        ReaderWriterLockSlim slimLock = new ReaderWriterLockSlim();

        public override T this[int index] {
            get {
                using (slimLock.ReadLock()) {
                    return base[index];
                }
            }
            set {
                using (slimLock.WriteLock()) {
                    base[index] = value;
                }
            }
        }
    }


    class Program {


        private static void Repeat(int times, int asyncThreads, Action action) {
            if (asyncThreads > 0) {

                var threads = new List<Thread>();

                for (int i = 0; i < asyncThreads; i++) {

                    int iterations = times / asyncThreads;
                    if (i == 0) {
                        iterations += times % asyncThreads;
                    }

                    Thread thread = new Thread(new ThreadStart(() => Repeat(iterations, 0, action)));
                    thread.Start();
                    threads.Add(thread);
                }

                foreach (var thread in threads) {
                    thread.Join();
                }

            } else {
                for (int i = 0; i < times; i++) {
                    action();
                }
            }
        }

        static void TimeAction(string description, Action func) {
            var watch = new Stopwatch();
            watch.Start();
            func();
            watch.Stop();
            Console.Write(description);
            Console.WriteLine(" Time Elapsed {0} ms", watch.ElapsedMilliseconds);
        }

        static void Main(string[] args) {

            int threadCount = 40;
            int iterations = 200;
            int readToWriteRatio = 60;

            var baseList = Enumerable.Range(0, 10000).ToList();

            List<SlowList<int>> lists = new List<SlowList<int>>() {
                new SynchronizedList<int>() ,
                new ReaderWriterLockedList<int>(),
                new ManualReaderWriterLockedList<int>()
            };

            foreach (var list in lists) {
                list.AddRange(baseList);
            }


            foreach (var list in lists) {
                TimeAction("Time for " + list.GetType().ToString(), () =>
                {
                    Repeat(iterations, threadCount, () =>
                    {
                        list[100] = 99;
                        for (int i = 0; i < readToWriteRatio; i++) {
                            int ignore = list[i];
                        }
                    });
                });
            }



            Console.WriteLine("DONE");
            Console.ReadLine();
        }
    }
}

Outras dicas

O código parece usar uma estrutura para evitar objeto criação em cima, mas não toma as outras medidas necessárias para manter este peso leve. Eu acredito que as caixas do valor de retorno de ReadLock, e se assim nega toda a vantagem da estrutura. Isso deve resolver todos os problemas e executar tão bem como não indo através da interface IDisposable.

Edit: Benchmarks exigiu. Estes resultados são normalizados para que o Manual método (chamado Enter / ExitReadLock e Enter / ExitWriteLock em linha com o código protegido) tem um valor de tempo de 1,00. O método original é lento porque ele aloca objetos no heap que o método manual não. Eu fixo este problema, e no modo de versão, mesmo a chamada de método de extensão sobrecarga vai embora deixando-o de forma idêntica tão rápido quanto o método manual.

Debug Constituição:

Manual:              1.00
Original Extensions: 1.62
My Extensions:       1.24

compilação de lançamento:

Manual:              1.00
Original Extensions: 1.51
My Extensions:       1.00

Meu código:

internal static class RWLSExtension
{
    public static ReadLockHelper ReadLock(this ReaderWriterLockSlim readerWriterLock)
    {
        return new ReadLockHelper(readerWriterLock);
    }

    public static UpgradeableReadLockHelper UpgradableReadLock(this ReaderWriterLockSlim readerWriterLock)
    {
        return new UpgradeableReadLockHelper(readerWriterLock);
    }

    public static WriteLockHelper WriteLock(this ReaderWriterLockSlim readerWriterLock)
    {
        return new WriteLockHelper(readerWriterLock);
    }

    public struct ReadLockHelper : IDisposable
    {
        private readonly ReaderWriterLockSlim readerWriterLock;

        public ReadLockHelper(ReaderWriterLockSlim readerWriterLock)
        {
            readerWriterLock.EnterReadLock();
            this.readerWriterLock = readerWriterLock;
        }

        public void Dispose()
        {
            this.readerWriterLock.ExitReadLock();
        }
    }

    public struct UpgradeableReadLockHelper : IDisposable
    {
        private readonly ReaderWriterLockSlim readerWriterLock;

        public UpgradeableReadLockHelper(ReaderWriterLockSlim readerWriterLock)
        {
            readerWriterLock.EnterUpgradeableReadLock();
            this.readerWriterLock = readerWriterLock;
        }

        public void Dispose()
        {
            this.readerWriterLock.ExitUpgradeableReadLock();
        }
    }

    public struct WriteLockHelper : IDisposable
    {
        private readonly ReaderWriterLockSlim readerWriterLock;

        public WriteLockHelper(ReaderWriterLockSlim readerWriterLock)
        {
            readerWriterLock.EnterWriteLock();
            this.readerWriterLock = readerWriterLock;
        }

        public void Dispose()
        {
            this.readerWriterLock.ExitWriteLock();
        }
    }
}

Meu palpite (você precisaria perfil para verificar) é que a queda de desempenho não é de criar as instâncias descartáveis ??(que deve ser bastante barato, sendo estruturas). Em vez disso eu espero é de criar os delegados ação. Você pode tentar mudar a implementação de sua estrutura descartável para armazenar a instância do ReaderWriterLockSlim em vez de criar um delegado Ação.

Editar:. @ 280Z28 do confirma pós que é a alocação de pilha de delegados de Ação que está causando a desaceleração

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top