tkinter, python e automi cellulari – life

lifeRiprendiamo l’esempio del gioco life, già visto in un precedente articolo, per esaminare l’utilizzo della libreria tkinter del python. Questa libreria permette l’utilizzo della GUI in questo linguaggio.

Il codice integrale di questo esempio è disponibile a questo indirizzo; riporto qui solo gli stralci relativi alla libreria tkinter.

Il corpo principale del programma inizia ovviamente importando i moduli necessari:

import random
from tkinter import *

random per la generazione casuale della configurazione iniziale e tkinter per la GUI.

Viene poi richiesta la dimensione della tabella che si vuole gestire e, a seguire, creato un oggetto CellularAutoma.

dim = input( "Inserisci la dimensione della board: ")
board = CellularAutoma( int(dim) )

CellularAutoma è la classe su cui si basa tutto l’applicativo e, oltre a contenere una tabella (lista di liste per la precisione) con i dati dell’ultimo stato del sistema definisce i metodi per rappresentarlo graficamente (show) e calcolare lo stato successivo (evolve).

board.show()

while True:
   board.evolve( )
   board.show()
   board.master.update()

Il programma, come si vede, contiene poco più che chiamate a questi metodi; infatti, dopo lo show della configurazione iniziale, entra in un loop nel quale viene calcolato lo stato successivo, viene mostrato a video lo stato e si utilizza la funzione update della libreria per permettere al ciclo di procedere e non rimanere bloccato nei loop degli eventi dell’interfaccia grafica.

Passiamo ora ad esaminare la classe: non entreremo nel dettaglio di tutti i metodi, come detto, perché alcuni di questi non hanno differenze rilevanti dalle funzioni esaminate nel precedente articolo sul gioco life.

La struttura della classe è:

class CellularAutoma:
   def __init__(self, lenght=6):
      """genera la tabella iniziale quadrata e di dimensione iniziale lenght"""
      self.board = [[]]
 
   def evolve( self ):
      """esegue lo step di evoluzione del gioco life su una tabella sferica"""
 
   def show( self ):
      """Gives a representation of the data"""

Esaminiamo ora i metodi iniziando dal costruttore dove oltre all’inizializzazione dei dati, che qui non vediamo nel dettaglio ma che si basa sulla generazione di numeri casuali, viene anche creato un’oggetto per la GUI:

   def __init__(self, lenght=6):
      """genera la tabella iniziale quadrata e di dimensione iniziale lengoht"""
      self.board = [[]]
      
      [...]

      #inizializzo ambiente grafico 
      self.master = Tk()
      self.master.title('life game')
      self.w = Canvas(self.master, width=lenght*10, height=lenght*10)
      self.w.pack()

Viene infatti inizializzato l’ambiente grafico creando un oggetto della classe Tk, assegnato un nome alla finestra, aggiunto un oggetto immagine (Canvas) alla finestra e infine si utilizza il metodo pack() per organizzare gli oggetti nella finestra. In questo caso la chiamata a pack()  è relativamente utile essendoci solamente un oggetto canvas nella finestra.

Non entro nei dettagli del metodo evolve che non è coinvolto nella rappresentazione grafica.

Segue infine il metodo show() che rappresenta graficamente lo stato del sistema.

   def show( self ):
      self.w.delete(ALL)
      for i,v in enumerate(self.board):
         for j,w in enumerate( self.board[i] ):
            if (self.board[i][j] == 0):
               self.w.create_rectangle(i*10, j*10, i*10+10, j*10+10, fill="blue")
            else:
               self.w.create_rectangle(i*10, j*10, i*10+10, j*10+10, fill="yellow")

Anche in questo caso la libreria tkinter rende le cose piuttosto seplici; abbiamo infatti un loop su tutti gli elementi della tabella e andreamo a rappresentare sul nostro oggetto canvas un quadrato blu per le celle vuote ed un quadrato giallo per quelle piene. La funzione che utilizziamo in questo caso è

create_rectangle(x0, y0, x, y, fill="yellow")

La delete() a inizio metodo serve a rimuovere gli oggetti creati in precedenza.

