Pergunta

Eu tenho um custom thread pool de classe, que cria alguns segmentos que cada esperar no seu próprio evento (sinal).Quando uma nova tarefa é adicionada ao pool de threads, desperta o primeiro thread livre para que ele seja executado o trabalho.

O problema é o seguinte :Eu tenho cerca de 1000 ciclos de cerca de 10'000 iterações para fazer.Estes circuitos devem ser executadas seqüencialmente, mas eu tenho 4 CPUs disponíveis.O que eu tento fazer é dividir o 10'000 ciclos de iteração em 4 a 2'500 iterações loops, ou seja, um por linha.Mas eu tenho que esperar para os 4 ciclos pequenos para concluir, antes de ir para a próxima "grande" iteração.Isso significa que eu não posso agrupar os postos de trabalho.

O meu problema é que usando o pool de threads e 4 threads é muito mais lento do que fazer as tarefas sequencialmente (tendo um loop executado por um thread separado é muito mais lento do que executá-lo diretamente no thread principal sequencialmente).

Eu estou no Windows, portanto, criar eventos com CreateEvent() e, em seguida, aguarde que em um deles, utilizando WaitForMultipleObjects(2, handles, false, INFINITE) até que a thread principal chama SetEvent().

Parece que este evento, que coisa (juntamente com a sincronização entre as threads usando seções críticas) é muito caro !

A minha pergunta é :é normal que a utilização de eventos leva "muito tempo"?Se sim, há outro mecanismo que eu poderia usar e que iria ser menos dispendiosas ?

Aqui está um código para ilustrar (algumas partes relevantes copiado do meu thread pool de classe) :

// thread function
unsigned __stdcall ThreadPool::threadFunction(void* params) {
    // some housekeeping
    HANDLE signals[2];
    signals[0] = waitSignal;
    signals[1] = endSignal;

    do {
        // wait for one of the signals
        waitResult = WaitForMultipleObjects(2, signals, false, INFINITE);

        // try to get the next job parameters;
        if (tp->getNextJob(threadId, data)) {
            // execute job
            void* output = jobFunc(data.params);

            // tell thread pool that we're done and collect output
            tp->collectOutput(data.ID, output);
        }

        tp->threadDone(threadId);
    }
    while (waitResult - WAIT_OBJECT_0 == 0);

    // if we reach this point, endSignal was sent, so we are done !

    return 0;
}

// create all threads
for (int i = 0; i < nbThreads; ++i) {
    threadData data;
    unsigned int threadId = 0;
    char eventName[20];

    sprintf_s(eventName, 20, "WaitSignal_%d", i);

    data.handle = (HANDLE) _beginthreadex(NULL, 0, ThreadPool::threadFunction,
        this, CREATE_SUSPENDED, &threadId);
    data.threadId = threadId;
    data.busy = false;
    data.waitSignal = CreateEvent(NULL, true, false, eventName);

    this->threads[threadId] = data;

    // start thread
    ResumeThread(data.handle);
}

// add job
void ThreadPool::addJob(int jobId, void* params) {
    // housekeeping
    EnterCriticalSection(&(this->mutex));

    // first, insert parameters in the list
    this->jobs.push_back(job);

    // then, find the first free thread and wake it
    for (it = this->threads.begin(); it != this->threads.end(); ++it) {
        thread = (threadData) it->second;

        if (!thread.busy) {
            this->threads[thread.threadId].busy = true;

            ++(this->nbActiveThreads);

            // wake thread such that it gets the next params and runs them
            SetEvent(thread.waitSignal);
            break;
        }
    }

    LeaveCriticalSection(&(this->mutex));
}
Foi útil?

Solução

Se você é apenas loops paralelos e usando o VS 2008, sugiro olhar para o OpenMP. Se você está usando o Visual Studio 2010 beta 1, eu sugeriria olhar para o Biblioteca de padrões paralelos, particularmente o "Paralelo para" / "paralelo para cada" APIs ou o "Grupo de tarefas Classe porque isso provavelmente fará o que você está tentando fazer, apenas com menos código.

Em relação à sua pergunta sobre desempenho, aqui realmente depende. Você precisará analisar quanto trabalho está programando durante suas iterações e quais são os custos. WaitFormultiLeObjects pode ser bastante caro se você acertar muito e seu trabalho é pequeno, e é por isso que sugiro usar uma implementação já criada. Você também precisa garantir que não esteja executando no modo de depuração, sob um depurador e que as próprias tarefas não estejam bloqueando em um bloqueio, E/S ou alocação de memória, e você não está atingindo o compartilhamento falso. Cada um deles tem o potencial de destruir a escalabilidade.

