Lock, Monitor e Mutex em C#

Introdução

Com a utilização de vários processadores, tanto em notebooks e PCs, como em dispositivos móveis, a necessidade de programação multi-thread tornou-se bastante evidente. Isso porque, a execução de Threads em paralelo permite a utilização dos vários processadores ao mesmo tempo, melhorando assim a performance da aplicação que utiliza essa técnica.

Além disso, a execução de várias Threads em paralelo, permite que aplicações executem longas operações em background enquanto interagem com o usuário. Pense por exemplo, no Windows quando baixa atualizações da internet ou o famoso WhatsApp que envia e recebe mensagens assincronamente, o tempo todo, em celulares.

Por outro lado, a má utilização de Threads leva a problemas como bugs intermitentes, bem difíceis de serem detectados, por um motivo simples: como Threads são executadas ao mesmo tempo, sem garantia de ordem, é impossível saber a ordem exata que porções de código serão executadas.

Daí percebe-se a importância e a necessidade de se programar adequadamente usando técnicas de multi-threading. Nesse artigo é mostrado o constructo lock, disponível em C# .NET para trabalhar-se com Threads. No exemplo mostrado, ele é utilizado para implementar o design pattern Monitor. Além disso, a técnica de programação assíncrona Mutex, implementada pela classe System.Threading.Mutex, é mostrada também. Mutex também utiliza-se do design pattern Monitor.

O design pattern Monitor, resumidamente, garante de forma elegante que: dado que várias Threads precisem acessar um mesmo conjunto de dados ou trecho de código, será permitido que somente uma delas faça esse acesso por vez.

A técnica de programação Mutex, tem exatamente o mesmo objetivo do design pattern Monitor, contudo ao invés de Threads, processos e aplicações distintos podem acessar um conjunto de dados ou trecho de código por vez.

Perceba que, o Monitor isola porções de código e dados em apenas uma aplicação, ao passo que o Mutex o faz para processos e aplicações distintos.

Um exemplo no mundo real onde isso acontece é a utilização de lavatórios em aviões. Dado um lavatório, pode haver uma fila com várias pessoas querendo usá-lo ao mesmo tempo. Porém, apenas uma pode fazê-lo por vez. Note que aqui pessoas são equivalentes a Threads e a utilização do lavatório refere-se aos trechos de código e ao conjunto de dados que pode ser acessado apenas por uma Thread. Também observe que, caso pessoas sejam Threads, trata-se do Monitor, contudo, caso sejam processos ou aplicações distintos, a técnica Mutex é do que se trata o exemplo.

O constructo lock também é mostrado aqui. Ele isola um bloco de código que, obrigatoriamente só pode ser acessado por uma Thread, por vez. Logo, pode-se implementar o design pattern Monitor com ele.

A seguir, é mostrado um exemplo de execução de duas Threads simultaneamente e sem sincronização, causando problemas. Depois, um segundo exemplo é exibido, usando lock implementando o design pattern Monitor. Um terceiro e último caso é mostrado, usando Mutex que também implementa o Monitor, como discutido anteriormente. Finalmente, algumas considerações são feitas no final do artigo.

Exemplo sem sincronização

A seguir, é mostrado um exemplo de código representando transações bancárias. A classe a seguir chamada BankAccountSimple possui três operações simples:

  • Construtor com um parâmetro double, que representa a quantidade de dinheiro inicial da conta.
  • Colocar dinheiro – método: void AddMoney(double)
  • Retirar dinheiro – método: void WithdrawMoney(double)
  • Verificar extrato – método: double GetBankStatement()

O funcionamento de ambos é bem simples. Caso se deseje colocar dinheiro na conta, chame o método void AddMoney(double), para se fazer uma retirada: void WithdrawMoney(double) e para saber o saldo da conta: double GetBankStatement(). Também note que, a qualquer momento, se a conta ficar negativa a exceção ArgumentException é atirada informando que o argumento do método utilizado é inválido.



