Gestione del time slice in GTK#

| | Comments (0)

Come promesso tempo addietro, pubblico qualcuno dei miei primi esperimenti con Mono e C#.

Ho iniziato a cimentarmi con le interfacce grafiche GTK#, e quindi a lottare controtutte le complicanzioni che esse comportano nella scrittura di software: in questo breve articolo me la vedo con la gestione del time slice. In altre parole, ho cercato di capire come fare sì che l'interfaccia di un programma rimanga reattiva anche durante operazioni che richiedono un lungo tempo. Tutto ciò senza ricorrere a thread o fork.

Quando si lavora in una GUI, uno dei problemi che si configurano solitamente è quello di far si che il proprio programma risponda all'utente anche in caso stia eseguendo compiti (ad esempio calcoli) che richiedono un certo tempo. Non è sempre indispensabile confrontarsi con questo problema, poiché la maggior parte delle comuni operazioni richiede un tempo trascurabile su computer moderni. In altri casi ancora, il lavoro che il software sta eseguendo, per quanto lungo, è indispensabile per la prosecuzione dell'utilizzo di esso; anche in queste situazioni, tuttavia, è quantomeno elegante che l'interfaccia grafica continui a "rispondere", ad esempio a ridisegnarsi qualora un'altra finestra ci venga messa sopra e poi nuovamente spostata.

Una delle possibilità a disposizione del programmatore consiste nell'utilizzo di più processi (tramite forking) oppure, seguendo un approccio decisamente più moderno e molto ben supportato in Mono, dei thread. Tuttavia, in casi in cui le operazioni "lunghe" da gestire non siano tante, il ricorso ai thread probabilmente complica inutilmente il programma, causando un ricorso ad un numero molto maggiore di imprecazioni durante la fase di debug.

Il seguente breve programma crea una finestra con un menu. Scegliendo da tale menu la voce Calcola approssimazioni viene calcolata l'approssimazione armonica dei valori tra 1 e maxnum (impostato a 20000 di default, cambiatelo se nella vostra macchina l'operazione richiede troppo o troppo poco tempo - nella mia ci vogliono circa 15 secondi). I risultati del calcolo vengono di volta in volta indicati in console. Ah, vorrei precisare che non ho la minima idea di cosa sia un'approssimazione armonica: l'ho trovata su un libro ed andava bene per lo scopo di questo articolo. ;-)

using System;
using Gtk;
using Gdk;

public class TSWin : Gtk.Window {	
    private const int maxnum = 20000;

    private Label stlabel;

    public TSWin () : base ("Time Slice") {
        this.SetDefaultSize (300, 200);
        MenuBar mb = new MenuBar ();
        AccelGroup agrp = new AccelGroup ();
        this.AddAccelGroup(agrp);

        // Menu File
        Menu file_menu = new Menu ();
        MenuItem item = new MenuItem ("_File");
        item.Submenu = file_menu;
        mb.Append (item);

        item = new MenuItem ("_Calcola approssimazioni");
        item.AddAccelerator ("activate", agrp,
           new AccelKey(Gdk.Key.C, Gdk.ModifierType.ControlMask, AccelFlags.Visible)
        );
        item.Activated += onMenuCalcola;
        file_menu.Append (item);

        item = new ImageMenuItem (Stock.Quit, agrp);
        item.Activated += onMenuQuit;
        file_menu.Append (item);

        // Label di stato
        stlabel = new Label ("Nulla da fare ancora...");

        VBox v = new VBox ();
        v.PackStart (mb, false, false, 0);
        v.PackStart (stlabel, false, false, 0);
        this.Add (v);
    }

    public void onMenuQuit(object o, EventArgs e) {
        Application.Quit();
    }

    public void onMenuCalcola(object o, EventArgs e) {
        for (int i = 1; i <= maxnum; i++) {
            double arm = appr_armonica(i);
            Console.WriteLine("Approssimazione armonica di {0}: {1}", i, arm);
        }
        stlabel.Text = "Ho terminato il calcolo delle " + maxnum + " approssimazioni.";
    }

    // Approssimazione armonica
    private double appr_armonica(int n) {
        return
            Math.Log(n) +
            0.577215664901532 +
            (1 / (2 * n)) -
            (1 / (12 * Math.Pow(n, 2))) +
            (1 / (120 * Math.Pow(n, 4)))
        ;
    }
	
}

