Twitter iPhone pliant OnePlus 11 PS5 Disney+ Orange Livebox Windows 11

Design d'une queue d'ordre

5 réponses
Avatar
Fabien LE LEZ
Bonjour,

Encore une fois se pose à moi un problème que je crois courant, mais
auquel je ne trouve pas de solution élégante.

J'ai un programme multithread :
- un thread s'occupe de traiter des données, de façon asynchrone ;
- l'autre s'occupe de l'interface utilisateur.

J'ai une classe "GestionDonnees", qui contient une queue d'ordres. Le
thread "interface utilisateur" ajoute des ordres à cette queue, et le
thread "traitement de données" recupère les ordres un par un, et les
traite.
Ainsi, seule la queue doit être protégée par un mutex.

Comme les ordres ont des données différentes, j'ai créé une hiérarchie
de classes :

class GestionDonnees
{
public:
class Ordre
{
public:
virtual void DoIt (GestionDonnees&) const= 0;
};
class Ordre_Ouvrir: public Ordre
{
public:
Ordre_Ouvrir (std::string const& nom_fichier);
virtual void DoIt (GestionDonnees&) const;
private:
std::string const nom_fichier;
};
class Ordre_Atteindre: public Ordre
{
public:
Ordre_Atteindre (size_t offset);
virtual void DoIt (GestionDonnees&) const;
private:
size_t const offset;
};
class Ordre_RemplaceChaines: public Ordre
{
public:
Ordre_RemplaceChaines (std::string const& a_remplacer,
std::string const& remplacement);
virtual void DoIt (GestionDonnees&) const;
private:
std::string const& a_remplacer;
std::string const& remplacement;
};

void AjouterOrdre (Ordre const*);

private:
std::queue<Ordre const*> ordres;

Fichier fichier_en_cours;
};

Jusque-là, tout va bien : le thread de gestion de données récupère le
premier ordre dans la queue, et appelle la fonction virtuelle
"DoIt()".
Le problème est dans l'implémentation de cette fonction, qui doit
accéder aux données privées de GestionDonnees.
Par exemple, Ordre_Ouvrir::DoIt() doit pouvoir accéder à
GestionDonnees::fichier_en_cours.

Je ne peux mettre aucun accesseur public (même pas un
GestionDonnees::Ouvrir()), car il ne doit pas être possible de
modifier un GestionDonnees autrement que par ce système d'ordres.

Je n'aime pas non plus l'idée de déclarer toutes les classes Ordre_*
amies de GestionDonnees.

Pour l'instant, j'en suis réduit à une bidouille : j'utilise l'idiome
pimpl, mais avec un pointeur public.

