No desenvolvimento de software concorrrente, garantir o acesso seguro a estruturas de dados compartilhadas é crucial. A implementação de uma fila segura (thread-safe queue) no projeto thr/thread-pool serve como componente cantral para o agendamento de tarefas, utilizando os recursos de concorrência do C++11 para equilibrar integridade de dados e eficiência.
O Papel de uma Fila Concorrente
Ambientes multithreading estão sujeitos a condições de corrida quando múltiplas threads acessam simultaneamente uma mesma estrutura de dados. A classe SafeQueue encapsula uma fila padrão e um mutex, proporcionando:
- Proteção contra acessos simultâneos a dados compartilhados.
- Sincronização entre threads produtoras e consumidoras.
- Gerenciamento automático de recursos por meio do padrão RAII.
Detalhamento da Implementação
Componentes Internos
A estrutura básica do SafeQueue é composta por:
- Um container
std::queue<T>para armazenamento dos elementos. - Um
std::mutexpara serializar o acesso às operações de fila.
Essa abordagem minimiza o tempo de posse do lock, otimizando o desempenho.
Operações Fundamentasi
Inserção de Elementos:
void inserir(T& item) {
std::unique_lock<std::mutex> guarda(lock_);
dados_.push(item);
}
Utiliza std::unique_lock para garantir a liberação automática do mutex, mesmo em caso de exceções.
Extração de Elementos:
bool extrair(T& alvo) {
std::unique_lock<std::mutex> guarda(lock_);
if (dados_.empty()) {
return false;
}
alvo = std::move(dados_.front());
dados_.pop();
return true;
}
A operação move o elemento evitando cópias desnecessárias e indica se a extração foi bem-sucedida.
Consultas de Estado:
bool esta_vazia() {
std::unique_lock<std::mutex> guarda(lock_);
return dados_.empty();
}
std::size_t tamanho() {
std::unique_lock<std::mutex> guarda(lock_);
return dados_.size();
}
Garantem leituras consistentes mesmo em ambiente concorrente.
Integração com o Pool de Threads
No ThreadPool, a SafeQueue atua como buffer de tarefas:
class ThreadPool {
SafeQueue<std::function<void()>> fila_tarefas_;
// ... demais membros
public:
template<typename F, typename... Args>
auto enviar(F&& func, Args&&... argumentos) {
auto tarefa_empacotada = [func, argumentos...]() { func(argumentos...); };
fila_tarefas_.inserir(tarefa_empacotada);
sinal_.notify_one();
// retorna um std::future
}
};
As threads de trabalho executam um loop consumidor:
void operator()() {
while (!pool_.encerrando) {
std::function<void()> tarefa;
{
std::unique_lock<std::mutex> guarda(pool_.mutex_);
pool_.sinal_.wait(guarda, [&]() {
return !pool_.fila_tarefas_.esta_vazia() || pool_.encerrando;
});
pool_.fila_tarefas_.extrair(tarefa);
}
if (tarefa) tarefa();
}
}
A variável de condição permite que as threads durmam de forma eficiente até que haja trabalho disponível.
Exemplo Prático de Utilização
#include "ThreadPool.h"
void processar(int x) { /* ... */ }
int main() {
ThreadPool pool(4); // 4 threads
for (int i = 0; i < 100; ++i) {
pool.enviar(processar, i);
}
pool.aguardar(); // espera todas as tarefas concluírem
return 0;
}
Considerações de Projeto
A implementação do SafeQueue exemplifica boas práticas em C++ concorrente:
- Cercas de Memória Implícitas: O mutex fornece garantias de visibilidade entre threads.
- Prevenção de Starvation: O uso de
notify_oneem vez denotify_allpode reduzir o thundering herd. - Design para Exceção-Safety: O uso de RAII assegura liberação correta de recursos.
Pontos de atenção ao adotar este modelo incluem a granularidade do lock e a possibilidade de utilizar filas sem lock para cenários de alto desempenho.