Variable globale en C

a marqué ce sujet comme résolu.

Bonjour,

Il s'agit d'un petit programme en C que je fais tourner sur ma Raspberry Pi. Rien de bien utile, je m'amuse simplement à faire clignoter des LEDs ^^ Dans ce programme, j'ai dû utiliser une variable globale. Je sais ce n'est pas dérangeant, mais du moment que cette pratique n'est pas forcément recommandable, je me demandais s'il n'y avait pas une autre façon de procéder. Après tout, je fais également cela pour apprendre (tout en ayant un projet amusant :) )

Le programme permet de faire clignoter des LEDs à une certaine fréquence. Fréquence qui est réglée par un client qui se connecte, via le système de socket, au "serveur" : la Raspberry Pi.

Voici le fonctionnement général :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void server() {
    // Crée le socket etc
    // [...] 
    // Crée un thread qui va exécuter la fonction (appelons la led() par exemple) faisant clignoter les LEDs 
    // [...]
    while( (csock = accept(sock, (sockaddr *) &csin, &crecsize)) ) {
        // [...]
        recv(csock, buffer, 32, 0);
        ledDataHandler(buffer);
    }
}

Il est important de créer un thread pour faire clignoter les LEDs, car la fonction ne peut se permettre d'attendre de recevoir des données (tels que la fréquence de clignotement). Ensuite, je me contente d'envoyer les données reçues à la fonction qui va s'occuper de traiter ces données.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    // Ma variable globale (structure contenant la fréquence de chaque LED).
    LEDS leds = {0}

    void * led(void *pVoid) {
        // S'occupe de faire clignoter les LEDs
    }

    void ledDataHandler(char *data) {
        // Modifie la variable leds en fonction du paramètre data.
    }

Ici, j'ai la fonction led() qui va lire les fréquences contenues dans la variable leds. Et ledDataHandler() qui va modifier ces fréquences en fonction des donnés reçues par le serveur.

J'ai pensé à créer la variable leds dans la fonction du serveur puis passer sa référence aux deux fonctions mais… Je ne trouve pas cela très pratique. En plus, je ne trouve pas cela logique de créer ma variable leds dans la fonction qui reçoit les informations du serveur.

Je pourrai également traiter directement les données dans la fonction leds mais étant donné qu'il y a une boucle, ce traitement se ferait à chaque itération… Même quand les données ne changent pas.

Est ce que vous voyez une autre façon de procéder ?

Merci :)

+0 -0

En soit les variables globales sont certes déconseillées, mais parfois elles sont presque indispensables ou très utiles. Typiquement, s'il y a une variable / structure de donnée que tu utilises dans tout le code, le mettre en global n'est pas forcément une mauvaise idée. Mais il faut faire attention aux effets de bord et bien peser le pour et le contre.

Concernant ton cas, je n'arrive pas à identifier en quoi ta variable globale est si importante pour un cas d'usage aussi élémentaire. Pire, de ce que tu racontes, tu as l'air de créer des threads avec cette variable globale partagée : si tu ne mets pas de mutexs pour protéger sa lecture et son écriture, tu t'exposes à des incohérences et conflits dans l'exploitation des données.

+2 -0

Hello :)

Merci d'avoir répondu.

Concernant ton cas, je n'arrive pas à identifier en quoi ta variable globale est si importante pour un cas d'usage aussi élémentaire.

Oui, il y a sans doute une alternative valable. Mais je ne la vois pas ^^ L'autre idée que j'ai en tête, c'est transmettre la variable de la fonction qui gère le serveur à celles qui gèrent les LEDs, comme cela :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void server() {
    [...]
    LEDS leds = {0};

    // Crée le thread qui va gérer les LEDS
    pthread_t ledThread;
    pthread_create( &ledThread , NULL ,  led , (void *) &leds);

    // On attend que quelqu'un se connecte
    while( (csock = accept(sock, (sockaddr *) &csin, &crecsize)) ) {
        // Lance le gestionnaire de connexion
        connectionHandler(csock, &leds);
    }
}

int connectionHandler(int sock, LEDS *leds) {
    [...]
    // Attend les messages du client
    while ((read_size = recv(sock, buffer, 32, 0)) != 0) {
        [...]
        // Lance le gestionnaire des LEDs
        ledDataHandler(buffer, leds);
    }
}
1
2
3
4
5
6
7
8
9
void * led(void *pVoid) {
    LEDS *leds = (LEDS *) pVoid;
    // Lit les fréquences des LEDs et les fait clignoter
    [...]
}

void ledDataHandler(char *data, LEDS *leds) {
    // Modifie la variable leds en fonction des données...
}

La variable passe de fonction en fonction… Ce n'est pas très pratique non ?

si tu ne mets pas de mutexs pour protéger sa lecture et son écriture, tu t'exposes à des incohérences et conflits dans l'exploitation des données.

Oui bien sûr, j'ai volontairement simplifié le code que j'ai posté :)

Mon post peut sembler un peu inutile, mais si je poste ce n'est pas pour avoir un programme qui fonctionne. J'essaye plutôt d'apprendre à faire mieux :)

+0 -0

Salut.

Ah oui, bonne idée ! J'avais encore jamais cette méthode, merci :)

yata

Nan mais ça reste une variable globale, y'a juste du sucre syntaxique collé dessus.

La variable passe de fonction en fonction… Ce n'est pas très pratique non ?

yata

C'est pas une question d'être pratique, c'est une question de bonnes pratiques. Une variable globale, c'est de l'autorité ambiante : avec une variable globale, une fonction peut accéder librement à des choses hors de la « bulle » des éléments qu'elle a créé ou qu'on lui a donné en paramètre, et ça c'est mal.

  • Ça bousille le raisonnement local. Si quelque chose peut être modifié ailleurs sans préavis, ça complique énormément la logique du programme, que ce soit pour le programmeur qui le lit ou pour faire de la preuve formelle ;
  • Ça empêche la réutilisation de code, car on utilise un élément précis et pas un autre, au lieu d'un élément choisi par l'appelant. Dans le même genre, ça empêche la paramétrisation. On pourrait préférer qu'une fonction utilise un élément passé en paramètre qui réponde à des critères précis plutôt qu'un élément imposé (exemple typique : un système de logging) ;
  • Il faut regarder le code source pour savoir quels sont les éléments utilisés, alors qu'avec des paramètres de fonction c'est auto documenté ;
  • C'est souvent révélateur de mauvaise conception (« Allez je fous une globale pour éviter de réfléchir ») ;
  • Ça créé des risques de data race dans un environnement concurrent.

Bref, ne fais pas ça, il y a toujours une solution alternative. Ici, je te conseille de rester en monothread, avec une structure de ce genre :

1
2
3
4
5
6
7
tant que ...
  si nouvelles données réseau
    lire données réseau
    mettre les LEDs à jour
  fin si
  clignoter
fin tant que

Si tu veux vraiment rester en multithread, tu devrais séparer complètement le réseau et les LEDs et transmettre les données réseau au thread LED via un canal thread-safe (une simple file SPSC est suffisante ici). Le thread réseau enverrait les données reçues au thread LEDs, qui vérifierait périodiquement les messages envoyés par le thread réseau, mettrait à jour les LEDs et les ferait clignoter.

Typiquement, s'il y a une variable / structure de donnée que tu utilises dans tout le code, le mettre en global n'est pas forcément une mauvaise idée.

Renault

Je dirais plutôt qu'une donnée utilisée dans plusieurs fonctions/classes/acteurs/whatever n'ayant pas la même responsabilité est un signe flagrant d'erreur de conception. Il n'y a aucune raison qui justifie que des modules indépendants utilisent obligatoirement la même donnée sans que l'appelant n'ait son mot à dire, pour les raisons évoquées au dessus.

+2 -0

Bref, ne fais pas ça, il y a toujours une solution alternative.

[…]

Je dirais plutôt qu'une donnée utilisée dans plusieurs fonctions/classes/acteurs/whatever n'ayant pas la même responsabilité est un signe flagrant d'erreur de conception. Il n'y a aucune raison qui justifie que des modules indépendants utilisent obligatoirement la même donnée sans que l'appelant n'ait son mot à dire, pour les raisons évoquées au dessus.

Mouais, je ne sais pas dans quoi tu programmes dans la vie, mais personnellement quand j'entends des trucs « il faut toujours faire comme ça », « il y a toujours moyen d'éviter ceci ou cela ». Non, c'est faux. Je t'assure, programme au niveau noyau voire bibliothèques élémentaires et tu verras que les variables globales sont utilisées et à bon escient, soit parce qu'il est impossible de faire autrement (sisi, cela arrive), soit la complexité ajoutée est telle que tu perds tout le bénéfice du concept.

D'ailleurs, dès que tu fais des programmes parallélisé tu auras des données partagés avec probablement des mutexs globaux pour les protéger. C'est parfois difficiles ou impossible de faire sans.

Après, je ne le nie pas, ici, ce n'est pas indispensable et ce genre de chose est à éviter au maximum. Mais il y en a des situations où l'appliquer n'est pas un mal en soit…

+1 -0

Utiliser des variables globales n'est pas forcément une mauvaise idée, surtout lorsque l'on parle de programmation concurrente. Néanmoins, comme cela a déjà été dit précédemment, ce n'est pas forcément hyper-propre, et ça tourne vite au vinaigre si on ne maîtrise pas ce que l'on fait.

La norme POSIX propose malgré tout une alternative à cette solution "bricolée" : la mémoire partagée (shared memory dans la langue de Shakespeare). Je ne vais pas faire un cours ici, mais je t'invite à aller jeter un œil à la documentation à ce propos.

Salut,

Ça bousille le raisonnement local. Si quelque chose peut être modifié ailleurs sans préavis, ça complique énormément la logique du programme, que ce soit pour le programmeur qui le lit ou pour faire de la preuve formelle ;

Ça empêche la réutilisation de code, car on utilise un élément précis et pas un autre, au lieu d'un élément choisi par l'appelant. Dans le même genre, ça empêche la paramétrisation. On pourrait préférer qu'une fonction utilise un élément passé en paramètre qui réponde à des critères précis plutôt qu'un élément imposé (exemple typique : un système de logging) ;

Praetonus

Ouais, je pense qu'il ne faut pas abuser non plus… Je rappelle quand même la mise en situation de l'auteur :

Il s'agit d'un petit programme en C que je fais tourner sur ma Raspberry Pi.

yata

Il n'est pas en train de mettre au point une bibliothèque ou de réaliser des calculs de hautes précisions, il s'agit de faire fonctionner une LED. Alors, d'accord, l'abus de variables globales nuit à la santé (mentale) des programmeurs, mais bon, j'ai envie de dire qu'ici, on s'en fout un peu. :-°

+0 -0

Mouais, je ne sais pas dans quoi tu programmes dans la vie, mais personnellement quand j'entends des trucs « il faut toujours faire comme ça », « il y a toujours moyen d'éviter ceci ou cela ». Non, c'est faux. Je t'assure, programme au niveau noyau voire bibliothèques élémentaires et tu verras que les variables globales sont utilisées et à bon escient, soit parce qu'il est impossible de faire autrement (sisi, cela arrive), soit la complexité ajoutée est telle que tu perds tout le bénéfice du concept.

Oui, mais ce sont des cas extrêmes loin de représenter la majorité. Une fois un certain niveau d'abstraction dépassé, les variables globales ne sont absolument pas nécessaires, à condition de respecter le principe de responsabilité unique. D'ailleurs, les langages sans globales s'en sortent très bien.

Mais je suis d'accord, toujours est une exagération.

D'ailleurs, dès que tu fais des programmes parallélisé tu auras des données partagés avec probablement des mutexs globaux pour les protéger. C'est parfois difficiles ou impossible de faire sans.

Je dirais au contraire que les globales sont encore plus à proscrire dans un programme concurrent. Avoir des globales protégées par des mutexes globaux, c'est le meilleur moyen pour se retrouver avec des goulots d'étranglement et des risques de deadlock un peu partout. L'idéal est de séparer entièrement les unités parallèles, qui communiqueraient par des files de messages (dont les implémentations canoniques sont lock-free dans tous les cas, et wait-free dans les meilleurs cas). Le meilleur exemple pour illustrer ça est l'actor model.

Il n'est pas en train de mettre au point une bibliothèque ou de réaliser des calculs de hautes précisions, il s'agit de faire fonctionner une LED. Alors, d'accord, l'abus de variables globales nuit à la santé (mentale) des programmeurs, mais bon, j'ai envie de dire qu'ici, on s'en fout un peu. :-°

Certes, mais autant prendre les bonnes habitudes tout de suite sur les cas simples.

+2 -0

Je dirais au contraire que les globales sont encore plus à proscrire dans un programme concurrent. Avoir des globales protégées par des mutexes globaux, c'est le meilleur moyen pour se retrouver avec des goulots d'étranglement et des risques de deadlock un peu partout. L'idéal est de séparer entièrement les unités parallèles, qui communiqueraient par des files de messages (dont les implémentations canoniques sont lock-free dans tous les cas, et wait-free dans les meilleurs cas). Le meilleur exemple pour illustrer ça est l'actor model.

Comme je te le dis, parfois, tu n'as pas vraiment le choix. ;) Bien entendu qu'en règle général, il y a moyen de s'en passer, et heureusement.