La libreria tkinter mi permette quindi di definire una GUI perfettamente funzionale con pochi passi:

  • definisco un oggetto GUI
  • ne specifico gli elementi grafici
  • controllo il flusso del programma con mainloop, update e funzioni per la gestione dei vari eventi che è necessario controllare

Abbiamo visto qui un esempio di utilizzo della libreria tkinter, documentazione più completa è disponibile nella documentazione ufficiale di python.

esempio thread in python: calcolo della matrice prodotto

Vediamo ora un esempio di programma per il calcolo di una matrice prodotto in python che esegue il calcolo di ogni elemento in un thred differente

Matematicamente l’elemento C{}under{i,j} della matrice prodotto è definito da

C{}under{i,j}=sum{n=1}{K}{A{}under{i,n}B{}under{n,j}}

Il programma andrà quindi a definire una lista di thread, aspetterà il completamento di tutti e scriverà a avideo la matrice prodotto.
Inserisco per prima cosa le tre matrici coinvolte: A e B sono le matrici che verranno moltiplicate e C sarà usato per contenere il risultato.
Il programma è in grado di gestire anche matrici di dimensioni differenti anche se A e B qui hanno una dimensione specifica. Si può quindi rieseguire l’esempio semplicemente sostituendo A e B o modificandolo per andare a leggere da disco i valori.

if __name__ == "__main__":
   """ inizializzo le tre matrici"""
   # definisco le matrici iniziali
   A = [ [1,1,1], [1,1,1]]
   B = [ [1,1], [1,1],[1,1]]
   C = [[0 for j in range(len(A))] for i in range(len(B[0]))]

Definisco una lista in cui raggrupperò i thread che in questo caso utilizzerò per assicurarmi che tutti i thread hanno completato la loro attività prima di stampare il risultato.
Il codice contiene anche, commentata, la definizione di un oggetto lock che può essere utilizzato per controllare la sincronizzazione dei thread. In questo caso però non è utile dato che le operazioni eseguite dai vari thread sono indipendenti.

   
   # acquisizione del lock
   # threadLock = threading.Lock()
   threads = []

Definisco e avvio poi i vari thread sfruttando un’apposita classe myThread che descriveremo più sotto nel dettaglio.

   # avvio in thread per ogni elemento della nuova matrice
   thread_id = 0
   for row in range( len(A) ):
      for column in range( len(B[0]) ):  
         # Create new threads
         threads_a = myThread(thread_id, "Thread-"+str(row)+"_"+str(column), row, column)
         # Start new Threads
         threads_a.start()
         # Add threads to thread list
         threads.append(threads_a)
         thread_id = thread_id + 1

attendo il completamento di tutti i thread

   #    Wait for all threads to complete
   for t in threads:
      t.join()

stampo in fine il risultato

   for i,v in enumerate(C):
      print(C[i])

Passiamo ora alla classe myThread che è costituita ridefinendo la classe threading.Thread.
In particolare ridefiniamo due funzioni: un costruttire e la funzione run che viene eseguita dal thread avviato con lo start().
Il costruttore si limita a definire nell’oggetto alcune variabili passate per argomento

class myThread( threading.Thread ):
   def __init__(self, threadID, name, row, column):
      threading.Thread.__init__(self)
      self.threadID = threadID
      self.name = name
      self.row = row
      self.column = column

la funzione run invece contiene tutto il calcolo dell’elemento di matrice

   def run( self ):
      print ( "Starting " + self.name )
      # Get lock to synchronize threads
      # threadLock.acquire()
      # calculate matrix element
      self.totale = 0
      for a in range(len(A[self.row])):
         for b in range( len(B) ):
            if( a == b ): 
               self.totale = self.totale + A[self.row][a] * B[b][self.column]
            #print("{:s}, self.totale = {:d}".format(self.name, self.totale) )

      C[self.row][self.column] = self.totale
      # Free lock to release next thread
      # threadLock.release()

Decommentando la riga print si può vedere come i thread procedono in parallelo.

Riporto sotto il codice completo

class myThread( threading.Thread ):
def __init__(self, threadID, name, row, column):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.row = row
self.column = column
def run( self ):
print ( "Starting " + self.name )
# Get lock to synchronize threads
# threadLock.acquire()
# calculate matrix element
self.totale = 0
for a in range(len(A[self.row])):
for b in range( len(B) ):
if( a == b ):
self.totale = self.totale + A[self.row][a] * B[b][self.column]
#print("{:s}, self.totale = {:d}".format(self.name, self.totale) )

C[self.row][self.column] = self.totale
# Free lock to release next thread
# threadLock.release()

if __name__ == "__main__":
""" inizializzo le tre matrici"""
# definisco le matrici iniziali
A = [ [1,1,1], [1,1,1]]
B = [ [1,1], [1,1],[1,1]]
C = [[0 for j in range(len(A))] for i in range(len(B[0]))]

# acquisizione del lock
# threadLock = threading.Lock()
threads = []

# avvio in thread per ogni elemento della nuova matrice
#numThread = A.len() * B[0].len()
thread_id = 0
for row in range( len(A) ):
for column in range( len(B[0]) ):
# Create new threads
threads_a = myThread(thread_id, "Thread-"+str(row)+"_"+str(column), row, column)
# Start new Threads
threads_a.start()
# Add threads to thread list
threads.append(threads_a)
thread_id = thread_id + 1

# Wait for all threads to complete
for t in threads:
t.join()

for i,v in enumerate(C):
print(C[i])


			

esempio libreria pthread in c

Vediamo ora un esempio di utilizzo della libreria pthread in C. Scriviamo un programma che troverà tutti i numeri primi minori ed uguali ad un numero passato a linea di comando.

Per prima cosa in C bisogna includere le librerie che si utilizzeranno.

#include <pthread.h>
#include  <stdio.h>
#include  <stdlib.h>

importo quindi la libreria pthread per la gestione dei thread, la libreria stdio per comunicare i risultati e la stdlib per gestire l’input.

Dichiaro poi le variabili che utilizzero’ per far comunicare il processo padre e i suoi thred e le funzioni che verranno esplicitate più sotto.

long int value = 0, upper_limit;
int isPrime( int number );
void *runner( void *param );

qui particolarmente rilevante è *runner(): definendo il thread dirò che la sua esecuzione inizierà da questa funzione.

Segue poi il corpo principale del programma e le definizioni delle variabili locali alla funzione

int main( int argc, char * argv[] )
{
        int pid;
        pthread_t tid;
        pthread_attr_t attr;
        char *endptr;

qui la variabile tid verrà usata per contenere l’identificativo del thread, mentre attr è una struttura con gli attributi del thread relativi ad esempio allo scheduling. Qui non li specifichiamo ed utilizzeremo i valori di default.

Recupero il parametro in ingresso controllando l’esistenza del parametro stesso

        if ( argc < 2 ) {
                printf( "Usage: %s ", argv[1] );
        } else upper_limit =  strtol(argv[1], &endptr, 10 );

        printf("PARENT: upper_limit = %d\n", (int)upper_limit );

Passo poi alla generazione del thread vero e proprio:
Per prima cosa recupero i valori di default per attr

        pthread_attr_init( &attr );

Creo poi il thread recuperando il tid, gli attr e specificando che l’esecuzione del thread deve essere nella funzione runner.

        pthread_create( &tid, &attr, runner, NULL );

devo poi solo attendere la conclusione delle attività del thread.

        
        pthread_join( tid, NULL );

Concludo poi l’esecuzione.

        printf( "End of Child\n" );
}

Seguono poi le due funzioni che sono utilizzate in questo programma. La prima è quella che verifica se un numero è primo. Non è particolarmente efficace ma è chiara e semplice e assolutamente adeguata ad un esempio di uso dei thread.

int isPrime(int number)
{
    int i;
    for ( i = 2; i < number; i++)
    {
      if (number % i == 0 && i != number)
        return 0;
    }
    return 1;
}

Semplicemente, per il numero che viene passato, verifico tutti i possibili divisori.

Segue poi la funzione richiamata con il thread:

void *runner( void *param ) {
        int prime;
        int lista[upper_limit];
        int i,j;
        printf( "START runner: %d\n", (int)upper_limit );

        printf("1\n");
        for( i = 2; i <= upper_limit; i++ ){
                if( isPrime( i ) ) printf("%d\n", i);
        }
        pthread_exit(0);
}

funzione che scriverà a video i numeri individuati come primi dalla funzione precedente.

Riporto sotto il listato dell’intero programma. Per compilarlo con gcc bisogna specificare il parametro -pthread.

gcc -pthread prime_thread.c


#include <pthread.h>
#include  <stdio.h>
#include  <stdlib.h>

long int value = 0, upper_limit;
int isPrime( int number );
void *runner( void *param );

int main( int argc, char * argv[] )
{
        int pid;
        pthread_t tid;
        pthread_attr_t attr;
        char *endptr;

        if ( argc < 2 ) {
                printf( "Usage: %s ", argv[1] );
        } else upper_limit =  strtol(argv[1], &endptr, 10 );

        printf("PARENT: upper_limit = %d\n", (int)upper_limit );

        pthread_attr_init( &attr );
        pthread_create( &tid, &attr, runner, NULL ); 
        pthread_join( tid, NULL ); 
        printf( "End of Child\n" );
}

int isPrime(int number)
{
    int i;
    for ( i = 2; i < number; i++)
    {
      if (number % i == 0 && i != number)
        return 0;
    }
    return 1;
}

void *runner( void *param ) {
        int prime;
        int lista[upper_limit];
        int i,j;

        printf( "START runner: %d\n", (int)upper_limit );

        printf("1\n");
        for( i = 2; i <= upper_limit; i++ ){
                if( isPrime( i ) ) printf("%d\n", i);
        }
        pthread_exit(0);
}

python – fork e comunicazione tra processi

Nel seguente programma, utilizzando il calcolo della serie di fibonacci per pretesto, mostro un esempio di utilizzo del fork e della comunicazione tra processi.

Ai fini del calcolo dei termini della successione di fibonacci utilizzo una funzione ricorsiva che richiama se stessa fino a che non arriva a termini noti: non è il massimo per quel che riguarda le prestazioni ma è comoda e compatta e in questo caso è solo un pretesto.

Iniziamo dal listato del programma:

#!/usr/bin/env python3 
import os, sys

def fib_rec(n):
    """calculate Fibonacci series up to n"""
    if n == 1:
       return 1
    elif n == 2:
       return 1
    else:
       return fib_rec(n-1)+fib_rec(n-2)

while 1:
    r,w=os.pipe()

    num = int(input("Please enter an integer: "))

    pid = os.fork()
    if pid:          # Parent
        while 1:
            data=os.read(r,64)
            if data == (" ").encode():
                break
            else:
                print (str(pid)+": child calculate: " + str(data))
    else:           # Child
        count=1;
        while count <= num:
            fibn =  fib_rec(count)
            # il loop serve a passare parole di 64 byte fisse visto che poi leggo byte per byte
            count2 = 1
            while count2 <= 64 - len(str(fibn)):
                os.write(w, ("").encode());
                count2 = count2 + 1
            msg = (str(fibn)).encode()
            os.write(w, msg)
            count = count+1
        # invio stringa vuota per chiudere pipe
        count2 = 1
        while count2 < 64 :
            os.write(w, ("").encode());
            count2 = count2 + 1
        os.write(w, (" ").encode());
        exit(0)

Non spieghiamo nel dettaglio fib_rec che è semplicemente una funzione ricorsiva per il calcolo della successione di fibonacci.

Il programma è costituito da un loop infinito nel quale viene chiesto quanto deve essere lunga la successione da calcolare. Ad ogni ciclo il programma definisce una pipe, genera un processo child per eseguire il calcolo e mostra a video i risultati calcolati dal processo figlio e passati al processo padre.

Vediamo più nel dettaglio:

    r,w=os.pipe()

definisce la pipe e restituisce due file descriptor; il primo è utilizzabile per leggere ed il secondo per scrivere.

Una volta ottenuta la lunghezza della sequenza da calcolare, il programma genera il figlio

pid = os.fork()

l'esecuzione del programma procede da qui sia per il processo padre sia per quello figlio ma nel caso del padre pid conterrà il pid del figlio, nel caso del figlio pid conterrà il valore 0. Questo viene usato per distinguere il comportamento di padre e figlio.

Il processo padre entra in un loop infinito in cui legge parole di 64 byte dalla pipe e da cui esce se il valore ritornato è uno spazio.

    if pid:          # Parent
        while 1:
            data=os.read(r,64)
            if data == (" ").encode():
                break
            else:
                print (str(pid)+": child calculate: " + str(data))

Il processo figlio, di contro, calcola i termini della successione di fibonacci e li scrive nella pipe assieme a tanti caratteri vuoti quanti ne servono per completare la parola di 64 byte. Terminata la sequenza, invia una stringa contenente un solo spazio per dire al processo padre di concludere il suo ciclo ed esce.

        count=1;
        while count <= num:
            fibn =  fib_rec(count)
            # il loop serve a passare parole di 64 byte fisse visto che poi leggo byte per byte
            count2 = 1
            while count2 <= 64 - len(str(fibn)):
                os.write(w, ("").encode());
                count2 = count2 + 1
            msg = (str(fibn)).encode()
            os.write(w, msg)
            count = count+1
        # invio stringa vuota per chiudere pipe
        count2 = 1
        while count2 < 64 :
            os.write(w, ("").encode());
            count2 = count2 + 1
        os.write(w, (" ").encode());
        exit(0)

backup e rsync

Avere una politica di backup che ci permetta di limitare i danni in caso di errori o problemi è una necessità che risale all’origine dell’informatica.

UNIX e Linux hanno ovviamente diversi tool che vengono in aiuto per questa necessità, e, almeno nel caso di una macchina singola, si possono implementare delle politiche asolutamente soddisfacenti con soli tool interni come cron ed rsync.

Come molti comandi della shell UNIX rsync è estremamente versatile. Il comando di base è:

rsync /path/to/source/file /path/to/destination

Come suggerisce il nome rsync non è un semplice tool per la copia dei file ma si occupa di sincronizzare due directory effettuando la copia dei soli file che lo richiedono.

Tendenzialmente rsync è utile soprattutto se si ha la necessità di copiare molti file e quindi sarà necessario utilizzare l’opzione -r

rsync -r /path/to/source/ /path/to/destination
rsync -r /path/to/source /path/to/destination

Ho riportato due comandi per sottolineare una particolarità di rsync. Se si copia una directory e si specifica / nel path della sorgente il contenuto della directory verrà copiato nella destinazione; se invece il path viene scritto senza / è la directory che viene copiata nella destinazione.

Rsync generalmente non ha quasi nessun output a video, il che è ottimo per script in crontab, se però si sta eseguendo il backup in real time si vorrà probabilmente un’indicazione di come sta procedendo l’attività. PEr questo si hanno due opzioni -v e –progress.

rsync generalmente controlla le dimensioni e la data di ultima modifica per capire se devono essere copiate. Sono però disponibili specifiche opzioni per fare controlli differenti. Ad esempio –size-only se si vole considerare solo le dimensioni del file o –checksum se si vuole un conformto più affidabile anche se più oneroso.

Le opzioni di questo comando sono svarite ma generalmente se si stanno effettuando dei backup -a farà si che vengano attivate le opzioni che più comunemente vengono utilizzate per questa attività.

Una mensione particolare merita l’opzione -b; in questa modalità rsync effettuerà un backup incrementale spostando i file rimossi dalla sorgente in un appositp path e scrivendo su un file tutte le operazioni effettuate. Si otterrà quindi una directory allineata a quella sorgente e si manterranno i file e le informazioni necessari per ricostruire la situazione al momento del backup precedente.

Un esempio di comando reale potrebbe quindi essere

rsync -avb --size-only --delete --log-file=/path/to/file_log/file.log --backup-dir=/path/to/directory_file_incrementali/ --progress /directory/source/ /directory/destination/