📖 Origem: Real-Time Design Patterns: Robust Scalable Architecture for Real-Time Systems – Bruce Powel Douglass
Resumo
O Pool Allocation Pattern é um padrão que gerencia a memória de forma eficiente ao pré-alocar blocos de tamanho fixo para reutilização. Ele resolve problemas comuns da alocação dinâmica, como fragmentação e imprevisibilidade no tempo de acesso, tornando-se essencial para sistemas embarcados e de tempo real. Esse padrão melhora a eficiência do uso da RAM ao garantir que blocos de memória sejam reutilizados conforme necessário, sem necessidade de alocações dinâmicas constantes.
Problema a ser resolvido
A alocação dinâmica de memória (malloc()
e free()
) pode levar à fragmentação e latências imprevisíveis, o que é inaceitável para sistemas embarcados de tempo real. Além disso, quando a memória se esgota ou está altamente fragmentada, pode ocorrer falha na alocação de novos blocos, resultando em comportamento inesperado do sistema.
Outro problema comum é a sobrecarga de gerenciamento de memória, onde a alocação e desalocação frequente de pequenos blocos gera um overhead significativo. Isso reduz a eficiência do sistema e pode impactar o tempo de resposta em aplicações críticas. Em sistemas embarcados com recursos limitados, é crucial garantir que a memória seja usada de forma previsível e eficiente.
O Pool Allocation Pattern resolve esses problemas ao manter um conjunto fixo de blocos de memória pré-alocados, permitindo que o sistema reutilize esses blocos de maneira eficiente. Isso elimina a necessidade de alocação e desalocação dinâmica, garantindo tempos de resposta constantes e previsíveis.
Estrutura do Padrão
A estrutura desse padrão se baseia em um gerenciador de pool de memória, que mantém uma lista de blocos livres prontos para serem usados. Cada bloco tem um tamanho fixo e pode ser alocado rapidamente sem a necessidade de recorrer ao heap. A fábrica de objetos pode ser usada para criar e inicializar os blocos, enquanto um gerenciador de heap dimensionado garante que os blocos sejam usados de maneira eficiente.
Os dados são armazenados no segmento de memória, garantindo que a RAM seja utilizada de forma previsível. O acesso ao pool pode ser gerenciado por mecanismos como filas FIFO (First In, First Out) ou LIFO (Last In, First Out), dependendo das necessidades da aplicação.
Papéis de Colaboração
- Cliente: Solicita e libera blocos de memória conforme necessário.
- Lista de Blocos Livres: Mantém o controle dos blocos de memória disponíveis para reutilização.
- Gerenciador de Pool: Supervisiona a alocação e liberação dos blocos, garantindo que a memória seja usada eficientemente.
- Segmento de Memória: Armazena fisicamente os blocos alocados.
- Fábrica de Objetos: Pode ser usada para criar instâncias de objetos que utilizam a alocação por pool.
- Heap Dimensionado: Garante que os blocos tenham um tamanho fixo e que a memória seja utilizada de maneira eficiente.
Consequências
O Pool Allocation Pattern traz diversas vantagens para sistemas embarcados. Ele reduz drasticamente o tempo de alocação de memória, pois os blocos já estão pré-alocados e prontos para uso. Isso melhora o tempo de resposta do sistema e evita problemas de fragmentação. Além disso, ao reutilizar blocos já existentes, o padrão reduz a necessidade de operações de gerenciamento de heap, economizando processamento.
Entretanto, esse padrão pode levar ao desperdício de memória se o número de blocos pré-alocados for maior do que o necessário. Se a quantidade de blocos for insuficiente, pode ser necessário um mecanismo de fallback para lidar com situações onde não há memória disponível. Além disso, a implementação deve ser bem planejada para evitar vazamentos de memória devido a blocos que não são liberados corretamente.
Estratégias de Implementação
Para implementar o Pool Allocation Pattern, um array de blocos de memória é criado no momento da inicialização do sistema. Um gerenciador de pool mantém uma lista encadeada de blocos disponíveis, permitindo alocação e liberação rápidas. Em sistemas que exigem eficiência máxima, um buffer circular pode ser utilizado para gerenciar os blocos de forma ainda mais eficiente.
A implementação pode incluir verificações para evitar acessos indevidos a blocos não alocados. Além disso, em sistemas multitarefa, mutexes ou semáforos podem ser usados para garantir que múltiplas tarefas acessem o pool de maneira segura. Em casos onde há necessidade de realocação dinâmica, pode-se combinar esse padrão com o Fixed-Sized Buffer Pattern, garantindo que cada bloco tenha um tamanho fixo e previsível.
Padrões Relacionados
O Pool Allocation Pattern está diretamente relacionado a outros padrões de gerenciamento de memória e sincronização:
- Fixed-Sized Buffer Pattern: Ambos evitam alocação dinâmica e melhoram a previsibilidade do uso da memória.
- Message Queuing Pattern: Pode ser usado em conjunto para gerenciar filas de mensagens sem consumir memória excessiva.
- Garbage Compactor Pattern: Pode ser útil quando há necessidade de reorganizar a memória para evitar fragmentação.
- Critical Section Pattern: Pode ser necessário para evitar acesso concorrente a blocos do pool.
Modelo de Amostragem (Exemplo de Código)
Aqui está um exemplo de implementação do Pool Allocation Pattern em C:
#include <stdio.h>
#include <stdint.h>
#define POOL_SIZE 1000 // Número máximo de objetos TempData
// Estrutura de dados de temperatura
typedef struct {
int id;
float temperature;
} TempData;
// Estrutura para o Pool de Recursos
typedef struct {
TempData pool[POOL_SIZE]; // Array de objetos
uint8_t allocated[POOL_SIZE]; // Flags para indicar quais objetos estão em uso
} TempDataPool;
// Inicializa o pool de dados
void initPool(TempDataPool* pool) {
for (int i = 0; i < POOL_SIZE; i++) {
pool->allocated[i] = 0; // Nenhum objeto está alocado inicialmente
}
}
// Aloca um objeto TempData do pool
TempData* allocateTempData(TempDataPool* pool) {
for (int i = 0; i < POOL_SIZE; i++) {
if (!pool->allocated[i]) { // Se não estiver em uso
pool->allocated[i] = 1; // Marca como alocado
return &pool->pool[i];
}
}
return NULL; // Pool esgotado
}
// Libera um objeto TempData e o devolve ao pool
void releaseTempData(TempDataPool* pool, TempData* data) {
int index = data - pool->pool; // Calcula o índice do objeto
if (index >= 0 && index < POOL_SIZE) {
pool->allocated[index] = 0; // Marca como livre
}
}
// Simulação de sensores e histórico
void simulateUsage() {
TempDataPool tempPool;
initPool(&tempPool);
TempData* temp1 = allocateTempData(&tempPool);
if (temp1) {
temp1->id = 1;
temp1->temperature = 23.5;
printf("TempData %d alocado: %.2f°C\n", temp1->id, temp1->temperature);
}
TempData* temp2 = allocateTempData(&tempPool);
if (temp2) {
temp2->id = 2;
temp2->temperature = 24.0;
printf("TempData %d alocado: %.2f°C\n", temp2->id, temp2->temperature);
}
releaseTempData(&tempPool, temp1);
printf("TempData %d liberado\n", temp1->id);
TempData* temp3 = allocateTempData(&tempPool);
if (temp3) {
temp3->id = 3;
temp3->temperature = 22.8;
printf("TempData %d alocado: %.2f°C\n", temp3->id, temp3->temperature);
}
}
int main() {
simulateUsage();
return 0;
}
Abaixo segue o diagrama Estrutural:

A seguir o diagrama de sequência: