Introduction

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.

Mise en place

Pour commencer, créez le canevas de l'extension dans le fichier synchronisation.svm_plugin en utilisant ce code :

PLUGIN synchro

DEFINE

Verrous

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.

Création de 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);
%}

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.

Verrouillage en lecture

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.

Verrouillage en écriture

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.

Evénements

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.

Adresses

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 :

  1. les adresses simples sans structure,
  2. les adresses avec structure, qui permettent d'identifier grâce au contenu de la structure qui est l'émetteur ou le récepteur d'un événement.

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.

Files d'événements

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.

Création

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.

Rejoindre et quitter

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);
%}

Echanger des événements

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 :

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.

Conclusion

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.