Fremtidige løfter


mandag 9. august 2010 Clojure C#

Nei, denne blogposten handler ikke om nyttårsforsetter, og heller ikke om politiske valgløfter. Den handler om to concurrency-relaterte features i Clojure som heter future og promise. Tilsvarende abstraksjoner har jeg ikke vært borti i andre språk, men til slutt i denne blogposten vil jeg implementere lignende funksjonalitet i C#.

Future er en liten makro som vil kjøre koden du gir den i en ny tråd. Resultatet av koden vil bli cachet, slik at man ikke behøver å re-kalkulere verdien hver gang man refererer den. Her er et lite eksempel:

(def radius 5.8)

(def x (future
         (Thread/sleep 1000) ; simulerer at dette tar litt tid
         (* 3.141592 radius radius)))

(time (println "Arealet er" @x))
(time (println "Arealet er" @x))
Output:
Arealet er 105.68315488
"Elapsed time: 1000.634298 msecs"
Arealet er 105.68315488
"Elapsed time: 1.096718 msecs"

Future er glimrende når man skal opprette en verdi som det tar litt tid å beregne, og man ikke nødvendigvis trenger verdien med en gang. Når man trenger verdien og den ikke er ferdig beregnet vil man måtte vente til den er ferdig. Men man kan også bruke future til samtidighet generelt, for å kjøre kode i en annen tråd, uten å bry seg om returverdien.

Promise er et løfte om at vi skal beregne en verdi på et eller annet tidspunkt. Her er et Hello World-eksempel som bruker både promise og future:

(comment "Promise that there at some point will be a
          name to greet..")
(def name-to-greet (promise))

(comment "At some point in the future, when there is
          a name to greet, say hello")
(future (println "Hello," @name-to-greet))

(comment "Ask for the user's name, and deliver on the
          promise made earlier.")
(print "What is your name? ") (flush)
(deliver name-to-greet (read-line))

(shutdown-agents) ; Just some thread cleanup

Future brukes til å opprette en tråd hvor man trenger verdien som er lovet. Den vil stå og vente på at verdien er tilgjengelig. Når vi leverer det vi har lovet kan futuren fullføres.

Eksempelkjøring:
What is your name? Billy Bob
Hello, Billy Bob

Advarsel: Promise er merket med "Alpha – subject to change" i API-dokumentasjonen. Bruk den derfor med varsomhet.

Som nevt i innledningen vil jeg nå vise én mulig C#-implementasjon av det jeg har vist. Det er godt mulig det finnes bedre måter, og at for eksempel Task Parallel Library i .NET 4.0 kan brukes på samme måte (har ikke lekt med det enda), men jeg synes følgende abstraksjoner ble ganske så fine. Først begynner vi med en C#-variant av den første Clojure-demoen:

static void Main(string[] args)
{
    const double radius = 5.8;
    var x = Future.Create(() =>
    {
        Thread.Sleep(1000);
        return 3.141592 * radius * radius;
    });
 
    Benchmark.Measure(() => Console.WriteLine(x.Value));
    Benchmark.Measure(() => Console.WriteLine(x.Value));
}

Her brukte jeg QuickBencher (et open source prosjekt jeg har liggende på CodePlex) til å måle forbrukt tid, og verifiserte samme oppførsel som Clojure-varianten.

Før jeg viser implementasjonen av Future-klassen tar vi også med en C#-variant av promise-eksempelet:

class Program
{
    static void Main(string[] args)
    {
        var nameToGreet = new Promise<string>();
 
        Future.Create(() => Console.WriteLine("Hello {0}", nameToGreet.Value));
 
        Console.Write("What is your name? ");
        nameToGreet.Deliver(Console.ReadLine());
    }
}

Ser ganske greit ut, ikke sant? Denne koden fungerer også som den skal. Her følger til slutt implementasjonen av Promise og Future. Legg merke til at Future egentlig er to ulike klasser, hvor den ene representerer en Future som har en returverdi. Create-metodene i den ikke-generiske klassen brukes til å opprette riktig type basert på lambda-argumentet.

public class Promise<T>
{
    private T _value;
    private bool _delivered;
    public T Value
    {
        get
        {
            while (!_delivered) Thread.Sleep(10);
            return _value;
        }
    }
    public void Deliver(T value)
    {
        _value = value;
        _delivered = true;
    }
}
 
public class Future<T>
{
    private Thread _producerThread;
    private T _value;
    public T Value 
    { 
        get 
        {
            _producerThread.Join();
            return _value;
        } 
    }
    public Future(Func<T> producer)
    {
        _producerThread = new Thread(() => _value = producer());
        _producerThread.Start();
    }
}
 
public class Future
{
    public static Future<T> Create<T>(Func<T> producer)
    {
        return new Future<T>(producer);
    }
    public static Future Create(Action action)
    {
        return new Future(action);
    }
    public Future(Action action)
    {
        new Thread(() => action()).Start();    
    }
}

Promise fungerer rett og slett slik at om man forsøker å aksessere verdien før den er satt så vil den vente til den har en verdi å levere (med en optimistisk og kanskje litt naiv loop). Future fungerer ved at den eksekverer jobben den har fått i en ny tråd. Versjonen som har en returverdi er nokså lik Promise; om man forsøker å aksessere verdien sørger jeg for at tråden er ferdig med det den skal gjøre (altså produsere verdien) før jeg returnerer ved å kalle Join() på tråden.

I eksempelet hvor jeg kombinerte Promise og Future vil altså futuren stå og vente på at løftet leveres. Og løftet står og venter på at jeg taster inn navnet mitt, som jeg gjør i hovedtråden.

Spørsmål?


comments powered by Disqus