using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace MutexArticle
{
    class BankAccountSimple
    {
        private double bankMoney = 0d;

        public BankAccountSimple(double money)
        {
            LogConsole("Setting initial amount of money: " + money);

            if (money < 0)
            {
                LogConsole("The entered money quantity cannot be negative. Money: " + money);
                throw new ArgumentException(GetMessageWithTreadId("The entered money quantity cannot be negative. Money: " + money));
            }

            this.bankMoney = money;
        }

        public void AddMoney(double money = 0) 
        {
            LogConsole("Money to be added: " + money);

            if (money < 0)
            {
                LogConsole("The entered money quantity cannot be negative. Money: " + money);
                throw new ArgumentException(GetMessageWithTreadId("The entered money quantity cannot be negative. Money: " + money));
            }

            this.bankMoney = this.bankMoney + money;

            if (this.bankMoney < 0)
            {
                LogConsole("The money quantity cannot be negative. Money: " + money);
                throw new ArgumentException(GetMessageWithTreadId("The money quantity cannot be negative. Money: " + money));
            }

            LogConsole("Total amount of money: " + this.bankMoney);
        }

        public void WithdrawMoney(double money = 0)
        {

            if (money < 0)
            {
                LogConsole("The entered money quantity cannot be negative. Money: " + money);
                throw new ArgumentException(GetMessageWithTreadId("The entered money quantity cannot be negative. Money: " + money));
            }

            LogConsole("Money to be withdrawed: " + money);

            this.bankMoney = this.bankMoney - money;

            if (this.bankMoney < 0)
            {
                LogConsole("The money quantity cannot be negative. Money: " + money);
                throw new ArgumentException(GetMessageWithTreadId("The money quantity cannot be negative. Money: " + money));
            }

            Console.WriteLine("Thread ID: " + getCurrenThreadId() + ": Total amount of money: " + this.bankMoney);
        }

        public double GetBankStatement()
        {
            LogConsole("Bank Statement: Total amount of money: " + this.bankMoney);
            return bankMoney;
        }

        private String getCurrenThreadId()
        {
            return Thread.CurrentThread.ManagedThreadId.ToString();
        }

        private void LogConsole(String message)
        {
            Console.WriteLine(GetMessageWithTreadId(message));
        }

        private String GetMessageWithTreadId(String message)
        {
            return "Thread ID: " + getCurrenThreadId() + ": " + message;
        }
    }
}


Agora, imagine o seguinte cenário: duas Threads acessando operações de colocar e retirar dinheiro da conta, ao mesmo tempo. Perceba que, caso esses métodos não sejam isolados de alguma maneira, duas Threads podem executá-los ao mesmo tempo fazendo uma bagunça. Por exemplo, ao depositar dinheiro, uma Thread pode já ter depositado o dinheiro e estar imprimindo o extrato bancário, ao mesmo tempo que uma outra pode estar colocando dinheiro. Note que, dados inconsistentes serão impressos ao usuário na tela. Além disso, o balanço final da conta pode estar errado.

Pense em outro cenário: uma Thread deposita e outra saca dinheiro ao mesmo tempo. Caso essas operações não sejam feitas separadamente, além de possíveis problemas de saldo as informações impressas ao usuário podem ser bagunçadas.

Veja o trecho do código a seguir em que duas Threads são executadas ao mesmo tempo, concorrendo por um Objeto BankAccountSimple. Não haverá garantia de ordem de execução, muito de que a execução de um método é terminada antes de que outro também o execute. Isso, claramente, pode gerar dados inconsistentes.

...........

BankAccountSimple accountSimple = new BankAccountSimple(100);

Thread threadAdd_50 = new Thread(() => accountSimple.AddMoney(50));
Thread threadWithdraw_110 = new Thread(() => accountSimple.WithdrawMoney(110));

threadAdd_50.Start();
threadWithdraw_110.Start();

...........

O resultado de execuções repetidas do código acima, levam a resultados mostrados nas duas figuras abaixo:

mutex_c#_RunWithNoLock
Resultados da várias execuções de código não sem tratamento de sincronização.
mutex_c#_RunWithNoLock2
Resultados da várias execuções de código não sem tratamento de sincronização.

Note que, cada Thread chama apenas um método. Para que a operação ocorra de forma correta, o método todo, independente de qual seja, deve ser executado sem interrupções. Por exemplo, o método void AddMoney(double) não pode ser interrompido. Logo, se ele tiver um Thread ID (10 por exemplo), nenhuma outra outra Thread pode ser executada até que todos os passos do método em questão sejam executados. Isso pode ser visto facilmente nas figuras acima. Quando existem interpolações de Thread IDs, significa que o método foi interrompido e houve problemas de concorrência.

Observe na primeira imagem que:

  • Na primeira execução as Threads foram executadas na ordem correta.
  • Já na segunda, as Threads se misturaram nos métodos. Observe que, a Thread de ID 3 começou sua execução, contudo a de ID 4 começou a rodar antes que a de ID 3 terminasse. Logo, os dados ficam de forma inconsistente pois a Thread de ID 3 deveria ter sido executada por completo antes da sua companheira começar.
  • O mesmo ocorre na terceira execução, com uma exceção sendo lançada pois o saldo da conta fica negativo.

Na segunda imagem, cenários parecidos mostram problemas de concorrência.

Implementando o design pattern Monitor utilizando-se o lock

Antes de ser mostrada a solução com Mutex, a clássica solução utilizando-se o constructo de lock será descrita aqui.

A ideia é a seguinte: quando se coloca um código dentro do constructo lock, o mesmo é acessado por apenas uma Thread, por vez. Em outras palavras, dada duas Threads, T1 e T2 e, dado um trecho de código CC1. Caso T1 e T2 tentem acessar o mesmo trecho de código CC1, apenas uma poderá fazê-lo por vez.

Além disso, o constructo lock precisa receber como parâmetro um objeto. Isso significa que, para um trecho de código com lock CC1, com parâmetro um objeto O1 e dois trechos de código CC2 e CC3 com parâmetros definidos também como objeto O1, caso um dos trechos de código CC1, CC2 ou CC3 estejam sendo acessados, nenhum outro poderá ser executado por outra Thread. Note que, caso o objeto O1 seja acessado pelo trecho de código CC1, CC2 e CC3 não poderão ser executados, logo o objeto O1 estará protegido e o design pattern Monitor é implementado.

Perceba também que, no caso anterior, caso CC1 utilize o parâmetro O1, CC2 o parâmetro O2 e CC3 o parâmetro O3, o acesso aos trechos de código CC1, CC2 e CC3 não é mutualmente exclusivo. Ou seja, CC1, CC2 e CC3 podem ser acessados simultaneamente por Threads diferentes. Contudo, eles não podem ser acessados por mais de uma Thread pois estão sincronizados.

Abaixo é mostrado o exemplo do capítulo anterior, utilizando-se lock.


using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace MutexArticle
{
    class BankAccountLock
    {
        private double bankMoney = 0d;

        public BankAccountLock(double money)
        {
            LogConsole("Setting initial amount of money: " + money);

            if (money < 0)
            {
                LogConsole("The entered money quantity cannot be negative. Money: " + money);
                throw new ArgumentException(GetMessageWithTreadId("The entered money quantity cannot be negative. Money: " + money));
            }

            this.bankMoney = money;
        }

        public void AddMoney(double money) 
        {
            lock (this)
            {
                LogConsole("Money to be added: " + money);

                if (money < 0)
                {
                    LogConsole("The entered money quantity cannot be negative. Money: " + money);
                    throw new ArgumentException(GetMessageWithTreadId("The entered money quantity cannot be negative. Money: " + money));
                }

                this.bankMoney = this.bankMoney + money;


                if (this.bankMoney < 0)
                {
                    LogConsole("The money quantity cannot be negative. Money: " + this.bankMoney);
                    throw new ArgumentException(GetMessageWithTreadId("The money quantity cannot be negative. Money: " + this.bankMoney));
                }

                Console.WriteLine("Thread ID: " + getCurrenThreadId() + ": Total amount of money: " + this.bankMoney);
            }
        }

        public void WithdrawMoney(double money)
        {
            lock (this)
            {
                if (money < 0)
                {
                    LogConsole("The entered money quantity cannot be negative. Money: " + money);
                    throw new ArgumentException(GetMessageWithTreadId("The entered money quantity cannot be negative. Money: " + money));
                }

                Console.WriteLine("Thread ID: " + getCurrenThreadId() + ": Money to be withdrawed: " + money);

                this.bankMoney = this.bankMoney - money;

                if (this.bankMoney < 0)
                {
                    LogConsole("The money quantity cannot be negative. Money: " + this.bankMoney);
                    throw new ArgumentException(GetMessageWithTreadId("The money quantity cannot be negative. Money: " + this.bankMoney));
                }

                Console.WriteLine("Thread ID: " + getCurrenThreadId() + ": Total amount of money: " + this.bankMoney);
            }
        }

        public double GetBankStatement()
        {
            LogConsole("Bank Statement: Total amount of money: " + this.bankMoney);
            return bankMoney;
        }

        private String getCurrenThreadId()
        {
            return Thread.CurrentThread.ManagedThreadId.ToString();
        }

        private void LogConsole(String message)
        {
            Console.WriteLine(GetMessageWithTreadId(message));
        }

        private String GetMessageWithTreadId(String message)
        {
            return "Thread ID: " + getCurrenThreadId() + ": " + message;
        }
    }
}


O trecho de código que executa o exemplo acima é mostrado a seguir:


................................

BankAccountLock accountLock = new BankAccountLock(100);

Thread threadAdd_50 = new Thread(() => accountLock.AddMoney(50));
Thread threadWithdraw_110 = new Thread(() => accountLock.WithdrawMoney(110));

threadAdd_50.Start();
threadWithdraw_110.Start();
................................

Perceba que agora os métodos void AddMoney(double)void WithdrawMoney(double) possuem o constructo de lock que envolve todo o código. Agora, o trecho de código dentro do lock é acessado apenas por uma Thread, por vez. Isso significa que, dado que uma operação de depósito ou retirada de dinheiro ocorra, nenhuma outra será feita na mesma porção do código, ou em qualquer outra que esteja com o constructo lock no this.

Como dito anteriormente, o lock tem como parâmetro o this (note que, pode ser qualquer objeto, o uso do this é usado nesse exemplo). Isso implica que todo o objeto é acessado por uma única Thread durante a execução de qualquer um dos blocos sincronizados. Portanto, mesmo que Threads executem métodos diferentes, as mesmas não poderão executá-los simultaneamente. Por exemplo, caso o método void AddMoney(double) esteja sendo executado, o método void WithdrawMoney(double) não poderá pois o lock trava o mesmo objeto. Veja que o design pattern Monitor foi implementado aqui. Abaixo é mostrada a execução do código desse tópico.

mutex_c_runWithLock
Resultados da várias execuções de código implementando o design pattern Monitor.

Agora, um método é sempre executado por completo, sem ser interrompido por outras Threads. Desse modo todas as operações se tornam consistentes. No primeiro caso mostrado, a Thread com ID 3 executou seu método por completo, bem como a Thread de ID 4. No segundo exemplo executado, Como a Thread que faz retirada foi executada primeiro, uma exceção foi atirada já que a conta ficou com saldo negativo. Em momento nenhuma Thread que faz depósito foi chamada.

Note que, caso se tenha dois blocos CC1, CC2 com lock nos Objetos O1 e O2, respectivamente. Perceba que, nesse caso, a execução de C1 não impede a execução C2, pois os blocos travam objetos diferentes. Nesse caso, não haveria a garantia de uma operação atômica no depósito e retirada de dinheiro. Menos ainda, o design pattern Monitor não seria implementado.

Implementando o Mutex

Agora, será utilizada a técnica Mutex. No exemplo, dois métodos são importantes:

  • bool Mutex.WaitOne(…)  – retorna true caso nenhuma Thread tenha requerido o lock de um trecho de código através desse mesmo método, ou quando uma Thread tenha liberado o objeto de Mutex através do método Mutex.ReleaseMutex(). No caso do tempo de timeout expirar, false é retornado. Isso indica que o lock não foi obtido e isso deve ser tratado pelo código.
  • void Mutex.ReleaseMutex() – Libera o lock do Mutex para que outra Thread possa pegá-lo. Note que esse método deve sempre ser chamado na cláusula finally, pois se algum erro inesperado ocorrer, o trecho de código do finally sempre será chamado. Caso não haja liberação do objeto de Mutex, existe a chance do mesmo ficar travado indefinidamente.

Veja como o código a seguir utiliza o Mutex:


using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Runtime.InteropServices;
using System.Reflection;
using System.Security.AccessControl;
using System.Security.Principal;

namespace MutexArticle
{
    class BankAccountMutex
    {
        private double bankMoney = 0d;

        Mutex mutex = null;

        private const int MUTEX_WAIT_TIMEOUT = 5000;

        // Note: configuration based on stackoverflow answer: http://stackoverflow.com/questions/229565/what-is-a-good-pattern-for-using-a-global-mutex-in-c
        public BankAccountMutex(double money)
        {
            // get application GUID as defined in AssemblyInfo.cs
            string appGuid = ((GuidAttribute)Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(GuidAttribute), false).GetValue(0)).Value.ToString();

            // unique id for global mutex - Global prefix means it is global to the machine
            string mutexId = string.Format("Global\\{{{0}}}", appGuid);

            // Need a place to store a return value in Mutex() constructor call
            bool createdNew;

            // set up security for multi-user usage
            // work also on localized systems (don't use just "Everyone") 
            var allowEveryoneRule = new MutexAccessRule(new SecurityIdentifier(WellKnownSidType.WorldSid, null), MutexRights.FullControl, AccessControlType.Allow);
            var securitySettings = new MutexSecurity();
            securitySettings.AddAccessRule(allowEveryoneRule);

            mutex = new Mutex(false, mutexId, out createdNew, securitySettings);

            LogConsole("Setting initial amount of money: " + money);

            if (money < 0)
            {
                LogConsole("The entered money quantity cannot be negative. Money: " + money);
                throw new ArgumentException(GetMessageWithTreadId("The entered money quantity cannot be negative. Money: " + money));
            }

            this.bankMoney = money;
        }

        public void AddMoney(double money = 0) 
        {
            bool hasHandle = mutex.WaitOne(MUTEX_WAIT_TIMEOUT, false);

            if (!hasHandle)
            {
                throw new TimeoutException(GetMessageWithTreadId("Method void AddMoney(double): Timeout due to look waiting."));
            }

            try
            {
                LogConsole("Money to be added: " + money);

                if (money < 0)
                {
                    LogConsole("The entered money quantity cannot be negative. Money: " + money);
                    throw new ArgumentException(GetMessageWithTreadId("The entered money quantity cannot be negative. Money: " + money));
                }

                this.bankMoney = this.bankMoney + money;

                if (this.bankMoney < 0)
                {
                    LogConsole("The money quantity cannot be negative. Money: " + money);
                    throw new ArgumentException(GetMessageWithTreadId("The money quantity cannot be negative. Money: " + money));
                }

                LogConsole("Total amount of money: " + this.bankMoney);
            }
            finally
            {
                if (hasHandle)
                {
                    mutex.ReleaseMutex();
                }
            }
        }

        public void WithdrawMoney(double money = 0)
        {
            bool hasHandle = mutex.WaitOne(MUTEX_WAIT_TIMEOUT, false);

            if (!hasHandle)
            {
                throw new TimeoutException(GetMessageWithTreadId("Method void WithdrawMoney(double): Timeout due to look waiting."));
            }

            try
            {

                if (money < 0)
                {
                    LogConsole("The entered money quantity cannot be negative. Money: " + money);
                    throw new ArgumentException(GetMessageWithTreadId("The entered money quantity cannot be negative. Money: " + money));
                }

                LogConsole("Money to be withdrawed: " + money);

                this.bankMoney = this.bankMoney - money;

                if (this.bankMoney < 0)
                {
                    LogConsole("The money quantity cannot be negative. Money: " + money);
                    throw new ArgumentException(GetMessageWithTreadId("The money quantity cannot be negative. Money: " + money));
                }

                LogConsole("Total amount of money: " + this.bankMoney);
            }
            finally
            {
                if (hasHandle)
                {
                    mutex.ReleaseMutex();
                }
            }
        }

        public double GetBankStatement()
        {
            LogConsole("Bank Statement: Total amount of money: " + this.bankMoney);
            return bankMoney;
        }

        private String getCurrenThreadId()
        {
            return Thread.CurrentThread.ManagedThreadId.ToString();
        }

        private void LogConsole(String message)
        {
            Console.WriteLine(GetMessageWithTreadId(message));
        }

        private String GetMessageWithTreadId(String message)
        {
            return "Thread ID: " + getCurrenThreadId() + ": " + message;
        }
    }
}


A seguir é mostrado o código que executa Threads usando Mutex.


................................


BankAccountMutex accountMutex = new BankAccountMutex(100);

Thread threadAdd_50 = new Thread(() => accountMutex.AddMoney(50));
Thread threadWithdraw_110 = new Thread(() => accountMutex.WithdrawMoney(110));

threadAdd_50.Start();
threadWithdraw_110.Start();
.................................

Note que nos métodos void AddMoney(double)void WithdrawMoney(double) a utilização do Mutex é a mesma. No início se obtém a trava do objeto e na clausula finally se libera a mesma.

Dada uma Thread T1 que acesse o método void AddMoney(double), Threads T2 e T3 não poderiam acessar o método mencionado nem o método void WidthdrawMoney(double), pois o objeto Mutex travou seu acesso na primeira chamada do método void AddMoney(double). Quando o mesmo for liberado, aí a Threads T2 e T3 poderão competir pelos métodos controlados pelo objeto Mutex. 

Portanto perceba que a classe BankAccountMutex também implementa o design pattern Monitor, utilizando Mutex.

A execução do código desse exemplo, é idêntica ao do tópico anterior. Nesse caso também, métodos são executados sem interrupções de outras Threads.

Considerações sobre MonitorsMutex e sincronização utilizando lock

O constructo lock faz com que recursos e porções de código sejam executados por uma Thread, por vez. Note que, ele não consegue garantir o mesmo para processos ou aplicações distintos. Além disso, esse constructo não necessariamente implementa o design pattern Monitor, mas geralmente é interessante que ele o faça. Entendendo as diferença e semelhanças entre lockMonitorMutex, pode-se utilizá-los adequadamente.

A classe System.Threading.Mutex, provida pelo C#, garante o isolamento de recursos e trechos de código, de processos ou aplicações distintos. Utilizada corretamente ela implementa o design pattern Monitor.

Note que existe em C# a classe System.Threading.Monitor. Contudo, a mesma não foi utilizada propositalmente, para que os conceitos desse design pattern fossem expostos de outras maneiras, utilizando exemplos menos óbvios. Isso facilita o entendimento do leitor.

Código-fonte

Você pode baixar todo o código utilizado nesse artigo no seguinte link do Github: https://github.com/dufernandes/talkitbr/tree/master/MutexArticle

Referências

Anúncios

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s