class GestionDonnees
{
public:
class Impl;
Impl* GetImpl() { return pimpl; }
private:
Impl* pimpl;
...

Ainsi, les Ordre_* peuvent appeler les fonctions de
GestionDonnees::Impl, mais le code client ne le peut pas, car il ne
voit pas la définition de cette classe Impl.

Si quelqu'un a une meilleure idée...

Merci d'avance.

5 réponses

Avatar
kanze
Fabien LE LEZ wrote:

Encore une fois se pose à moi un problème que je crois
courant, mais auquel je ne trouve pas de solution élégante.

J'ai un programme multithread :
- un thread s'occupe de traiter des données, de façon asynchrone ;
- l'autre s'occupe de l'interface utilisateur.


C'est un cas classique. J'utilise prèsqu'exclusivement une queue
de messages pour la communication du thread de GUI vers
l'autre ; l'interface de la queue de messages utilise les
auto_ptr, de façon à ce que le thread émitteur ne peut plus y
toucher une fois le message émis.

Dans l'autre direction, c'est plus délicat, parce que le thread
de GUI doit constamment être à l'attente des évenements GUI. Il
faut donc emballer le message dans un évenement GUI, qu'on
poste, d'une façon qui dépende de la charpente de GUI dont on se
sert.

J'ai une classe "GestionDonnees", qui contient une queue
d'ordres.


Est-ce bien ? J'aurais plutôt tendance à faire de la queue une
entité à part. Mais ça doit dépendre de l'application.

Le thread "interface utilisateur" ajoute des ordres à cette
queue, et le thread "traitement de données" recupère les
ordres un par un, et les traite.

Ainsi, seule la queue doit être protégée par un mutex.


Tout à fait. Surtout, les opérations sur la queue sont
extrèmement courtes. De cette façon, on ne bloque jamais le
thread GUI que pour des durées très courtes.

Comme les ordres ont des données différentes, j'ai créé une
hiérarchie de classes :

class GestionDonnees
{
public:
class Ordre
{
public:
virtual void DoIt (GestionDonnees&) const= 0;
};
class Ordre_Ouvrir: public Ordre
{
public:
Ordre_Ouvrir (std::string const& nom_fichier);
virtual void DoIt (GestionDonnees&) const;
private:
std::string const nom_fichier;
};
class Ordre_Atteindre: public Ordre
{
public:
Ordre_Atteindre (size_t offset);
virtual void DoIt (GestionDonnees&) const;
private:
size_t const offset;
};
class Ordre_RemplaceChaines: public Ordre
{
public:
Ordre_RemplaceChaines (std::string const& a_remplacer,
std::string const& remplacement);
virtual void DoIt (GestionDonnees&) const;
private:
std::string const& a_remplacer;
std::string const& remplacement;
};


Est-ce que tu ne crois pas que c'est un peu trop pour une seule
classe ? Est-ce que c'est normal que pour ajouter une classe
dérivée de plus, il faut modifier « GestionDonnees » ?

void AjouterOrdre (Ordre const*);

private:
std::queue<Ordre const*> ordres;

Fichier fichier_en_cours;
};

Jusque-là, tout va bien : le thread de gestion de données
récupère le premier ordre dans la queue, et appelle la
fonction virtuelle "DoIt()".


Je n'aime pas le nom:-). Ici, en fait, ce que tu passes, c'est
une fonctionnalité. Liée à des données, je veux bien, mais avant
tout, c'est une fonctionnalité -- c'est aussi le sens de DoIt en
anglais. Alors, pourquoi pas simplement « operator()() » ?
Sinon, traditionnellement, vue que c'est un ordre que je passe,
je choisissais un nom qui correspond à l'idée de ce qu'on fait
sur un ordre. Quelque chose comme « process() ». Mais ça
n'apporte pas réelement beaucoup plus d'informations que
« operator()() ». C'est plutôt un point de vue différent : dans
un cas, je passe une fonctionnalité à effectuer dans l'autre
thread, fonctionnalité qui en l'occurance tourne sur un ordre.
Dans l'autre, je passe un ordre, et c'est le thread recevant qui
décide ce qu'il y a à faire -- mais en l'occurance, il n'y a
qu'une chose qu'il peut faire, c-à-d traiter l'ordre.

Le problème est dans l'implémentation de cette fonction, qui
doit accéder aux données privées de GestionDonnees. Par
exemple, Ordre_Ouvrir::DoIt() doit pouvoir accéder à
GestionDonnees::fichier_en_cours.


Si d'autres classes doivent en accéder directement, il n'y a pas
de sens à ce que les données soit privées.

Je ne peux mettre aucun accesseur public (même pas un
GestionDonnees::Ouvrir()), car il ne doit pas être possible de
modifier un GestionDonnees autrement que par ce système
d'ordres.

Je n'aime pas non plus l'idée de déclarer toutes les classes
Ordre_* amies de GestionDonnees.


Pas nécessaire, réelement. Il suffit que la classe de base soit
amie, et qu'elle propose des fonctions protégées d'accès. (C'est
un peu la solution que j'ai utilisé dans des versions anciennes
de FieldArray, pour le délégué. L'interface proposée une
fonction virtuelle pure publique, split(), qui était appelée par
FieldArray lors de l'affectation d'une chaîne. L'« interface »
avait aussi une fonction non-virtuelle protégée « insertField »,
pour insérer les champs trouvés, et était l'ami de FieldArray,
de façon à pouvoir réelement faire l'insertion.)

Pour l'instant, j'en suis réduit à une bidouille : j'utilise
l'idiome pimpl, mais avec un pointeur public.

class GestionDonnees
{
public:
class Impl;
Impl* GetImpl() { return pimpl; }
private:
Impl* pimpl;
...

Ainsi, les Ordre_* peuvent appeler les fonctions de
GestionDonnees::Impl, mais le code client ne le peut pas, car
il ne voit pas la définition de cette classe Impl.


Je n'aime pas. Dans ce cas-ci ; il y a d'autres cas où je crois
que cette solution pourrait convenir.

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34

Avatar
Fabien LE LEZ
On 8 Jun 2006 00:33:39 -0700, James Kanze:

Dans l'autre direction, c'est plus délicat


Dans mon application, le thread "gestion de données" n'a aucun ordre à
donner au thread "GUI". Il se contente de modifier un "état" (qui est
une variable protégée par un mutex) ; le thread "GUI" va regarder cet
état quand bon lui semble. Éventuellement, jamais.

J'ai une classe "GestionDonnees", qui contient une queue
d'ordres.


Est-ce bien ? J'aurais plutôt tendance à faire de la queue une
entité à part. Mais ça doit dépendre de l'application.


La queue est un objet à part entière. Mais comme seules les fonctions
de GestionDonnees ont une bonne raison d'accéder à cette queue, elle
est naturellement membre privé de la classe.

Est-ce que tu ne crois pas que c'est un peu trop pour une seule
classe ? Est-ce que c'est normal que pour ajouter une classe
dérivée de plus, il faut modifier « GestionDonnees » ?


De toutes façons, si je rajoute une classe héritant de Ordre, ça veut
vraisemblablement dire que je dois modifier quelque chose dans
GestionDonnees. Mais c'est vrai que j'aurais pu sortir cette
hiérarchie.


Avatar
Fabien LE LEZ
On 8 Jun 2006 00:33:39 -0700, "kanze" :

Jusque-là, tout va bien : le thread de gestion de données
récupère le premier ordre dans la queue, et appelle la
fonction virtuelle "DoIt()".


Je n'aime pas le nom:-)


J'ai bien pensé à JustDoIt(), mais j'ai eu peur des ennuis de
copyright :-p

Alors, pourquoi pas simplement « operator()() » ?


Les opérateurs ont un gros intérêt : ils permettent d'alléger le code
client. Mais du coup, ce code est (AMHA) un peu moins explicite.
Comme je n'appelle cette fonction qu'à un seul endroit (la boucle qui
lit la liste des ordres), je préfère mettre les points sur les i et
utiliser une fonction nommée.

D'un autre côté, maintenant que j'y pense, tu as raison sur un point,
et par conséquent tort sur deux autres : les classes Ordre_* sont en
fait les fonctions publiques[*] de la classe GestionDonnees[**]. Par
conséquent, il me paraît tout naturel qu'elles soient membres de
GestionDonnees, et qu'elles aient accès à ses données privées.
Et du coup, "operator()()" serait effectivement le choix le plus
logique.

[*] Des foncteurs membres, en somme...
[**] même si la convention d'appel est différente.



Sinon, traditionnellement, vue que c'est un ordre que je passe,
je choisissais un nom qui correspond à l'idée de ce qu'on fait
sur un ordre. Quelque chose comme « process() ».


Barf...
"DoIt", c'est clair : ça veut dire "fais-le" (ou "mets-toi à bosser,
espèce de fainéant"[***]).
Le mot "process" a au moins deux sens : ça peut être un verbe, ou un
nom.


[***] Mais bon, "void MetsToiABosserEspeceDeFaineant()", ça risque
d'être un peu lourd. En plus, ça manque de ponctuation.


Avatar
kanze
Fabien LE LEZ wrote:
On 8 Jun 2006 00:33:39 -0700, James Kanze:

Dans l'autre direction, c'est plus délicat


Dans mon application, le thread "gestion de données" n'a aucun
ordre à donner au thread "GUI". Il se contente de modifier un
"état" (qui est une variable protégée par un mutex) ; le
thread "GUI" va regarder cet état quand bon lui semble.
Éventuellement, jamais.


Dans ton application, peut-être. Mais en général... ?

J'ai une classe "GestionDonnees", qui contient une queue
d'ordres.


Est-ce bien ? J'aurais plutôt tendance à faire de la queue
une entité à part. Mais ça doit dépendre de l'application.


La queue est un objet à part entière. Mais comme seules les
fonctions de GestionDonnees ont une bonne raison d'accéder à
cette queue, elle est naturellement membre privé de la classe.


D'accord. C'est que dans ton exemple, GestionDonnees contenait
un std::deque en guise de queue. Et un std::deque ne convient
pas, tout seul, comme queue entre deux processus. (Il pourrait
bien servir dans l'implémentation de la queue de messages,
évidemment.)

Est-ce que tu ne crois pas que c'est un peu trop pour une
seule classe ? Est-ce que c'est normal que pour ajouter une
classe dérivée de plus, il faut modifier « GestionDonnees » ?


De toutes façons, si je rajoute une classe héritant de Ordre,
ça veut vraisemblablement dire que je dois modifier quelque
chose dans GestionDonnees.


Réelement ? C'est à éviter, si possible.

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34



Avatar
kanze
Fabien LE LEZ wrote:
On 8 Jun 2006 00:33:39 -0700, "kanze" :

Jusque-là, tout va bien : le thread de gestion de données
récupère le premier ordre dans la queue, et appelle la
fonction virtuelle "DoIt()".


Je n'aime pas le nom:-)


J'ai bien pensé à JustDoIt(), mais j'ai eu peur des ennuis de
copyright :-p


Copyright, je ne crois pas. Marque déposée, peut-être (mais
seulement si tu l'utilisais pour faire de la pub de ton
programme).

Alors, pourquoi pas simplement « operator()() » ?


Les opérateurs ont un gros intérêt : ils permettent d'alléger
le code client. Mais du coup, ce code est (AMHA) un peu moins
explicite. Comme je n'appelle cette fonction qu'à un seul
endroit (la boucle qui lit la liste des ordres), je préfère
mettre les points sur les i et utiliser une fonction nommée.

D'un autre côté, maintenant que j'y pense, tu as raison sur un
point, et par conséquent tort sur deux autres : les classes
Ordre_* sont en fait les fonctions publiques[*] de la classe
GestionDonnees[**]. Par conséquent, il me paraît tout naturel
qu'elles soient membres de GestionDonnees, et qu'elles aient
accès à ses données privées. Et du coup, "operator()()" serait
effectivement le choix le plus logique.


C'était effectivement une partie de ma question. Est-ce
conceptuellement, les objets étaient des fonctions à exécuter
dans l'autre thread, ou est-ce qu'ils étaient des « ordres »,
dont le traitement (ou une partie du traitement) se faisait dans
un autre thread. Ce sont deux points de vue. Très souvent, les
deux sont valides. Mais il vaut mieux en choisir un, et y rester
fidèle.

Sinon, traditionnellement, vue que c'est un ordre que je passe,
je choisissais un nom qui correspond à l'idée de ce qu'on fait
sur un ordre. Quelque chose comme « process() ».


Barf...
"DoIt", c'est clair : ça veut dire "fais-le" (ou "mets-toi à bosser,
espèce de fainéant"[***]).


Mais fais quoi ? On ne « do » pas un ordre.

Le mot "process" a au moins deux sens : ça peut être un verbe,
ou un nom.


Sauf qu'en tant que nom de fonction, il ne peut être qu'un
verbe. En tant que nom, il ne pourrait être qu'un type, et
alors, il commencera par un majuscule. (Évidemment, tes
conventions de nommage ne sont pas forcément les mêmes que les
miennes. Alors, je ne sais pas en ce qui concerne le majuscule.
Mais la distinction nom/verbe doit y être en tout cas, ainsi
qu'un moyen qui distingue bien les types des autres choses.)

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34