class TSMain {
    public static void Main (string[] args)	{
        Application.Init ();

        TSWin tsw = new TSWin ();
        tsw.ShowAll ();
        tsw.DeleteEvent += onDelete;

        Application.Run ();
    }

    public static void onDelete(object o, DeleteEventArgs e) {
        Application.Quit();
        e.RetVal = true;
    }
}

GUI non reattiva Il principale problema di questo codice è appunto quello descritto (che si vede anche nell'immagine qui a fianco): appena si sceglie dal menu la voce che attiva il calcolo, esso rimane aperto e l'interfaccia smette totalmente di rispondere per tutto il tempo richiesto dal calcolo. Cliccando il bottone che chiude la finestra, non si ottiene altro che l'informazione - fornita dal sistema operativo - che l'applicazione non risponde, e l'offerta di terminarla forzatamente.

Di seguito è riportata la stessa applicazione, modificata in modo da ovviare a questo inconveniente.

using System;
using Gtk;
using Gdk;

public class TSWin : Gtk.Window {	
    private const int maxnum = 20000;
    private const int passes = 200;

    private int nxpass = maxnum / passes;
    private int lefties = maxnum % passes;
    private int curpass = passes;

    private Label stlabel;

    public TSWin () : base ("Time Slice") {
        this.SetDefaultSize (300, 200);

        MenuBar mb = new MenuBar ();

        AccelGroup agrp = new AccelGroup ();
        this.AddAccelGroup(agrp);

        // Menu File
        Menu file_menu = new Menu ();
        MenuItem item = new MenuItem ("_File");
        item.Submenu = file_menu;
        mb.Append (item);

        item = new MenuItem ("_Calcola approssimazioni");
        item.AddAccelerator ("activate", agrp,
            new AccelKey(Gdk.Key.C, Gdk.ModifierType.ControlMask, AccelFlags.Visible)
        );
        item.Activated += onMenuCalcola;
        file_menu.Append (item);

        item = new ImageMenuItem (Stock.Quit, agrp);
        item.Activated += onMenuQuit;
        file_menu.Append (item);

        // Label di stato
        stlabel = new Label ("Nulla da fare ancora...");

        VBox v = new VBox ();
        v.PackStart (mb, false, false, 0);
        v.PackStart (stlabel, false, false, 0);
        this.Add (v);
    }

    public void onMenuQuit(object o, EventArgs e) {
        Application.Quit();
    }

    public void onMenuCalcola(object o, EventArgs e) {
        // Se il calcolo è già in corso, non iniziarne un altro
        if (curpass != passes) {
            MessageDialog md = new MessageDialog(
                this,
                DialogFlags.DestroyWithParent,
                MessageType.Error,
                ButtonsType.Ok,
                "Calcolo già in esecuzione!"
            );
            md.Run();
            md.Destroy();
        } else {
            GLib.Idle.Add(new GLib.IdleHandler(IniziaCalcolo));
        }
    }

    private bool IniziaCalcolo() {
        curpass = 1;
        GLib.Timeout.Add(100, new GLib.TimeoutHandler(Calcola));

        // Questo non deve venire più chiamato nei successivi Idle
        return false; 
    }

    private bool Calcola() {
        for (int i = nxpass * (curpass-1) + 1; i <= nxpass * curpass; i++) {
            double arm = appr_armonica(i);
            Console.WriteLine("Approssimazione armonica di {0}: {1}", i, arm);
        }

        if (curpass != passes) {
            // Continua a chiamare Calcola() finché non abbiamo finito
            stlabel.Text = "Ho calcolato " + nxpass * curpass + " approssimazioni.";
            curpass++;
            return true;
        } else {
            // Calcola le approssimazioni residui e poi evita che Calcola() venga
            // chiamata nuovamente
            stlabel.Text = "Ho terminato il calcolo delle " + maxnum + " approssimazioni!";
            for (int i = nxpass * curpass + 1; i <= nxpass * curpass + lefties; i++) {
                double arm = appr_armonica(i);
                Console.WriteLine("Approssimazione armonica di {0}: {1}", i, arm);
            }
            return false;
        }
    }

    // Approssimazione armonica
    private double appr_armonica(int n) {
        return
            Math.Log(n) +
            0.577215664901532 +
            (1 / (2 * n)) -
            (1 / (12 * Math.Pow(n, 2))) +
            (1 / (120 * Math.Pow(n, 4)))
        ;
    }
	
}

class TSMain {
    public static void Main (string[] args) {
        Application.Init ();

        TSWin tsw = new TSWin ();
        tsw.ShowAll ();
        tsw.DeleteEvent += onDelete;

        Application.Run ();
    }

    public static void onDelete(object o, DeleteEventArgs e) {
        Application.Quit();
        e.RetVal = true;
    }
}

Il nuobo metodo onMenuCalcola non avvia il calcolo, bensì registra il metodo IniziaCalcolo come IdleHandler: in questo modo il time slice viene immediatamente rilasciato, cosicché il menu possa chiudersi. L'inizio del calcolo è delegato a quando l'applicazione non ha altro da fare, evento che in realtà si verifica immediatamente dopo la chiusura del menu ed il ridisegno della finestra.

Nemmeno IniziaCalcolo compie in realtà alcuna operazione ai fini dell'approssimazione armonica, ma si limita a registrare il metodo Calcola come TimeoutHandler: esso viene cioè eseguito il prima possibile (cioè appena l'applicazione non ha altro da fare) ma in ogni caso non prima che sia trascorso il tempo indicato al momento dell'inizializzazione di tale handler. In questo caso ho indicato 100 millisecondi: il valore può essere variato verso l'alto se si desidera "spalmare" il calcolo nel tempo, o verso il basso se lo si desidera eseguito più rapidamente. In questo caso specifico si potrebbe sempre optare per la soluzione più rapida, ma con applicazione più complesse potrebbe essere necessario fare della valutazioni. Prestate attenzione al return false: questo indica al framework di non chiamare nuovamente il metodo IniziaCalcolo al successivo ciclo di idle dell'applicazione.

Il clou del calcolo risiede - che sorpresa - all'interno del metodo Calcola, che si occupa di effettuare solo una parte delle operazioni durante ciascuna esecuzione di sé stesso (tali esecuzioni avvengono non più spesso di ogni 100 millisecondi, come indicato poc'anzi). Sulla base delle variabili d'istanza maxnum e passes si può determinare in quante volte dividere l'operazione di calcolo: più alta è passes e maggiori (ma meno CPU-intensive) saranno le chiamate a Calcola. Il numero di approssimazioni armoniche calcolate per ogni chiamata è infatti dato dal rapporto maxpass/passes, memorizzato nella variabile nxpass: se la divisione dovesse fornire un resto, esso viene memorizzato in lefties, al fine di evitare che le ultime approssimazioni arominche vengano tralasciate. Il ciclo for si occupa di effettuare i calcoli sull'intervallo di numeri appropriato. A ciclo completato, viene verificato se vi siano o meno ancora passes da effettuare: in caso positivo viene restituito un valore true, che indica al framework di chiamare nuovamente Calcola al prossimo timeout dei 100 millisecondi; in caso contrario vengono effettuati i calcoli per i lefties e poi il restituito false, il che fa sì che il framework non chiami più il metodo Calcola.

Il resto è tutto accessorio: la Label aggiornata periodicamente, il controllo in onCalcola che impedisce di avviare 2 sessioni di calcolo contemporanee, ... Il clou della gestione del time slice è la registrazione dell'IdleHandler e del TimeoutHandler, e la divisione delle operazioni di calcolo in più parti operata all'interno di quest'ultimo. Una volta effettuate le opportune scelte sul numero di parti in cui le operazioni vanno divise e sulla frequenza con la quale queste parti vengono eseguite, far sì che la propria GUI continui ad essere reattiva anche a fronte di lunghe e pesanti operazioni diventa piuttosto facile. E non serve complicarsi la vita con i thread. ;-)

Tutto il codice qui riportato funziona sia sotto Linux che sotto Windows che sotto qualsiasi altra piattaforma su cui sia installato Mono 1.0. Sotto Windows va benissimo il .NET framework, ma servono anche le librerie Gtk#.

Leave a comment