+0 -0

Je dirais au contraire que les globales sont encore plus à proscrire dans un programme concurrent. Avoir des globales protégées par des mutexes globaux, c'est le meilleur moyen pour se retrouver avec des goulots d'étranglement et des risques de deadlock un peu partout.

Praetonus

Déjà entendu parler de réseaux de Petri ? C'est un excellent moyen de faire des démonstrations sur les programmes concurrents.

Merci pour vos réponses :)

Si tu veux vraiment rester en multithread, tu devrais séparer complètement le réseau et les LEDs et transmettre les données réseau au thread LED via un canal thread-safe (une simple file SPSC est suffisante ici). Le thread réseau enverrait les données reçues au thread LEDs, qui vérifierait périodiquement les messages envoyés par le thread réseau, mettrait à jour les LEDs et les ferait clignoter.

Je pense essayer le monothread et le multithread, je pourrai ensuite comparer :) J'ai l'intention de faire évoluer le projet, en remplacent notamment les LEDs par des moteurs. Je trouve également intéressant de rajouter un petit accéléromètre pour voir le déplacement effectué. Je devrai recevoir le Raspberry Pi Sense HAT dans quelques semaines :) Intuitivement, je dirai que plus je peux faire d’itération, plus la valeur de l’accéléromètre sera précise (reste à voir s'il n'y a pas une limitation matériel). Donc si je peux éviter de vérifier à chaque itération si j'ai reçu des nouvelles données, ça pourrait aider. Après il faut également voir quelles ressources sont utilisés en parallèle par la fonction recv() dans un autre thread ^^ J'avoue n'avoir aucune expérience là dedans, donc je peux être complètement à coté, mais cela reste intéressant d'essayer toutes sortes d'alternatives :) Bon je m'éloigne un peu du sujet principal… Je vais déjà essayer d'avoir un code potable à ce stade. Par contre, je n'avais encore jamais entendu parler de "file SPSC" ^^ J'ai fait quelques recherches, mais j'ai l'impression de rien trouver de précis. Tu aurais un lien vers un bon article (en anglais c'est bon) ?

La norme POSIX propose malgré tout une alternative à cette solution "bricolée" : la mémoire partagée (shared memory dans la langue de Shakespeare). Je ne vais pas faire un cours ici, mais je t'invite à aller jeter un œil à la documentation à ce propos.

Je n'ai pas le temps de regarder ça ce soir, mais je vais voir dans les prochains jours. Merci :)

Certes, mais autant prendre les bonnes habitudes tout de suite sur les cas simples.

Oui ^^ Je compte par la suite faire des études dans l'informatique, donc si je tombe sur un cas similaire je saurai tout de suite quoi faire, et je pourrai me concentrer sur autre chose :)

+1 -0

Alors, c'était un excellent moyen. On a établit d'autres techniques entre temps (dieu merci).

