Cette question est liée à ma question sur les implémentations de coroutine existantes en Java . Si, comme je le soupçonne, il s’avère qu’aucune implémentation complète des coroutines n’est actuellement disponible en Java, que faudrait-il pour les implémenter?
Comme je l'ai dit dans cette question, je connais les éléments suivants:
Je vais aborder chacune des lacunes de chacun.
Cette "solution" est pathologique. L'intérêt des coroutines est de éviter les frais généraux liés aux threads, au verrouillage, à la planification du noyau, etc. Leur mise en œuvre en termes de filetages à inclinaison complète avec des restrictions strictes supprime tous les avantages.
Cette solution est plus pratique, mais un peu difficile à mettre en œuvre. C’est à peu près la même chose que de sauter dans le langage Assembly pour les bibliothèques de coroutine en C (c’est le nombre d’entre elles qui fonctionne) avec l’avantage que vous n’avez qu’une architecture à craindre et à laquelle vous devez vous adapter.
Cela vous oblige également à n'exécuter votre code que sur des piles JVM entièrement compatibles (ce qui signifie, par exemple, pas d'Android), sauf si vous pouvez trouver le moyen de faire la même chose sur une pile non conforme. Si vous y parvenez, vous avez maintenant doublé la complexité de votre système et vos besoins en tests.
La Da Vinci Machine est intéressante pour l'expérimentation, mais comme ce n'est pas une JVM standard, ses fonctionnalités ne seront pas disponibles partout. En effet, je soupçonne que la plupart des environnements de production interdiraient spécifiquement l’utilisation de la machine Da Vinci. Ainsi, je pourrais l'utiliser pour faire des expériences intéressantes, mais pas pour le code que je m'attends à publier dans le monde réel.
Cela a également le problème ajouté similaire à la solution de manipulation de bytecode JVM ci-dessus: ne fonctionnera pas sur des piles alternatives (comme Android).
Cette solution rend le point de faire cela en Java du tout théorique. Chaque combinaison de processeur et de système d'exploitation nécessite des tests indépendants et constitue un point d'échec subtil potentiellement frustrant. Alternativement, bien sûr, je pouvais m'attacher entièrement à une plate-forme, mais cela rend également le point crucial de faire des choses en Java.
Alors...
Est-il possible d'implémenter des coroutines en Java sans utiliser l'une de ces quatre techniques? Ou serai-je forcé d'utiliser celui de ces quatre qui sent le moins (manipulation de la machine virtuelle) à la place?
Modifié pour ajouter:
Juste pour s’assurer que la confusion est contenue, c’est une question liée à mon autre , mais pas la même chose. Celui-ci recherche une implémentation existante dans le but d'éviter de réinventer la roue inutilement. C’est une question qui concerne la manière dont l’un implémenterait des coroutines en Java si l’autre se révélait irréfutable. L'intention est de garder différentes questions sur différents threads.
Je voudrais jeter un coup d'oeil à ceci: http://www.chiark.greenend.org.uk/~sgtatham/coroutines.html , c'est très intéressant et devrait être un bon point de départ. Mais bien sûr, nous utilisons Java pour pouvoir faire mieux (ou peut-être pire car il n'y a pas de macros :))
D'après ce que je comprends des coroutines, vous avez généralement un producteur et un consommateur (ou du moins, c'est le modèle le plus courant). Mais sémantiquement, vous ne voulez pas que le producteur appelle le consommateur ou l'inverse, car cela introduit une asymétrie. Mais vu le fonctionnement des langages basés sur la pile, nous devrons faire appel à quelqu'un.
Donc, voici une hiérarchie de type très simple:
public interface CoroutineProducer<T>
{
public T Produce();
public boolean isDone();
}
public interface CoroutineConsumer<T>
{
public void Consume(T t);
}
public class CoroutineManager
{
public static Execute<T>(CoroutineProducer<T> prod, CoroutineConsumer<T> con)
{
while(!prod.IsDone()) // really simple
{
T d = prod.Produce();
con.Consume(d);
}
}
}
Bien sûr, la partie difficile est implémentation les interfaces, en particulier il est difficile de décomposer un calcul en étapes individuelles. Pour cela, vous voudrez probablement un autre ensemble de structures de contrôle persistent. L'idée de base est que nous voulons simuler un transfert de contrôle non local (en fin de compte, c'est un peu comme si nous simulions une goto
). Nous voulons essentiellement éviter l'utilisation de la pile et de la pc
(programme-compteur) en conservant l'état de nos opérations en cours dans le tas plutôt que dans la pile. Par conséquent, nous allons avoir besoin de plusieurs classes d’aide.
Par exemple:
Disons que dans un monde idéal, vous vouliez écrire un consommateur qui ressemblait à ceci (psuedocode):
boolean is_done;
int other_state;
while(!is_done)
{
//read input
//parse input
//yield input to coroutine
//update is_done and other_state;
}
nous devons abstraire la variable locale comme is_done
et other_state
et nous devons abstraire la boucle while elle-même car notre opération semblable à yield
ne va pas utiliser la pile. Créons donc une abstraction de boucle while et les classes associées:
enum WhileState {BREAK, CONTINUE, YIELD}
abstract class WhileLoop<T>
{
private boolean is_done;
public boolean isDone() { return is_done;}
private T rval;
public T getReturnValue() {return rval;}
protected void setReturnValue(T val)
{
rval = val;
}
public T loop()
{
while(true)
{
WhileState state = execute();
if(state == WhileState.YIELD)
return getReturnValue();
else if(state == WhileState.BREAK)
{
is_done = true;
return null;
}
}
}
protected abstract WhileState execute();
}
L'astuce de base consiste à déplacer les variables local en variables class et à transformer les blocs de scope en classes, ce qui nous permet de "rentrer" dans notre "boucle" après avoir renvoyé notre valeur de retour.
Maintenant pour implémenter notre producteur
public class SampleProducer : CoroutineProducer<Object>
{
private WhileLoop<Object> loop;//our control structures become state!!
public SampleProducer()
{
loop = new WhileLoop()
{
private int other_state;//our local variables become state of the control structure
protected WhileState execute()
{
//this implements a single iteration of the loop
if(is_done) return WhileState.BREAK;
//read input
//parse input
Object calcluated_value = ...;
//update is_done, figure out if we want to continue
setReturnValue(calculated_value);
return WhileState.YIELD;
}
};
}
public Object Produce()
{
Object val = loop.loop();
return val;
}
public boolean isDone()
{
//we are done when the loop has exited
return loop.isDone();
}
}
Des astuces similaires pourraient être utilisées pour d'autres structures de flux de contrôle de base. Vous devriez idéalement constituer une bibliothèque de ces classes auxiliaires, puis les utiliser pour implémenter ces interfaces simples qui vous donneraient au final la sémantique des co-routines. Je suis sûr que tout ce que j'ai écrit ici peut être généralisé et considérablement développé.
Je suggère de regarder Kotlin coroutines sur JVM . Cela tombe dans une catégorie différente, cependant. Il n'y a pas de manipulation de code octet impliqué et cela fonctionne aussi sur Android. Cependant, vous devrez écrire vos coroutines en Kotlin. L’avantage, c’est que Kotlin est conçu pour assurer l’interopérabilité avec Java, afin que vous puissiez continuer à utiliser toutes vos bibliothèques Java et à combiner librement le code Kotlin et Java dans le même projet, en les plaçant même côte à côte dans les mêmes paquets.
Ce Guide to kotlinx.coroutines fournit de nombreux autres exemples, tandis que la conception des coroutines document explique toutes les motivations, les cas d'utilisation et les détails de mise en œuvre.
Je viens de tomber sur cette question et je veux juste mentionner que j'estime qu'il serait possible d'implémenter des coroutines ou des générateurs de la même manière que C #. Cela dit, je n'utilise pas réellement Java, mais le CIL a des limitations assez similaires à celles de la JVM.
La déclaration yield in C # est une fonctionnalité de langage pur et ne fait pas partie du bytecode CIL. Le compilateur C # crée simplement une classe privée masquée pour chaque fonction du générateur. Si vous utilisez la déclaration de rendement dans une fonction, elle doit renvoyer un IEnumerator ou un IEnumerable. Le compilateur "emballe" votre code dans une classe similaire à statemachine.
Le compilateur C # peut utiliser certains "goto" dans le code généré pour faciliter la conversion en une machine à états. Je ne connais pas les capacités du bytecode Java et s'il y a quelque chose qui ressemble à un saut inconditionnel, mais au "niveau de l'assemblage", c'est généralement possible.
Comme déjà mentionné, cette fonctionnalité doit être implémentée dans le compilateur. Comme je n'ai que peu de connaissances sur Java et son compilateur, je ne sais pas s'il est possible de modifier/étendre le compilateur, peut-être avec un "pré-processeur" ou quelque chose du genre.
Personnellement j'adore les coroutines. En tant que développeur de jeux Unity, je les utilise assez souvent. Parce que je joue beaucoup à Minecraft avec ComputerCraft, j'étais curieux de savoir pourquoi les coroutines de Lua (LuaJ) sont implémentées avec des threads.
Kotlin utilise l'approche suivante pour les co-routines
(from https://kotlinlang.org/docs/reference/coroutines.html ):
Les Coroutines sont complètement implémentées via une technique de compilation (aucun support de la part du VM ou du système d'exploitation n'est requis), et la suspension fonctionne par transformation de code. Fondamentalement, chaque fonction de suspension (des optimisations peuvent s'appliquer, mais nous n'allons pas entrer ici), est transformée en une machine à états dans laquelle les états correspondent à des appels en suspension. Juste avant une suspension, le prochain état est stocké dans un champ d'une classe générée par le compilateur avec les variables locales pertinentes, etc. À la reprise de cette coroutine, les variables locales sont restaurées et la machine à états passe de l'état juste après la suspension.
Une coroutine suspendue peut être stockée et passée en tant qu'objet qui conserve son état et ses paramètres locaux suspendus. Le type de tels objets est Continuation, et la transformation globale du code décrite ici correspond au style classique Passage-Continuation. Par conséquent, les fonctions de suspension prennent un paramètre supplémentaire de type Continuation sous le capot.
Consultez le document de conception sur https://github.com/Kotlin/kotlin-coroutines/blob/master/kotlin-coroutines-informal.md
J'ai une classe Coroutine que j'utilise en Java. Il est basé sur des threads et son utilisation présente l’avantage de permettre un fonctionnement en parallèle, ce qui peut être un avantage pour les machines multicœurs. Par conséquent, vous pouvez envisager une approche basée sur les threads.
Il y a un autre choix est ici pour Java6 +
Une implémentation de coroutine Pythonic:
import Java.lang.ref.WeakReference;
import Java.util.ArrayList;
import Java.util.List;
import Java.util.concurrent.*;
import Java.util.concurrent.atomic.AtomicBoolean;
import Java.util.concurrent.atomic.AtomicReference;
class CorRunRAII {
private final List<WeakReference<? extends CorRun>> resources = new ArrayList<>();
public CorRunRAII add(CorRun resource) {
if (resource == null) {
return this;
}
resources.add(new WeakReference<>(resource));
return this;
}
public CorRunRAII addAll(List<? extends CorRun> arrayList) {
if (arrayList == null) {
return this;
}
for (CorRun corRun : arrayList) {
add(corRun);
}
return this;
}
@Override
protected void finalize() throws Throwable {
super.finalize();
for (WeakReference<? extends CorRun> corRunWeakReference : resources) {
CorRun corRun = corRunWeakReference.get();
if (corRun != null) {
corRun.stop();
}
}
}
}
class CorRunYieldReturn<ReceiveType, YieldReturnType> {
public final AtomicReference<ReceiveType> receiveValue;
public final LinkedBlockingDeque<AtomicReference<YieldReturnType>> yieldReturnValue;
CorRunYieldReturn(AtomicReference<ReceiveType> receiveValue, LinkedBlockingDeque<AtomicReference<YieldReturnType>> yieldReturnValue) {
this.receiveValue = receiveValue;
this.yieldReturnValue = yieldReturnValue;
}
}
interface CorRun<ReceiveType, YieldReturnType> extends Runnable, Callable<YieldReturnType> {
boolean start();
void stop();
void stop(final Throwable throwable);
boolean isStarted();
boolean isEnded();
Throwable getError();
ReceiveType getReceiveValue();
void setResultForOuter(YieldReturnType resultForOuter);
YieldReturnType getResultForOuter();
YieldReturnType receive(ReceiveType value);
ReceiveType yield();
ReceiveType yield(YieldReturnType value);
<TargetReceiveType, TargetYieldReturnType> TargetYieldReturnType yieldFrom(final CorRun<TargetReceiveType, TargetYieldReturnType> another);
<TargetReceiveType, TargetYieldReturnType> TargetYieldReturnType yieldFrom(final CorRun<TargetReceiveType, TargetYieldReturnType> another, final TargetReceiveType value);
}
abstract class CorRunSync<ReceiveType, YieldReturnType> implements CorRun<ReceiveType, YieldReturnType> {
private ReceiveType receiveValue;
public final List<WeakReference<CorRun>> potentialChildrenCoroutineList = new ArrayList<>();
// Outside
private AtomicBoolean isStarted = new AtomicBoolean(false);
private AtomicBoolean isEnded = new AtomicBoolean(false);
private Throwable error;
private YieldReturnType resultForOuter;
@Override
public boolean start() {
boolean isStarted = this.isStarted.getAndSet(true);
if ((! isStarted)
&& (! isEnded())) {
receive(null);
}
return isStarted;
}
@Override
public void stop() {
stop(null);
}
@Override
public void stop(Throwable throwable) {
isEnded.set(true);
if (throwable != null) {
error = throwable;
}
for (WeakReference<CorRun> weakReference : potentialChildrenCoroutineList) {
CorRun child = weakReference.get();
if (child != null) {
child.stop();
}
}
}
@Override
public boolean isStarted() {
return isStarted.get();
}
@Override
public boolean isEnded() {
return isEnded.get();
}
@Override
public Throwable getError() {
return error;
}
@Override
public ReceiveType getReceiveValue() {
return receiveValue;
}
@Override
public void setResultForOuter(YieldReturnType resultForOuter) {
this.resultForOuter = resultForOuter;
}
@Override
public YieldReturnType getResultForOuter() {
return resultForOuter;
}
@Override
public synchronized YieldReturnType receive(ReceiveType value) {
receiveValue = value;
run();
return getResultForOuter();
}
@Override
public ReceiveType yield() {
return yield(null);
}
@Override
public ReceiveType yield(YieldReturnType value) {
resultForOuter = value;
return receiveValue;
}
@Override
public <TargetReceiveType, TargetYieldReturnType> TargetYieldReturnType yieldFrom(CorRun<TargetReceiveType, TargetYieldReturnType> another) {
return yieldFrom(another, null);
}
@Override
public <TargetReceiveType, TargetYieldReturnType> TargetYieldReturnType yieldFrom(CorRun<TargetReceiveType, TargetYieldReturnType> another, TargetReceiveType value) {
if (another == null || another.isEnded()) {
throw new RuntimeException("Call null or isEnded coroutine");
}
potentialChildrenCoroutineList.add(new WeakReference<CorRun>(another));
synchronized (another) {
boolean isStarted = another.start();
boolean isJustStarting = ! isStarted;
if (isJustStarting && another instanceof CorRunSync) {
return another.getResultForOuter();
}
return another.receive(value);
}
}
@Override
public void run() {
try {
this.call();
}
catch (Exception e) {
e.printStackTrace();
stop(e);
return;
}
}
}
abstract class CorRunThread<ReceiveType, YieldReturnType> implements CorRun<ReceiveType, YieldReturnType> {
private final ExecutorService childExecutorService = newExecutorService();
private ExecutorService executingOnExecutorService;
private static final CorRunYieldReturn DUMMY_COR_RUN_YIELD_RETURN = new CorRunYieldReturn(new AtomicReference<>(null), new LinkedBlockingDeque<AtomicReference>());
private final CorRun<ReceiveType, YieldReturnType> self;
public final List<WeakReference<CorRun>> potentialChildrenCoroutineList;
private CorRunYieldReturn<ReceiveType, YieldReturnType> lastCorRunYieldReturn;
private final LinkedBlockingDeque<CorRunYieldReturn<ReceiveType, YieldReturnType>> receiveQueue;
// Outside
private AtomicBoolean isStarted = new AtomicBoolean(false);
private AtomicBoolean isEnded = new AtomicBoolean(false);
private Future<YieldReturnType> future;
private Throwable error;
private final AtomicReference<YieldReturnType> resultForOuter = new AtomicReference<>();
CorRunThread() {
executingOnExecutorService = childExecutorService;
receiveQueue = new LinkedBlockingDeque<>();
potentialChildrenCoroutineList = new ArrayList<>();
self = this;
}
@Override
public void run() {
try {
self.call();
}
catch (Exception e) {
stop(e);
return;
}
stop();
}
@Override
public abstract YieldReturnType call();
@Override
public boolean start() {
return start(childExecutorService);
}
protected boolean start(ExecutorService executorService) {
boolean isStarted = this.isStarted.getAndSet(true);
if (!isStarted) {
executingOnExecutorService = executorService;
future = (Future<YieldReturnType>) executingOnExecutorService.submit((Runnable) self);
}
return isStarted;
}
@Override
public void stop() {
stop(null);
}
@Override
public void stop(final Throwable throwable) {
if (throwable != null) {
error = throwable;
}
isEnded.set(true);
returnYieldValue(null);
// Do this for making sure the coroutine has checked isEnd() after getting a dummy value
receiveQueue.offer(DUMMY_COR_RUN_YIELD_RETURN);
for (WeakReference<CorRun> weakReference : potentialChildrenCoroutineList) {
CorRun child = weakReference.get();
if (child != null) {
if (child instanceof CorRunThread) {
((CorRunThread)child).tryStop(childExecutorService);
}
}
}
childExecutorService.shutdownNow();
}
protected void tryStop(ExecutorService executorService) {
if (this.executingOnExecutorService == executorService) {
stop();
}
}
@Override
public boolean isEnded() {
return isEnded.get() || (
future != null && (future.isCancelled() || future.isDone())
);
}
@Override
public boolean isStarted() {
return isStarted.get();
}
public Future<YieldReturnType> getFuture() {
return future;
}
@Override
public Throwable getError() {
return error;
}
@Override
public void setResultForOuter(YieldReturnType resultForOuter) {
this.resultForOuter.set(resultForOuter);
}
@Override
public YieldReturnType getResultForOuter() {
return this.resultForOuter.get();
}
@Override
public YieldReturnType receive(ReceiveType value) {
LinkedBlockingDeque<AtomicReference<YieldReturnType>> yieldReturnValue = new LinkedBlockingDeque<>();
offerReceiveValue(value, yieldReturnValue);
try {
AtomicReference<YieldReturnType> takeValue = yieldReturnValue.take();
return takeValue == null ? null : takeValue.get();
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
@Override
public ReceiveType yield() {
return yield(null);
}
@Override
public ReceiveType yield(final YieldReturnType value) {
returnYieldValue(value);
return getReceiveValue();
}
@Override
public <TargetReceiveType, TargetYieldReturnType> TargetYieldReturnType yieldFrom(final CorRun<TargetReceiveType, TargetYieldReturnType> another) {
return yieldFrom(another, null);
}
@Override
public <TargetReceiveType, TargetYieldReturnType> TargetYieldReturnType yieldFrom(final CorRun<TargetReceiveType, TargetYieldReturnType> another, final TargetReceiveType value) {
if (another == null || another.isEnded()) {
throw new RuntimeException("Call null or isEnded coroutine");
}
boolean isStarted = false;
potentialChildrenCoroutineList.add(new WeakReference<CorRun>(another));
synchronized (another) {
if (another instanceof CorRunThread) {
isStarted = ((CorRunThread)another).start(childExecutorService);
}
else {
isStarted = another.start();
}
boolean isJustStarting = ! isStarted;
if (isJustStarting && another instanceof CorRunSync) {
return another.getResultForOuter();
}
TargetYieldReturnType send = another.receive(value);
return send;
}
}
@Override
public ReceiveType getReceiveValue() {
setLastCorRunYieldReturn(takeLastCorRunYieldReturn());
return lastCorRunYieldReturn.receiveValue.get();
}
protected void returnYieldValue(final YieldReturnType value) {
CorRunYieldReturn<ReceiveType, YieldReturnType> corRunYieldReturn = lastCorRunYieldReturn;
if (corRunYieldReturn != null) {
corRunYieldReturn.yieldReturnValue.offer(new AtomicReference<>(value));
}
}
protected void offerReceiveValue(final ReceiveType value, LinkedBlockingDeque<AtomicReference<YieldReturnType>> yieldReturnValue) {
receiveQueue.offer(new CorRunYieldReturn(new AtomicReference<>(value), yieldReturnValue));
}
protected CorRunYieldReturn<ReceiveType, YieldReturnType> takeLastCorRunYieldReturn() {
try {
return receiveQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
protected void setLastCorRunYieldReturn(CorRunYieldReturn<ReceiveType,YieldReturnType> lastCorRunYieldReturn) {
this.lastCorRunYieldReturn = lastCorRunYieldReturn;
}
protected ExecutorService newExecutorService() {
return Executors.newCachedThreadPool(getThreadFactory());
}
protected ThreadFactory getThreadFactory() {
return new ThreadFactory() {
@Override
public Thread newThread(final Runnable runnable) {
Thread thread = new Thread(runnable);
thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
throwable.printStackTrace();
if (runnable instanceof CorRun) {
CorRun self = (CorRun) runnable;
self.stop(throwable);
thread.interrupt();
}
}
});
return thread;
}
};
}
}
Vous pouvez maintenant utiliser les coroutines Pythonic de cette manière .__ (par exemple, des nombres de fibonacci)
Version du fil:
class Fib extends CorRunThread<Integer, Integer> {
@Override
public Integer call() {
Integer times = getReceiveValue();
do {
int a = 1, b = 1;
for (int i = 0; times != null && i < times; i++) {
int temp = a + b;
a = b;
b = temp;
}
// A Pythonic "yield", i.e., it returns `a` to the caller and waits `times` value from the next caller
times = yield(a);
} while (! isEnded());
setResultForOuter(Integer.MAX_VALUE);
return getResultForOuter();
}
}
class MainRun extends CorRunThread<String, String> {
@Override
public String call() {
// The fib coroutine would be recycled by its parent
// (no requirement to call its start() and stop() manually)
// Otherwise, if you want to share its instance and start/stop it manually,
// please start it before being called by yieldFrom() and stop it in the end.
Fib fib = new Fib();
String result = "";
Integer current;
int times = 10;
for (int i = 0; i < times; i++) {
// A Pythonic "yield from", i.e., it calls fib with `i` parameter and waits for returned value as `current`
current = yieldFrom(fib, i);
if (fib.getError() != null) {
throw new RuntimeException(fib.getError());
}
if (current == null) {
continue;
}
if (i > 0) {
result += ",";
}
result += current;
}
setResultForOuter(result);
return result;
}
}
Version Sync (non-thread):
class Fib extends CorRunSync<Integer, Integer> {
@Override
public Integer call() {
Integer times = getReceiveValue();
int a = 1, b = 1;
for (int i = 0; times != null && i < times; i++) {
int temp = a + b;
a = b;
b = temp;
}
yield(a);
return getResultForOuter();
}
}
class MainRun extends CorRunSync<String, String> {
@Override
public String call() {
CorRun<Integer, Integer> fib = null;
try {
fib = new Fib();
} catch (Exception e) {
e.printStackTrace();
}
String result = "";
Integer current;
int times = 10;
for (int i = 0; i < times; i++) {
current = yieldFrom(fib, i);
if (fib.getError() != null) {
throw new RuntimeException(fib.getError());
}
if (current == null) {
continue;
}
if (i > 0) {
result += ",";
}
result += current;
}
stop();
setResultForOuter(result);
if (Utils.isEmpty(result)) {
throw new RuntimeException("Error");
}
return result;
}
}
Exécution (les deux versions fonctionneront):
// Run the entry coroutine
MainRun mainRun = new MainRun();
mainRun.start();
// Wait for mainRun ending for 5 seconds
long startTimestamp = System.currentTimeMillis();
while(!mainRun.isEnded()) {
if (System.currentTimeMillis() - startTimestamp > TimeUnit.SECONDS.toMillis(5)) {
throw new RuntimeException("Wait too much time");
}
}
// The result should be "1,1,2,3,5,8,13,21,34,55"
System.out.println(mainRun.getResultForOuter());
Il existe également Quasar pour Java et Project Loom chez Oracle où des extensions sont apportées à la machine virtuelle Java pour les fibres et les suites. Voici un présentation de Loom sur Youtoube. Il y en a plusieurs autres. Facile à trouver avec un peu de recherche.