Ce didacticiel est destiné aux utilisateurs maîtrisant la programmation en C ou C++, ainsi que l'architecture de la Simple Virtual Machine.
Dans ce didacticiel, vous allez apprendre à utiliser les outils de synchronisation offerts par la machine virtuelle.
Le temps de lecture de ce didacticiel est estimé à 35 minutes.
Pour commencer, créez le canevas de l'extension dans le fichier synchronisation.svm_plugin en utilisant ce code :
PLUGIN synchro
DEFINE
Les verrous proposés par l'interface programmatique permettent une gestion plus fine des accès aux ressources que les verrous d'exclusion mutuelle habituels. En effet, ils permettent l'accès à une ressource soit à plusieurs lecteurs soit à un seul écrivain.
Modifiez le code de l'extension :
PLUGIN synchro
DEFINE
TYPE synchro.lock
%{
SVM_Lock _lock;
%}
delete default:
%{
VARIABLE_LOCAL(object->_lock);
%}
INSTRUCTION synchro.new_lock -> synchro.lock
%{
auto t = new type_lock;
t->_lock = ::svm_lock_new(svm);
VARIABLE_GLOBAL(t->_lock);
return NEW_PLUGIN(synchro,lock,t);
%}
La fonction de création de verrou est svm_lock_new
. Cette fonction ne crée pas de barrière de synchronisation, elle crée juste le verrou.
Le premier verrouillage possible est celui utilisé par une entité qui doit lire la ressource protégée par le verrou.
Modifiez le code de l'extension :
PLUGIN synchro
DEFINE
TYPE synchro.lock
%{
SVM_Lock _lock;
%}
delete default:
%{
VARIABLE_LOCAL(object->_lock);
%}
INSTRUCTION synchro.new_lock -> synchro.lock
%{
auto t = new type_lock;
t->_lock = ::svm_lock_new(svm);
VARIABLE_GLOBAL(t->_lock);
return NEW_PLUGIN(synchro,lock,t);
%}
TYPE synchro.read
%{
SVM_LockGuard_Read _read;
%}
delete default:
%{
VARIABLE_LOCAL(object->_read);
%}
WAITING INSTRUCTION synchro.lock_read synchro.lock -> synchro.read
%{
auto l = ARGV_PLUGIN(0,synchro,lock);
auto t = new type_read;
t->_read = ::svm_lock_readguard_new(svm,l->_lock,TRUE);
VARIABLE_GLOBAL(t->_read);
return NEW_PLUGIN(synchro,read,t);
%}
La fonction svm_lock_readguard_new
cette fois crée une barrière de synchronisation pour un lecteur. Une fois la variable Simple Virtual Machine de type SVM_LockGuard_Read
obtenue, la barrière de synchronisation est active jusqu'à ce que cette variable soit détruite.
De la même manière, un verrouillage pour un accès en écriture peut être obtenu depuis le verrou.
Modifiez le code de l'extension :
PLUGIN synchro
DEFINE
TYPE synchro.lock
%{
SVM_Lock _lock;
%}
delete default:
%{
VARIABLE_LOCAL(object->_lock);
%}
INSTRUCTION synchro.new_lock -> synchro.lock
%{
auto t = new type_lock;
t->_lock = ::svm_lock_new(svm);
VARIABLE_GLOBAL(t->_lock);
return NEW_PLUGIN(synchro,lock,t);
%}
TYPE synchro.read
%{
SVM_LockGuard_Read _read;
%}
delete default:
%{
VARIABLE_LOCAL(object->_read);
%}
WAITING INSTRUCTION synchro.lock_read synchro.lock -> synchro.read
%{
auto l = ARGV_PLUGIN(0,synchro,lock);
auto t = new type_read;
t->_read = ::svm_lock_readguard_new(svm,l->_lock,TRUE);
VARIABLE_GLOBAL(t->_read);
return NEW_PLUGIN(synchro,read,t);
%}
TYPE synchro.write
%{
SVM_LockGuard_Write _write;
%}
delete default:
%{
VARIABLE_LOCAL(object->_write);
%}
WAITING INSTRUCTION synchro.lock_write synchro.lock -> synchro.write
%{
auto l = ARGV_PLUGIN(0,synchro,lock);
auto t = new type_write;
t->_write = ::svm_lock_writeguard_new(svm,l->_lock,TRUE);
VARIABLE_GLOBAL(t->_write);
return NEW_PLUGIN(synchro,write,t);
%}
Cette fois, c'est la fonction svm_lock_writeguard_new
qui crée une barrière de synchronisation plus forte que celle de la lecture : tous les écrivains sont exclusifs !
Générez et compilez l'extension, puis créez un fichier exécutable avec ce code de test :
#!/usr/bin/env svm
LOG
LOCAL PLUGIN "svmpluginsynchro/libsvmsynchro.so"
PLUGIN "svmrun.so"
PLUGIN "svmint.so"
PROCESS "main"
CODE "main" INLINE
:memory (synchro.lock,STR)/p, synchro.write/w, INT/i
:synchro.new_lock -> &p
:synchro.lock_write @&p -> &w
0 -> &i
:label create
:int.print @&i -> (p/1)
:run.parallel_call $"lock" p @(p/1) SCHED=run.parallel PARAMS=SHARE
:shift &i
:goto create :when @&i IN &0*5
:shutdown
:symbol lock
:memory synchro.read/r, synchro.write/w, STR/s
@(P/1) -> &s
:synchro.lock_write @&P -> &w
@&s -> (P/1)
:run.trace @(P/1)
:run.sleep HARD 2
[ ] -> w
:synchro.lock_read @&P -> &r
:run.trace @(P/1)
:run.sleep HARD 2
[ ] -> r
:shutdown
END
END
Lorsque vous lancez cette application, vous devriez voir chaque processus écrire la valeur tour à tour avec des valeurs différentes, puis tous les processus écrirent la dernière valeur écrite en même temps : les écritures sont séquencielles et les lectures sont parallèles.
svm_lock_new
permet de créer un verrou.svm_lock_readguard_new
permet de verrouiller pour une lecture : les autres verrouillages en lecture ne seront pas bloqués, mais les verrouillages en écriture seront quant à eux bloqués.svm_lock_writeguard_new
permet de verrouiller pour une écriture : les autres verrouillages en lecture et en écriture seront bloqués.Les verrous deviennent rapidement limités lorsque les processus à synchroniser deviennent nombreux, et qu'ils doivent échanger des informations en plus de la synchronisation.
Pour pallier à ce défaut, l'interface programmatique propose un autre mécanisme de synchronisation beaucoup plus évolué : la synchronisation sur événements.
Dans le système d'événements, ce sont des adresses qui émettent ou réceptionnent les événements proprement dits.
Modifiez le code de l'extension :
PLUGIN synchro
DEFINE
TYPE synchro.address
%{
SVM_Event_Queue_Address _address;
%}
delete default:
%{
VARIABLE_LOCAL(object->_address);
%}
copy object:
%{
auto c = new type_address;
c->_address = object->_address;
VARIABLE_GLOBAL(c->_address);
return c;
%}
STRUCT synchro.address
%{
SVM_Value _value;
%}
delete default:
%{
VARIABLE_LOCAL(object->_value);
%}
INSTRUCTION synchro.new_address VALUE ? -> synchro.address
%{
auto a = new type_address;
if(argc==0)
{
a->_address = ::svm_event_address_new(svm);
}
else
{
SVM_Value v = ::svm_parameter_value_get(svm,argv[0]);
VARIABLE_GLOBAL(v);
auto s = new struct_address;
s->_value = v;
a->_address = ::svm_event_address_new_struct(svm,NEW_STRUCT(synchro,address,s));
}
::svm_variable_scope_set_shared(svm,a->_address);
VARIABLE_GLOBAL(a->_address);
return NEW_PLUGIN(synchro,address,a);
%}
Ici, deux types d'adresses peuvent être créées :
Une fois l'adresse créée, il est possible de déterminer si une adresse possède une structure avec la fonction svm_event_address_has_struct
et le cas échéant, de récupérer cette structure grâce à la fonction svm_event_address_get_struct
.
Notez également que le type contient l'adresse sous forme de variable Simple Virtual Machine partagée, pour permettre la copie du type contenant l'adresse.
Il est maintenant temps de s'intéresser au second concept important des événements : les files d'événements. Ce sont elles qui transmettent les événements entre adresses, dans la mesure où ces adresses sont rattachées à la file d'événements.
En premier lieu, il faut créer les files d'événements.
Modifiez le code de l'extension :
PLUGIN synchro
DEFINE
TYPE synchro.address
%{
SVM_Event_Queue_Address _address;
%}
delete default:
%{
VARIABLE_LOCAL(object->_address);
%}
copy object:
%{
auto c = new type_address;
c->_address = object->_address;
VARIABLE_GLOBAL(c->_address);
return c;
%}
STRUCT synchro.address
%{
SVM_Value _value;
%}
delete default:
%{
VARIABLE_LOCAL(object->_value);
%}
INSTRUCTION synchro.new_address VALUE ? -> synchro.address
%{
auto a = new type_address;
if(argc==0)
{
a->_address = ::svm_event_address_new(svm);
}
else
{
SVM_Value v = ::svm_parameter_value_get(svm,argv[0]);
VARIABLE_GLOBAL(v);
auto s = new struct_address;
s->_value = v;
a->_address = ::svm_event_address_new_struct(svm,NEW_STRUCT(synchro,address,s));
}
::svm_variable_scope_set_shared(svm,a->_address);
VARIABLE_GLOBAL(a->_address);
return NEW_PLUGIN(synchro,address,a);
%}
TYPE synchro.queue
%{
SVM_Event_Queue _queue;
%}
delete default:
%{
VARIABLE_LOCAL(object->_queue);
%}
copy object:
%{
auto c = new type_queue;
c->_queue = object->_queue;
VARIABLE_GLOBAL(c->_queue);
return c;
%}
INSTRUCTION synchro.new_queue -> synchro.queue
%{
auto t = new type_queue;
t->_queue = ::svm_event_queue_new(svm);
VARIABLE_GLOBAL(t->_queue);
::svm_variable_scope_set_shared(svm,t->_queue);
return NEW_PLUGIN(synchro,queue,t);
%}
Lorsqu'une file d'événements est créée, aucune adresse n'est rattachée à la file.
Pour qu'une adresse soit connue d'une file d'événements, il faut que l'adresse rejoigne la file d'événements.
Modifiez le code de l'extension :
PLUGIN synchro
DEFINE
TYPE synchro.address
%{
SVM_Event_Queue_Address _address;
%}
delete default:
%{
VARIABLE_LOCAL(object->_address);
%}
copy object:
%{
auto c = new type_address;
c->_address = object->_address;
VARIABLE_GLOBAL(c->_address);
return c;
%}
STRUCT synchro.address
%{
SVM_Value _value;
%}
delete default:
%{
VARIABLE_LOCAL(object->_value);
%}
INSTRUCTION synchro.new_address VALUE ? -> synchro.address
%{
auto a = new type_address;
if(argc==0)
{
a->_address = ::svm_event_address_new(svm);
}
else
{
SVM_Value v = ::svm_parameter_value_get(svm,argv[0]);
VARIABLE_GLOBAL(v);
auto s = new struct_address;
s->_value = v;
a->_address = ::svm_event_address_new_struct(svm,NEW_STRUCT(synchro,address,s));
}
::svm_variable_scope_set_shared(svm,a->_address);
VARIABLE_GLOBAL(a->_address);
return NEW_PLUGIN(synchro,address,a);
%}
TYPE synchro.queue
%{
SVM_Event_Queue _queue;
%}
delete default:
%{
VARIABLE_LOCAL(object->_queue);
%}
copy object:
%{
auto c = new type_queue;
c->_queue = object->_queue;
VARIABLE_GLOBAL(c->_queue);
return c;
%}
INSTRUCTION synchro.new_queue -> synchro.queue
%{
auto t = new type_queue;
t->_queue = ::svm_event_queue_new(svm);
VARIABLE_GLOBAL(t->_queue);
::svm_variable_scope_set_shared(svm,t->_queue);
return NEW_PLUGIN(synchro,queue,t);
%}
INSTRUCTION synchro.join synchro.queue <= synchro.address
%{
auto q = ARGV_PLUGIN(0,synchro,queue);
auto a = ARGV_PLUGIN(2,synchro,address);
::svm_event_queue_join(svm,q->_queue,a->_address);
%}
INSTRUCTION synchro.leave synchro.queue => synchro.address
%{
auto q = ARGV_PLUGIN(0,synchro,queue);
auto a = ARGV_PLUGIN(2,synchro,address);
::svm_event_queue_leave(svm,q->_queue,a->_address);
%}
Il ne reste plus qu'à envoyer et recevoir les événements.
Modifiez le code de l'extension :
PLUGIN synchro
DEFINE
TYPE synchro.address
%{
SVM_Event_Queue_Address _address;
%}
delete default:
%{
VARIABLE_LOCAL(object->_address);
%}
copy object:
%{
auto c = new type_address;
c->_address = object->_address;
VARIABLE_GLOBAL(c->_address);
return c;
%}
STRUCT synchro.address
%{
SVM_Value _value;
%}
delete default:
%{
VARIABLE_LOCAL(object->_value);
%}
INSTRUCTION synchro.new_address VALUE ? -> synchro.address
%{
auto a = new type_address;
if(argc==0)
{
a->_address = ::svm_event_address_new(svm);
}
else
{
SVM_Value v = ::svm_parameter_value_get(svm,argv[1]);
VARIABLE_GLOBAL(v);
auto s = new struct_address;
s->_value = v;
a->_address = ::svm_event_address_new_struct(svm,NEW_STRUCT(synchro,address,s));
}
::svm_variable_scope_set_shared(svm,a->_address);
VARIABLE_GLOBAL(a->_address);
return NEW_PLUGIN(synchro,address,a);
%}
TYPE synchro.queue
%{
SVM_Event_Queue _queue;
%}
delete default:
%{
VARIABLE_LOCAL(object->_queue);
%}
copy object:
%{
auto c = new type_queue;
c->_queue = object->_queue;
VARIABLE_GLOBAL(c->_queue);
return c;
%}
INSTRUCTION synchro.new_queue -> synchro.queue
%{
auto t = new type_queue;
t->_queue = ::svm_event_queue_new(svm);
VARIABLE_GLOBAL(t->_queue);
::svm_variable_scope_set_shared(svm,t->_queue);
return NEW_PLUGIN(synchro,queue,t);
%}
INSTRUCTION synchro.join synchro.queue <= synchro.address
%{
auto q = ARGV_PLUGIN(0,synchro,queue);
auto a = ARGV_PLUGIN(2,synchro,address);
::svm_event_queue_join(svm,q->_queue,a->_address);
%}
INSTRUCTION synchro.leave synchro.queue => synchro.address
%{
auto q = ARGV_PLUGIN(0,synchro,queue);
auto a = ARGV_PLUGIN(2,synchro,address);
::svm_event_queue_leave(svm,q->_queue,a->_address);
%}
STRUCT synchro.event
%{
SVM_Value _value;
%}
delete default:
%{
VARIABLE_LOCAL(object->_value);
%}
INSTRUCTION synchro.push synchro.queue { synchro.address => [ synchro.address 'ALL' ] } VALUE
%{
auto q = ARGV_PLUGIN(0,synchro,queue);
auto s = ARGV_PLUGIN(2,synchro,address);
type_address *d = nullptr;
if(::svm_parameter_type_is_value(svm,argv[4]))
{
d = ARGV_PLUGIN(4,synchro,address);
}
SVM_Value v = ::svm_parameter_value_get(svm,argv[6]);
auto e = new struct_event;
e->_value = v;
VARIABLE_GLOBAL(e->_value);
if(d)
{
::svm_event_queue_push(svm,q->_queue,d->_address,s->_address,NEW_STRUCT(synchro,event,e));
}
else
{
::svm_event_queue_broadcast(svm,q->_queue,s->_address,NEW_STRUCT(synchro,event,e));
}
%}
WAITING INSTRUCTION synchro.pull synchro.queue { 'ANY' => synchro.address } INT ? -> PTR ?
%{
auto q = ARGV_PLUGIN(0,synchro,queue);
auto d = ARGV_PLUGIN(3,synchro,address);
SVM_Value_Integer t = nullptr;
if(argc>6)
{
t = ::svm_parameter_value_get(svm,argv[5]);
}
SVM_Event_Queue_Address s = nullptr;
SVM_Structure e = nullptr;
SVM_Boolean b = TRUE;
::svm_process_interruptionnotification_enable(svm,CURRENT(process));
if(t)
{
b = ::svm_event_queue_check(svm,q->_queue,d->_address,&s,&e,t,TRUE);
}
else
{
b = ::svm_event_queue_pull(svm,q->_queue,d->_address,&s,&e,TRUE);
}
::svm_process_interruptionnotification_disable(svm,CURRENT(process));
if(b==FALSE)
{
return NEW_NULL_VALUE(pointer);
}
SVM_Value *tv = ::svm_value_array_new(svm,2);
VARIABLE_GLOBAL(s);
auto ra = new type_address { s };
tv[0] = NEW_PLUGIN(synchro,address,ra);
::svm_value_state_set_movable(svm,tv[0]);
auto ee = reinterpret_cast<struct_event*>(::svm_structure_get_internal(svm,CONST_PEP(synchro,event),e));
tv[1] = ::svm_value_copy(svm,ee->_value);
::svm_value_state_set_movable(svm,tv[1]);
SVM_Memory_Zone z = ::svm_memory_zone_new(svm);
::svm_memory_zone_append_external__raw(svm,z,CONST_PEP(synchro,address),1);
::svm_memory_zone_append_internal__raw(svm,z,::svm_value_type_get_internal(svm,tv[1]),1);
SVM_Value_Pointer p = ::svm_memory_allocate(svm,CURRENT(kernel),z);
::svm_memory_write_pointer(svm,CURRENT(kernel),p,tv);
return p;
%}
Cette fois, le code est moins simple à comprendre ! Prenons le temps de décrire ces deux nouvelles instructions :
INSTRUCTION synchro.push synchro.queue { synchro.address => [ synchro.address 'ALL' ] } VALUE
envoie une valeur sous forme d'événement dans la file d'événements :
svm_event_queue_push
),ALL
, l'événement est envoyé à toutes les adresses rattachées à la file d'événements (grâce à svm_event_queue_broadcast
),WAITING INSTRUCTION synchro.pull synchro.queue { 'ANY' => synchro.address } INT ? -> PTR ?
récupère une valeur sous forme d'événement depuis la file d'événements :
ANY
, mais un entier peut être ajouté à la fin des paramètres pour indiquer la durée maximale d'attente en millisecondes (en utilisant svm_event_queue_check
au lieu de svm_event_queue_pull
qui elle attend indéfinimment),FALSE
: ici, l'instruction renvoie un pointeur nul,WAITING
à cause des fonctions d'attente d'événement.Générez et compilez l'extension, puis créez un fichier exécutable avec ce code de test :
#!/usr/bin/env svm
LOG
LOCAL PLUGIN "svmpluginsynchro/libsvmsynchro.so"
PLUGIN "svmrun.so"
PLUGIN "svmint.so"
PLUGIN "svmstr.so"
PROCESS "main"
CODE "main" INLINE
:memory INT/i, STR/s, (INT, synchro.queue, synchro.address*5)/a
:synchro.new_queue -> (a/1)
2 -> &i
:label address
@&i -> &a
:shift -2 &a
:synchro.new_address @&a -> (a/@&i)
:synchro.join @(a/1) <= @(a/@&i)
:shift &i
:goto address :when @&i IN a
2 -> &i
:label process
@&i -> &a
:shift -2 &a
:int.print @&a -> &s
:run.parallel_call $"event" a @&s SCHED=run.parallel PARAMS=COPY
:shift &i
:goto process :when @&i IN a
:shutdown
:symbol event
:memory INT/n, INT/p, PTR/r, STR/e
SIZE P -> &p
:shift -2 &p
@&P -> &n
:shift &n
:int.mod @&n @&p -> &n
:int.print @&P -> &e
:str.join "event_" @&e -> &e
:synchro.push @(P/1) { @((P/@&P)+2) => @((P/@&n)+2) } @&e
:synchro.pull @(P/1) { ANY => @((P/@&P)+2) } -> &r
:shutdown :unless &r INITIALISED
:run.trace @(@&r/1)
:synchro.leave @(P/1) => @((P/@&P)+2)
:shutdown
END
END
Lancez cette application, et remarquez dans le résultat que chaque processus récupère une donnée forgée dans un autre processus : les files d'événements sont un outil très pratique pour réaliser simplement une communication inter-processus fiable et opérable.
svm_event_queue_new
.svm_event_queue_join
et svm_event_queue_leave
.svm_event_queue_push
(pour un envoi à une seule autre adresse de la file) ou svm_event_queue_broadcast
(pour un envoi à toutes les adresses de la file).svm_event_queue_pull
(pour une attente de durée indéterminée) ou svm_event_queue_check
(pour une attente à durée limitée).Vous venez de voir comment utiliser les outils de synchronisation depuis une extension.
L'usage des verrous est très orienté protection locale de donnée partagée et se maîtrise rapidement.
Celui des files d'événements est plus long à prendre en main, mais devient cruxial au sein d'une application impliquant plusieurs processus de traitement devant communiquer entre eux.
Dans les deux cas, les alternatives du système d'exploitation sont possibles, mais parfois au prix de la réactivité de l'application aux notifications processus.