Eu sugiro olhar para isso sob um perfilador como xperf O F1 Profiler no Visual Studio 2010 Beta 1 (possui 2 novos modos de simultaneidade que ajudam a ver a contenção) ou o Vtune da Intel.

Você também pode compartilhar o código que está executando nas tarefas, para que as pessoas possam ter uma idéia melhor do que você está fazendo, porque a resposta que eu sempre recebo com problemas de desempenho é a primeira perfilou isso. "

Boa sorte

-Rick

Outras dicas

Isso me parece um padrão de consumidor de produtor, que pode ser implícito com dois semáforos, um guardando o transbordamento da fila e o outro a fila vazia.

Você pode encontrar alguns detalhes aqui.

Sim, WaitForMultipleObjects é muito caro. Se seus trabalhos forem pequenos, a sobrecarga de sincronização começará a sobrecarregar o custo de realmente fazer o trabalho, como você está vendo.

Uma maneira de corrigir isso é agrupar vários trabalhos em um: se você conseguir um trabalho "pequeno" (no entanto, avaliar essas coisas), guarde-o em algum lugar até que você tenha pequenos trabalhos suficientes juntos para fazer um trabalho de tamanho razoável. Em seguida, envie todos eles para um tópico de trabalhador para processamento.

Como alternativa, em vez de usar a sinalização, você pode usar uma fila de vários leitores de vários leitores para armazenar seus trabalhos. Nesse modelo, cada tópico de trabalhador tenta pegar empregos na fila. Quando encontra um, faz o trabalho; Caso contrário, dorme por um curto período, depois acorda e tenta novamente. Isso diminuirá a sobrecarga por tarefa, mas seus tópicos adotarão a CPU mesmo quando não houver trabalho a ser feito. Tudo depende da natureza exata do problema.

Cuidado, você ainda está pedindo um próximo emprego depois que o Endignal for emitido.

for( ;; ) {
    // wait for one of the signals
    waitResult = WaitForMultipleObjects(2, signals, false, INFINITE);
    if( waitResult - WAIT_OBJECT_0 != 0 )
        return;
    //....
}

Não deveria ser tão caro, mas se o seu trabalho o leva quase nenhuma hora em tudo, então a sobrecarga das linhas e sincronização de objetos irá tornar-se significativa.Pools de threads como este trabalho muito melhor para mais trabalhos de processamento ou para aqueles que usam um monte de e / s em vez de recursos da CPU.Se você está no limite da CPU quando o processamento de uma tarefa, assegurar que você tenha apenas 1 thread por CPU.

Pode haver outros problemas, como getNextJob obter seus dados para o processo?Se há uma grande quantidade de dados a copiar e, em seguida, aumentou significativamente a sobrecarga novamente.

Gostaria de optimizar permitindo que cada thread continuar puxando trabalhos fora da fila, até que a fila esteja vazia.dessa forma, você pode passar de uma centena de trabalhos para o pool de threads e a sincronização de objetos serão usados apenas uma vez para ativar a thread.Eu também armazenar os trabalhos em uma fila e passar um ponteiro, de referência ou de iterador para-los para o segmento, em vez de copiar os dados.

A troca de contexto entre os roscas também pode ser cara. Em alguns casos, é interessante desenvolver uma estrutura que você pode usar para processar seus trabalhos sequencialmente com um thread ou com vários threads. Dessa forma, você pode ter o melhor dos dois mundos.

A propósito, qual é exatamente sua pergunta? Serei capaz de responder com mais precisão com uma pergunta mais precisa :)

EDITAR:

A parte dos eventos pode consumir mais do que o seu processamento em alguns casos, mas não deve ser tão caro, a menos que seu processamento seja realmente rápido. Nesse caso, alternar entre thredas também é caro, daí a minha resposta em primeira parte sobre fazer as coisas sequenciais ...

Você deve procurar gargalos de sincronização entre threads. Você pode rastrear os tempos de espera de tópicos para começar ...

EDIT: Depois de mais dicas ...

Se eu acho que corretamente, seu problema é usar com eficiência todos os núcleos/processadores do computador para parralizar algum processamento essencialmente sequencial.

Pegue que você tenha 4 núcleos e 10000 loops para calcular como no seu exemplo (em um comentário). Você disse que precisa esperar que os 4 threads terminem antes de continuar. Em seguida, você pode simplificar seu processo de sincronização. Você só precisa dar aos seus quatro threads thor nd, nth+1, nth+2, nth+3 loops, aguarde os quatro threads concluírem e depois continuar. Você deve usar um encontro ou barreira (um mecanismo de sincronização que aguarde a conclusão de n threads). Impulso tem esse mecanismo. Você pode procurar a implementação do Windows em busca de eficiência. Seu pool de threads não é realmente adequado para a tarefa. A busca por um tópico disponível em uma seção crítica é o que está matando seu tempo de CPU. Não é a parte do evento.

Parece que toda essa coisa do evento (junto com a sincronização entre os threads usando seções críticas) é muito caro!

"Caro" é um termo relativo. Os jatos são caros? São carros? Ou bicicletas ... sapatos ...?

Nesse caso, a questão é: os eventos "caros" são em relação ao tempo necessário para que a função de trabalho seja executada? Ajudaria a publicar algumas figuras absolutas: quanto tempo o processo leva quando "desmathado"? São meses ou alguns femtossegundos?

O que acontece com o tempo à medida que você aumenta o tamanho do ThreadPool? Experimente um tamanho de piscina de 1, depois 2, então 4, etc.

Além disso, como você teve alguns problemas com o ThreadPools aqui no passado, sugiro algum depuração para contar o número de vezes em que sua função de thread é realmente invocada ... isso corresponde ao que você espera?

Escolhendo uma figura do ar (sem saber nada sobre o seu sistema de destino e assumindo que você não está fazendo nada 'enorme' no código que você não mostrou), eu esperaria a "sobrecarga do evento" de cada "trabalho" a ser medido em microssegundos. Talvez cem ou mais. Se o tempo necessário para executar o algoritmo na função de trabalho não for significativamente maior do que desta vez, é provável que seus threads custem tempo em vez de salvá -lo.

Já que você diz que é Muito de Mais lento em paralelo que a execução seqüencial, presumo que seu tempo de processamento para as iterações internas de 2500 loop seja minúsculo (na faixa de poucos micro segundos). Depois, não há muito que você possa fazer, exceto revisar seu algoritmo para dividir pedaços maiores de precesso; O OpenMP não ajudará e todas as outras técnicas de sincronização também não ajudarão, porque elas se baseiam fundamentalmente em eventos (os loops de spin não se qualificam).

Por outro lado, se o seu tempo de processamento das iterações de 2500 loop for maior que 100 micro segundos (nos PCs atuais), você poderá estar enfrentando limitações do hardware. Se o seu processamento usar muita largura de banda de memória, dividi -lo em quatro processadores não fornecerá mais largura de banda, ele realmente lhe dará menos por causa de colisões. Você também pode estar enfrentando problemas de ciclismo de cache, onde cada uma das suas 1000 iterações principais irá lavar e recarregar o cache dos 4 núcleos. Depois, não há uma solução e, dependendo do seu hardware de destino, pode não haver nenhum.

Como mencionado anteriormente, a quantidade de sobrecarga adicionada pelo encadeamento depende da quantidade relativa de tempo gasto para fazer os "trabalhos" que você definiu. Portanto, é importante encontrar um equilíbrio no tamanho dos pedaços de trabalho que minimize o número de peças, mas não deixa os processadores ociosos aguardando a conclusão do último grupo de cálculos.

Sua abordagem de codificação aumentou a quantidade de trabalho aéreo, procurando ativamente um tópico ocioso para fornecer um novo trabalho. O sistema operacional já está acompanhando isso e fazê -lo com muito mais eficiência. Além disso, a sua função ThreadPool :: addjob () pode achar que todos os threads estão em uso e não conseguem delegar o trabalho. Mas não fornece nenhum código de retorno relacionado a esse problema. Se você não está verificando essa condição de alguma forma e não está percebendo erros nos resultados, significa que sempre existem processadores ociosos. Eu sugeriria que a reorganização do código para que Addjob () faça o que é nomeado - adiciona apenas um trabalho (sem encontrar ou mesmo cuidar de quem faz o trabalho) enquanto cada tópico de trabalhador obtém um novo trabalho quando terminar com seu trabalho existente.

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