Sempre deparo com necessidade de fazer um Pattern Singleton mas como seria em C ? Existe algumas pontos importantes e que temos que tomar cuidado, vou apresentar formas de fazer o Singleton usando C, a forma “Not Thread Safe” e “Thread Safe”.
Este Artigo é uma abordagem totalmente didática para que possamos entender como poderíamos manter um estado atômico de u ma instância ou variável global em C enquanto utilizamos concorrência usando threads.
Em C temos um poderoso recurso que é a utilização de threads para fazermos execuções simultâneas e quando usamos este recurso tudo tem que ser repensado para trabalhar com concorrência, isso afeta diretamente todo o escopo de nosso código, temos que pensar e escrever códigos da forma Assíncrona. Um bom exemplo de possíveis problemas usando concorrência é a utilização de variáveis globais. Devido as threads nossa implementação do Pattern Singleton irá ter que ser escrito para aceitar o uso de concorrência e neste artigo iremos implementar algumas possibilidades e aplicar boas práticas de como escrever nosso código para esta abordagem.
Temos que tentar sempre que possível escrever nosso código usando boas práticas de programação, para tentar mitigar os possíveis bugs sigilosos que podem ocorrer em tempo de execução do seu programa escrito em C.
O que é Pattern Singleton?
A transcrição do que é um Pattern Singleton seria: “Singleton é um padrão de projeto de software. Este padrão garante a existência de apenas uma instância de uma classe, mantendo um ponto global de acesso ao seu objeto”
O Pattern Singleton é uma característica proveniente do paradigma orientado a objeto, então como iremos implementar em C se o mesmo não possui suporte a OO ?
Para responder esta pergunta temos que entender que a programação Orientada a Objeto é um conceito um padrão e pode ser implementando em qualquer linguagem de programação mesmo sendo de outros paradigmas. É claro que o nível de abstração e dificuldade torna-se uma tarefa árdua aumentando muito o nível de complexidade do código dependendo da linguagem, nossa intenção é puramente didática com intuito de entendermos melhor o cenário proposto quando falamos em pattern singleton.
Podemos imaginar uma implementação do Pattern Singleton em C, como já sabemos que o OO é somente um conceito, então conseguiríamos implementar OO usando C. Em C singletons são variáveis estáticas com algum encapsulamento. Podemos usar variáveis estáticas globais ou estáticas locais para implementar nosso singleton em C, em nosso exemplo vamos implementar uma variável estática local. No entanto, as variáveis globais geralmente têm muitos malefícios associados a esta abordagem, e acredite elas realmente causam males, então o quanto antes aprendermos sobre isto melhor, Quando falamos em concorrência ou simultaneidade o problema das variáveis globais aumentam exponencialmente tudo vira um caos, neste cenário a forma de escrever o código e pensar diferente e temos.
O exemplo abaixo é uma implementação de um singleton em C, queremos manter uma instância atômica de nossa conexão com banco de dados, sabemos o quão tão isso é custoso, o ideal seria fazer somente uma conexão para a mesma base e manter isso em memória ou cache.
/*
* Example Singleton C
* @package main
* @author @jeffotoni
* @size 10/09/2018
*/
#include <stdio.h>
int *Connect() {
// variavel static local
static int conn_pg = 0; // nosso ojbeto connexao
// existe connexao ?
if (conn_pg == 0) {
// Db.Connect
conn_pg = 5454; // atribuindo conexao postgres
}
// conn_pg++; // o valor ser atribuido sempre
// mantendo o valor, somente se for static
// retorna objeto
return &conn_pg;
}
int main() {
int Db = *Connect();
printf("Connect em C: %d\n", Db);
printf("Connect em C: %d\n", *Connect());
printf("Connect em C: %d\n", *Connect());
return 0;
}
Criamos uma função para retornar nossa referência de memória da nossa instância ou objeto. Nosso objeto será atribuído a um valor uma única vez na chamada da função por que nas próximas chamadas irá sempre retornar somente sua referência sem precisar atribuir novamente o valor. Em uma aplicação síncrona, isto já era suficiente para uma boa execução do programa, porém se fizemos threads em C e tornar o programa assíncrono, com concorrência entre as threads teremos um grande problema na nossa abordagem e nossa instância poderia bugar em tempo de execução.
Temos como melhorar um pouco mais nosso Singleton em C, vamos encapsular nossa variável estática local usando struct e vamos criar uma struct para “Conn” e encapsular nosso objeto “conn_pag”.
/*
* Example Singleton C
* @package main
* @author @jeffotoni
* @size 10/09/2018
*/
#include <stdlib.h>
#include <stdio.h>
struct Conn
{
int conn_pg;
};
struct Conn* Connect()
{
static struct Conn *instance = NULL;
// entra somente
// uma unica vez
if(instance == NULL)
{
instance = malloc(sizeof(*instance));
instance->conn_pg = 34498;
}
return instance;
};
int main() {
printf("Connect em C: %d\n", Connect()->conn_pg);
printf("Connect em C: %d\n", Connect()->conn_pg);
return 0;
}
Em C usamos a função malloc para alocarmos dinamicamente nosso tipo “Conn” na memória para que possamos criar uma referência e acessa-lo de forma segura. O que temos que entender é que usamos struct para acessar nosso “conn_pag” e para fazer a chamada de nossa função um ponteiro de instância. Como relatado acima se implementar threads neste código teremos grandes problemas, teríamos que tratar os blocos e variáveis com mutex ou pthread, e neste cenário as coisas iriam complicar um pouco mas teríamos que garantir “Thread Safe”.
Vamos agora colocar nosso código usando threads, em C iremos usar a lib pthread.h para mostrar como ficaria nosso exemplo:
/*
* Example Singleton C
* @package main
* @author @jeffotoni
* @size 10/09/2018
*/
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
struct Conn
{
int conn_pg;
int thread;
};
struct Conn* Connect()
{
static struct Conn *instance = NULL;
// entra somente
// uma unica vez
if(instance == NULL)
{
instance = malloc(sizeof(*instance));
instance->conn_pg = 34498;
}
return instance;
};
void *PConn(void *tmp){
struct Conn *v = (struct Conn *) tmp;
// aguarde ...
sleep(rand()%10)+1;
printf("Oi eu sou a thread %d\n", v->thread);
printf("Connect em C: %d\n", Connect()->conn_pg);
}
int main() {
pthread_t linhas[10];
int execute,i;
struct Conn *conn;
//// thread 1
printf("Criei a thread 1\n");
conn = (struct Conn *) malloc(sizeof(struct Conn *));
conn->thread = 1;
execute = pthread_create(&linhas[0],NULL,PConn,(void *)conn);
//// thread 2
printf("Criei a thread 2\n");
conn = (struct Conn *) malloc(sizeof(struct Conn *));
conn->thread = 2;
execute = pthread_create(&linhas[1],NULL,PConn,(void *)conn);
// aguarde ...
sleep(rand()%10)+2;
pthread_exit(NULL);
return 0;
}
O exemplo acima para atribuições de valores em nossa struct teríamos que criar um pthread_mutex_lock e o pthread_mutex_unlock usando pthread_mutex_t junto com pthread_mutex_init p*ara termos a certeza e garantia da* exclusão mútua, evitando race conditions e deadlock’s.
Não podemos deixar que ocorra um race conditions e nem um Deadlock
Confira nosso exemplo abaixo mostrando exatamente a utilização do mutex em nosso singleton.
/*
* Example Singleton C
* @package main
* @author @jeffotoni
* @size 10/09/2018
*/
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
typedef struct Conn
{
int conn_pg;
int thread;
}CONNECT;
CONNECT dotstr;
pthread_mutex_t mutexconn;
struct Conn* Connect()
{
static struct Conn *instance = NULL;
// entra somente
// uma unica vez
if(instance == NULL)
{
instance = malloc(sizeof(*instance));
pthread_mutex_lock (&mutexconn);
instance->conn_pg = 34498;
pthread_mutex_unlock (&mutexconn);
pthread_exit((void*) 0);
}
return instance;
};
void *PConn(void *tmp){
struct Conn *v = (struct Conn *) tmp;
// aguarde ...
sleep(rand()%10)+1;
printf("Oi eu sou a thread %d\n", v->thread);
printf("Connect em C: %d\n", Connect()->conn_pg);
}
int main() {
pthread_t linhas[10];
int execute,i;
struct Conn *conn;
pthread_mutex_init(&mutexconn, NULL);
//// thread 1
printf("Criei a thread 1\n");
conn = (struct Conn *) malloc(sizeof(struct Conn *));
conn->thread = 1;
execute = pthread_create(&linhas[0],NULL,PConn,(void *)conn);
//// thread 2
printf("Criei a thread 2\n");
conn = (struct Conn *) malloc(sizeof(struct Conn *));
conn->thread = 2;
execute = pthread_create(&linhas[1],NULL,PConn,(void *)conn);
// aguarde ...
sleep(rand()%10)+2;
pthread_exit(NULL);
pthread_mutex_destroy(&mutexconn);
return 0;
}
Uma outra perspectiva quando usamos C:
Cada arquivo em um programa C é efetivamente uma classe singleton que é auto instanciada em tempo de execução e não pode ser subclassificada.
Variáveis estáticas globais são seus membros de classe particulares.
Global estática são públicos (apenas declare-os usando “extern” em algum arquivo de cabeçalho e poderá ser acessado por outros arquivos).
Funções estáticas são métodos privados
Funções não estáticas são as públicas.
Conclusão
Como percebemos acima, trabalhar com C usando “threads” não é uma tarefa simples, e proteger nossa instância e deixa-la atômica torna-se uma tarefa complexa usando C. Uma solução foi utilizar structs e typedef para definirmos e encapsularmos nossas variáveis protegendo o acesso a nossa struct quando acessado simultaneamente pela nossas threads. Foi usado mutex para garantirmos que não ocorra uma race conditions e evitando um deadlock em nosso programa.
O artigo é somente uma introdução e uma simples apresentação de como implementar o pattern singleton quando temos threads concorrendo entre si.
Códigos fontes e exemplos do artigo
github.com/jeffotoni/medium-posts/pattern-singleton/example-c
Espero ter ajudado a esclarecer um pouco as dúvidas sobre utilização do Pattern Singleton em C, utilizando variáveis globais, threads e mutex diversos outros pontos importantes e técnicos abordados no artigo.
No próximo post será mais um desafio bem interessante, que estou preparando… Quem curtiu e chegou até o final deixa uma palminha aí…
Top comments (0)