Sou desenvolvedor júnior que trabalha para escrever uma atualização para software que recebe dados de uma solução de terceiros, armazena-os em um banco de dados e condiciona os dados para uso por outra solução de terceiros. Nosso software é executado como um serviço do Windows.
Observando o código de uma versão anterior, vejo o seguinte:
static Object _workerLocker = new object();
static int _runningWorkers = 0;
int MaxSimultaneousThreads = 5;
foreach(int SomeObject in ListOfObjects)
{
lock (_workerLocker)
{
while (_runningWorkers >= MaxSimultaneousThreads)
{
Monitor.Wait(_workerLocker);
}
}
// check to see if the service has been stopped. If yes, then exit
if (this.IsRunning() == false)
{
break;
}
lock (_workerLocker)
{
_runningWorkers++;
}
ThreadPool.QueueUserWorkItem(SomeMethod, SomeObject);
}
A lógica parece clara: aguarde espaço no conjunto de encadeamentos, verifique se o serviço não foi interrompido, aumente o contador de encadeamentos e enfileire o trabalho. _runningWorkers
é decrementado dentro de SomeMethod()
uma lock
instrução que chama Monitor.Pulse(_workerLocker)
.
Minha pergunta é:
Existe algum benefício em agrupar todo o código em um único lock
, assim:
static Object _workerLocker = new object();
static int _runningWorkers = 0;
int MaxSimultaneousThreads = 5;
foreach (int SomeObject in ListOfObjects)
{
// Is doing all the work inside a single lock better?
lock (_workerLocker)
{
// wait for room in ThreadPool
while (_runningWorkers >= MaxSimultaneousThreads)
{
Monitor.Wait(_workerLocker);
}
// check to see if the service has been stopped.
if (this.IsRunning())
{
ThreadPool.QueueUserWorkItem(SomeMethod, SomeObject);
_runningWorkers++;
}
else
{
break;
}
}
}
Parece que isso pode causar um pouco mais de espera por outros encadeamentos, mas parece que bloquear repetidamente em um único bloco lógico também seria um pouco demorado. No entanto, eu sou novo no multi-threading, então estou assumindo que existem outras preocupações aqui das quais não conheço.
Os únicos outros lugares em que _workerLocker
fica bloqueado estão SomeMethod()
e apenas com a finalidade de decrementar _runningWorkers
e, em seguida, fora do foreach
para aguardar o número de _runningWorkers
ir para zero antes de registrar e retornar.
Obrigado por qualquer ajuda.
EDIT 8/4/15
Agradecemos a @delnan pela recomendação de usar um semáforo. O código se torna:
static int MaxSimultaneousThreads = 5;
static Semaphore WorkerSem = new Semaphore(MaxSimultaneousThreads, MaxSimultaneousThreads);
foreach (int SomeObject in ListOfObjects)
{
// wait for an available thread
WorkerSem.WaitOne();
// check if the service has stopped
if (this.IsRunning())
{
ThreadPool.QueueUserWorkItem(SomeMethod, SomeObject);
}
else
{
break;
}
}
WorkerSem.Release()
é chamado por dentro SomeMethod()
.
SomeMethod
forma assíncrona, a seção "lock" acima será deixada antes ou pelo menos logo após o início do novo encadeamento SomeMethod
.
Monitor.Wait()
é liberar e readquirir o bloqueio para que outro recurso ( SomeMethod
neste caso) possa usá-lo. Por outro lado, SomeMethod
obtém o bloqueio, diminui o contador e, em seguida, chama o Monitor.Pulse()
que retorna o bloqueio ao método em questão. Novamente, esse é meu próprio entendimento.
Monitor.Wait
libera a trava. Eu recomendo dar uma olhada nos documentos.