Ksass`Peuk

Oui, bon, si les programmes de mon école étaient à jour, ça se saurait depuis le temps. :P

Quelles méthodes, par exemple, je dois avouer que ça m'intéresse, et que ça reste dans le thème du thread.

Quelles méthodes, par exemple, je dois avouer que ça m'intéresse, et que ça reste dans le thème du thread.

Richou D. Degenne

C'est pas les méthodes qui manquent, rien qu'avec les triplets de Hoare, pour ne citer que le plus connu, on va avoir : celle qu'a énoncé Hoare en 1976, la concurrent separation logic, le rely-guarantee, et les dérivées de ces machins là, sans compter la tripotée de trucs moins connus. Après on va avoir également les techniques de linéarisation, le bounded model-checking, de l'interprétation abstraite pour les programmes parallèles, les logiques temporelles et notamment la temporal logic of actions de Lamport.

J'ai une liste longue comme le bras de papiers fondateurs de différentes techniques majeures pour la preuve de programmes parallèles et je suis loin d'avoir tout lu.

Par contre, je n'avais encore jamais entendu parler de "file SPSC" ^^ J'ai fait quelques recherches, mais j'ai l'impression de rien trouver de précis. Tu aurais un lien vers un bon article (en anglais c'est bon) ?

Il s'agit d'une file thread-safe et lock-free, c'est à dire sans utilisation de mutex. SPSC signifie Single Producer-Single Consumer, la structure est donc sure tant qu'elle n'est utilisée que par deux threads en simultané et pour des opérations différentes. Autrement dit, si plusieurs threads essaient de push en même temps, ça casse, idem pour plusieurs pop en même temps. Par contre, si on a un thread sur push et un sur pop, c'est bon. Des structures permettant l'accès concurrent de plus d'acteurs existent (MPSC, SPMC et MPMC) et sont beaucoup plus complexes.

Au niveau de l'implémentation, ça passe par l'utilisation d'opérations atomiques, qui ont été standardisées avec C11. Pour une introduction au sujet, je conseille les talk de H. Sutter, atomic<> Weapons et Lock-Free Programming. Le premier parle des concepts fondamentaux du modèle mémoire et des atomiques et le second aborde les aspects pratiques de la question. Les exemples sont en C++, mais les concepts du multithread de C11 et C++11 sont identiques.

Je te propose également une implémentation basique et commentée de la file, pour y voir plus clair.
La file est implémentée en terme de buffer circulaire. Il s'agit d'un tableau de taille fixe sur lequel on conserve un indice d'écriture et un indice de lecture. Le push incrémente l'indice d'écriture et le pop incrémente l'indice de lecture. Les indices sont remis à zéro lorsqu'ils atteignent la fin du tableau.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#include <assert.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdlib.h>

typedef struct
{
    // Cette implémentation stocke des int, mais le concept est généralisable
    // à n'importe quelle structure de données
    int* data;
    _Atomic size_t write_index; // Variable atomique
    uint8_t padding[80]; // Pour éviter que les indices soient sur la même ligne de cache
    _Atomic size_t read_index;
    size_t max_size;
} SPSC_queue;

// Créé une file de taille size
// Renvoie une file de taille nulle en cas d'erreur
SPSC_queue queue_create(size_t size)
{
    SPSC_queue ret = {malloc(sizeof(int) * size), (size_t)0, {0}, (size_t)0, size};
    if (!ret.data)
        ret.max_size = 0;
    return ret;
}

void queue_destroy(SPSC_queue* queue)
{
    if (!queue)
        return;
    free(queue->data);
}

// Calcul des indices en prenant en compte la remise à 0
size_t queue_next_index(size_t idx, size_t max)
{
    if (max == 0)
        return 0;

    return (idx + 1) % max;
}

// Ajoute un élément à la file
// Renvoie false si la file est pleine, true sinon
bool queue_push(SPSC_queue* queue, int value)
{
    assert(queue);

    if (queue->max_size == 0)
        return false;

    // Le thread push est le seul à modifier l'indice d'écriture, on peut donc utiliser relaxed
    size_t write_index = atomic_load_explicit(&queue->write_index, memory_order_relaxed);
    size_t next = queue_next_index(write_index, queue->max_size);

    // Les instructions entre un acquire et un release forment une section critique (cf talk de H. Sutter)
    if (next == atomic_load_explicit(&queue->read_index, memory_order_acquire))
        return false; // File pleine
    queue->data[write_index] = value; // On stocke la valeur
    atomic_store_explicit(&queue->write_index, next, memory_order_release); // Puis on met à jour l'indice
    return true;
}

// Retire un élément de la file et stocke celui-ci dans output
// Renvoie false si la file est vide, true sinon
bool queue_pop(SPSC_queue* queue, int* output)
{
    assert(queue && output);

    if (queue->max_size == 0)
        return false;

    size_t write_index = atomic_load_explicit(&queue->write_index, memory_order_acquire);
    // De même, le thread pop est le seul à modifier l'indice de lecture, on peut donc utiliser relaxed
    size_t read_index = atomic_load_explicit(&queue->read_index, memory_order_relaxed);

    if (write_index == read_index)
        return false; // File vide
    *output = queue->data[read_index]; // On lit l'élément
    size_t next = queue_next_index(read_index, queue->max_size);
    atomic_store_explicit(&queue->read_index, next, memory_order_release); // Puis on met à jour l'indice
    return true;
}

bool queue_empty(SPSC_queue* queue)
{
    assert(queue);

    // On ne fait que lire des variables atomiques, on peut donc utiliser relaxed
    return atomic_load_explicit(&queue->write_index, memory_order_relaxed) ==
           atomic_load_explicit(&queue->read_index, memory_order_relaxed);
}

+1 -0

Merci beaucoup pour les liens, et le code que tu as envoyé :)

J'ai brièvement essayé le code : tout fonctionne. Maintenant, il ne me reste plus qu'à le comprendre ^^

Cela faisait un moment que je voulais m'intéresser aux variables atomiques, mais je devais à jour gcc (par défaut il y a gcc 4.8 sur Ubuntu) ^^ Voilà qui est désormais fait, et je vais m'y mettre dès ce week-end :)

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte