Thread Pool em C++ usando SDL2

Depois que fiz o processamento do conjunto de mandelbrot, fiquei com a ideia de fazer o mesmo  código executando em mais de uma thread de modo a alcançar maior desempenho; e se possível renderizar o conjunto a mais de 24 quadros por segundo (FPS), depois de muita pesquisa descobri que a melhor forma de trabalhar com Threads era o utilizando-se de um Thread Pool, essa técnica é muito eficiente pois evita o overhead de criação de threads quando se tem um número grande de tarefas a serem executadas, pesquisei por alguma biblioteca Open Source mas todas que eu encontrei usavam a pthread (posix thread), e no caso estou no Windows usando CodeBlocks e MinGW, a solução foi implementar o pattern usando a biblioteca SDL2.
Thread Pool Pattern

Thread Pool Pattern

Thread Pool Pattern fonte Wikipedia [1]

ThreadPool.h

<code>#ifndef THREADPOOL_H
#define THREADPOOL_H
#include
typedef enum TaskState {
Initialized,
Running,
Finished
};

class Task
{
public:

virtual void *DoWork() = 0;

};

class ThreadPool
{
public:
/** Default constructor */
ThreadPool();

/** Initialize with number of Threads */
void init(int _numThreads);

/** Destroy the pool */
void destroy();

/** Add task to Queque */
void addWork(Task *task);

/** Default destructor */
virtual ~ThreadPool();
int numThreads;
std::queue&lt;Task*&gt; workQueque;
};

extern ThreadPool poolThread;
#endif // THREADPOOL_H

ThreadPool.cpp

#include "ThreadPool.h"
#include "SDL.h"
#include "SDL_thread.h"
#include "SDL_timer.h"
#include <stdio.h>
#define MAX_THREADS 64

//Pointer to Threads
SDL_Thread *threads[MAX_THREADS];

//Mutex for trhead safe sync
SDL_mutex *quequeMutex;
SDL_mutex *workerMutex;
SDL_mutex *mutexBarrier;

//Signals
SDL_cond *condHaveWork;
SDL_cond *condBarrier;

extern ThreadPool poolThread;

//This main function, while thread pool alive (detroy not called)
//this function monitor task queque, to process it

void *worker(void *param)
{
    while (1)
    {

        //Try to get Task
        Task *task = NULL;
        SDL_LockMutex(quequeMutex);
        //have task in queque
        int num = poolThread.workQueque.size();
        if (num > 0)
        {

            task = poolThread.workQueque.front();
            poolThread.workQueque.pop();
        }
        else
        {

        }
        SDL_UnlockMutex(quequeMutex);

        //if have task, run it
        if (task != NULL)
        {
            task->DoWork();
        }
         //if no have job thread in IDLE
        if (num == 0)
        {

            SDL_LockMutex(workerMutex);
            SDL_CondWait(condHaveWork,workerMutex);
            SDL_UnlockMutex(workerMutex);
        }
    }
}
ThreadPool::ThreadPool()
{
    //ctor

}

void ThreadPool::init(int _numThreads)
{
    //setup mutex
    quequeMutex = SDL_CreateMutex();
    workerMutex = SDL_CreateMutex();
    condHaveWork = SDL_CreateCond();
    mutexBarrier = SDL_CreateMutex();
    condBarrier = SDL_CreateCond();

    //setup number of threads
    numThreads = _numThreads;

    //initialize all threads
    for(int i = 0; i < numThreads; i++)
    {
        char threadName[128];
        sprintf(threadName,"Thread %d",i);
        //Create thread
        threads[i] = SDL_CreateThread(worker,threadName,&poolThread);
        if (NULL == threads[i])
        {
            printf ("Falha ao Criar thread \n");
        }

    }
}

//Destroy the thread pool
void ThreadPool::destroy()
{
    for (int i = 0; i < numThreads; i++)
    {
        //Kill threads
        if (threads[i] != NULL)
            SDL_DetachThread(threads[i]);
    }

    //Destroy Mutex and Signals
    SDL_DestroyMutex(quequeMutex);
    SDL_DestroyMutex(workerMutex);
    SDL_DestroyMutex(mutexBarrier);
    SDL_DestroyCond(condBarrier);
    SDL_DestroyCond(condHaveWork);
}

//Enqueque work to pool
void ThreadPool::addWork(Task *task)
{
    //Enqueque work
    SDL_LockMutex(quequeMutex);
    workQueque.push(task);
    int size = workQueque.size();
    SDL_UnlockMutex(quequeMutex);

    //Emit signal of new work
    SDL_LockMutex(workerMutex);
    SDL_CondSignal(condHaveWork);
    SDL_UnlockMutex(workerMutex);

}

ThreadPool::~ThreadPool()
{

}

Um ponto de atenção é com a linha onde está declarado o ThreadPool

extern ThreadPool poolThread;

Isso acontece pelo fato de eu não ter deixado a função void *worker (void *params), dentro da classe o contexto em que eu estou usando é bem definido então não achei necessário usar mais que um ThreadPool.

Toda tarefa a ser executado no Thread Pool, deve ser uma classe que herdada da classe Task .

//Task of compute mandelbrot slice
//this is sent to ThreadPool
class SliceComputeTask : Task
{
public:
    double step; // is ScreenHeight / NumberOfCores
    unsigned index; //Actual "Slice"
    int state; //Used for wait all threads done

    virtual void *DoWork()
    {
        state = Running;

        //Offset of slice
        double y1 = step*index;
        for(unsigned y=y1; y<y1+step; ++y)
        {
            double c_im = MaxIm - y*Im_factor;
            for(unsigned x=0; x<SCREEN_WIDTH; ++x)
            {
                double c_re = MinRe + x*Re_factor;

                double Z_re = c_re, Z_im = c_im;
                bool isInside = true;
                //offset of pixelBuffer
                const unsigned offset = ( SCREEN_WIDTH * 4 * y ) + x * 4;
                //offset of Color Palette
                unsigned pOffset = 0;
                unsigned n;

                for(n=0; n<MaxIterations; ++n)
                {
                    double Z_re2 = Z_re*Z_re, Z_im2 = Z_im*Z_im;

                    if(Z_re2 + Z_im2 > 4)
                    {
                        isInside = false;
                        break;
                    }
                    else
                    {
                        //Draw Pixel According with color table
                        pixelArray[offset] = colorPal[n];
                        pixelArray[offset + 1] = colorPal[n+1];
                        pixelArray[offset + 2] = colorPal[n+2];
                        pixelArray[offset + 3] = SDL_ALPHA_OPAQUE;

                    }
                    Z_im = 2*Z_re*Z_im + c_im;
                    Z_re = Z_re2 - Z_im2 + c_re;
                }
                if(isInside)
                {
                    //Draw pixel
                    pixelArray[offset] = 0;
                    pixelArray[offset + 1] = 255;
                    pixelArray[offset + 2] = 255;
                    pixelArray[offset + 3] = SDL_ALPHA_OPAQUE;

                }
            }
        }
        //Task finished
        state = Finished;

    };
};

 

Antes de ser usado o Thread Pool dever ser iniciado

    //Initialize thread Pool

    //Get CPU Count
    CPUCount = SDL_GetCPUCount();

    //initialize Threads
    poolThread.init(CPUCount);

    //allocate task's in memory
    sliceTasks = new SliceComputeTask[64];

Agora finalmente podemos enviar tarefas para o threadPool, a função draw_mandelbrot() é chamada no loop principal, eu dividi o  conjunto em fatias e cada fatia será processado por uma thread (CPUCount)

//mandelbrot main function
void draw_madelbrot()
{
    //calculate slice size
    int TaskSize = CPUCount;
    double step = SCREEN_HEIGHT / CPUCount;

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

        //Setup task
        sliceTasks[i].step = step;
        sliceTasks[i].index = i;
        sliceTasks[i].state = Initialized;

        //Queque task
        poolThread.addWork((Task*)&sliceTasks[i]);
    }

    //wait all task finish
    int k = 0;
    bool frameDone = false;
    while(frameDone == false)
    {
        if (k == TaskSize)
            frameDone = true;

        if (sliceTasks[k].state == Finished)
            k++;

    }

}

Resultado com 2 Threads.

Mandelbrot sendo computado com duas threads

Mandelbrot sendo computado com duas threads

Reultado com 4 Threads.

Conjunto de mandelbrot sendo computado com 4 threads

Conjunto de mandelbrot sendo computado com 4 threads

Resultado com 8 Threads (SDL_GetCPUCount())

Conjunto de mandelbrot sendo computado com oito threads.

Conjunto de mandelbrot sendo computado com oito threads.

Podemos ver um aumento do número de FPS e do uso da CPU conforme aumentamos o número de threads, a CPU só não fica em 100% pelo fato de algumas regiões do conjunto demorarem mais para serem computadas do que outras e o uso do vsync (espera de todas as thread terminarem para atualizar a tela); acredito que se eu dividir o conjunto em uma grid de 8×8, conseguiria alcançar uma taxa de mais de 24 FPS, mas isso fica para uma próxima.

O código fonte completo pode ser encontrado no github.

https://github.com/psilogroup/Mandelbrot-Mult-Thread

Espero que goste xD

 

[1] https://en.wikipedia.org/wiki/Thread_pool

Posted in C++
One comment on “Thread Pool em C++ usando SDL2
  1. Gabriel Moreira says:

    Teste de comentários

Leave a Reply