PDA

View Full Version : [C#] Gestione dei thread all'interno di un ciclo di "for"


alex783
19-10-2009, 09:56
Ciao a tutti! sto avendo un problema a gestire i thread all'interno di un ciclo di "for".

Mi spiego: il mio obiettivo è di avviare un metodo ogni qualvolta il ciclo di "for" avanza di uno step. Però, per evitare che il metodo blocchi l'esecuzione del programma, avevo pensato di avviarlo in un thread separato. In questo modo, ad ogni ciclo di "for" viene creato un thread che avvia il proprio metodo.

Però non funziona come vorrei e non sto capendo il perché. :muro:
Pubblico il codice perché mi possiate aiutare, se potete. Grazie.


public partial class MyForm: Form
{
private ArrayList listOfThreads = new ArrayList();
int i=0;
int a = 0;
public MyForm()
{

InitializeComponent();
for (i = 0; i < 3; i++)
{
this.listOfThreads.Add(new Thread(delegate()
{
MessageBox.Show("'i' corrente vale: "+i.ToString());
mio_metodo(i);
}));
((Thread)this.listOfThreads[i]).SetApartmentState(ApartmentState.STA);
((Thread)this.listOfThreads[i]).Start();

}



}




}


In sostanza, anziché mostrarmi tre finestre di dialogo "'i' vale: 0", "'i' vale: 1", "'i' vale: 2" e, dopo ognuna delle quali, eseguire il metodo mio_metodo(i) passandogli il valore di "i" corrente, mi mostra due finstre "'i' vale: 2" e una finestra "'i' vale: 3" :mbe: (come fa "i" a raggiungere il valore "3" se il limite al ciclo di "for" è "2"? :muro: ) e mi esegue tre volte il metodo mio_metodo(i) con "i" sempre pari a "3"


HELP, Please! :help:

Thanks so much! ;)

||ElChE||88
19-10-2009, 11:51
È un problema di sincronizzazione.
La variabile i viene incrementata dal ciclo for contemporaneamente all'esecuzione dei vari thread.
Puoi risolvere passando la variabile come argomento ad un metodo (anonimo e non) in modo da crearne una copia.

PS: Usa le collezioni generiche, evita ArrayList come la peste.

MarcoGG
19-10-2009, 21:07
il mio obiettivo è di avviare un metodo ogni qualvolta il ciclo di "for" avanza di uno step. Però, per evitare che il metodo blocchi l'esecuzione del programma, avevo pensato di avviarlo in un thread separato. In questo modo, ad ogni ciclo di "for" viene creato un thread che avvia il proprio metodo.
...
...
(come fa "i" a raggiungere il valore "3" se il limite al ciclo di "for" è "2"? :muro: )


Vero. :D Succede qualcosa di veramente strano. Me lo chiedo anch'io, com'è possibile che i arrivi a 3 ?! :stordita:

Cmq, la risolverei così :

private ArrayList listOfThreads = new ArrayList();

private void mio_metodo(object I)
{
MessageBox.Show("'i' corrente vale: " + I.ToString());
//...
}

for (int i = 0; i < 3; i++)
{
Thread T = new Thread(new ParameterizedThreadStart(mio_metodo));
T.SetApartmentState(ApartmentState.STA);
this.listOfThreads.Add(T);
T.Start(i);
}

alex783
19-10-2009, 21:12
@||ElChE||88: in parte ho risolto... come hai detto tu...

P.S: perché dovrei evitare le ArrayList? le ho sempre usate, mai un problema, e le trovo molto comode


EDIT:
@MarcoGG: uhm... ho visto or ora il tuo intervento, ti ringrazio. Ora provo subito la tua soluzione.

alex783
19-10-2009, 21:32
Marco, ho provato la tua soluzione, ma mi dà questo errore:
"Errore 1 Nessun overload per 'mio_metodo' corrispondente al delegato 'System.Threading.ParameterizedThreadStart'"

Come mai?
Poi, perché nella dichiarazione del metodo mio_metodo hai messo il tipo dato generico object anziché int? è necessario fare questo?

MarcoGG
19-10-2009, 21:40
Marco, ho provato la tua soluzione, ma mi dà questo errore:
"Errore 1 Nessun overload per 'mio_metodo' corrispondente al delegato 'System.Threading.ParameterizedThreadStart'"

Come mai?
Poi, perché nella dichiarazione del metodo mio_metodo hai messo il tipo dato generico object anziché int? è necessario fare questo?

Esatto : private void mio_metodo(object I)
E' richiesto dal ParameterizedThreadStart.

alex783
19-10-2009, 21:47
ora funziona... però mi sa che così sia possibile passare solo un dato al metodo... se avessi bisogno di passare ad esempio due variabili, mi restituisce l'errore di prima... certo, potrei passare un arraylist che contiene le diverse variabili che mi servono, ma non la trovo molto elegante come soluzione.

Grazie comunque, per ora questa soluzione va benone! ;)

gugoXX
19-10-2009, 21:57
Il perche' stampi sempre 3 con il tuo metodo e' presto detto.
Nonostante tu abbia lanciato ciascuno Thread dentro il ciclo, i Thread partono e processati schedulati quando vuole lo shcedulatore.
Il tuo output significa che da ciascuno viene raggiunta l'istruzione
MessageBox.Show("'i' corrente vale: "+i.ToString());
quando in realta' il ciclo chiamante e' gia' terminato, ovvero quando la i vale gia' 3.

Per come risolvere problemi come questo, ci sono tanti modi. Alcuni eleganti, alcuni efficienti, alcuni preferiti.


Relativamente a .net 2.0 / 3.5 puro, per eseguire un metodo piu' volte in maniera asincrona, non e' necessario scomodare i Thread, ma bastano i delegate (che tanto usi comunque per dichiarare il Thread, quindi un passo in meno)

Senza scomodare .Net4.0 o 3.5 con le parallel extensions, sotto 3.5 puro lo scrirverei cosi', (sotto 2.0 risulta solo un po' piu' verboso).

for (int i = 0; i < 3; i++)
{
Action<int> newAct = new Action<int>{ i=> MetodoDaEseguire(i)};
newAct.BeginInvoke(i, null, null);
}

Non stampare nulla a video nei MetodoDaEseguire (come quelle MessageBox)Se devi fare uscire qualcosa a video (tipicamente no), allora usa i Dispatcher per sincronizzare.
Male forzare ApartmentState, dato che non e' assicurato il risutalto, ora e' deprecata e occorrerebbe usare TrySetApartmentState (che come dice il nome non ne assicura di per se il risultato)

Per un compito come questo non userei Thread o ThreadPool, sebbene sia possibile.

MarcoGG
19-10-2009, 21:58
ora funziona... però mi sa che così sia possibile passare solo un dato al metodo... se avessi bisogno di passare ad esempio due variabili, mi restituisce l'errore di prima... certo, potrei passare un arraylist che contiene le diverse variabili che mi servono, ma non la trovo molto elegante come soluzione.


Non "un dato", un oggetto, con tutti i vantaggi che comporta.
Puoi passare una List di Objects e nel Metodo eseguire i cast opportuni. Elegante o meno, è la soluzione giusta per questo tipo di approccio.
Altra soluzione è avere l'istanza di una classe che esponga il Metodo. I campi della classe sono i "parametri", e una volta impostati, si lancia il Thread su quel Metodo, senza passare alcun parametro, e senza bisogno di ParameterizedThreadStart.

In ogni caso ParameterizedThreadStart è opzionale, in quanto il compilatore è in grado di utilizzarlo da solo, se rileva il passaggio di un argomento. Questo codice funziona ugualmente :

Thread T = new Thread(mio_metodo);
T.SetApartmentState(ApartmentState.STA);
this.listOfThreads.Add(T);
T.Start(i);

alex783
19-10-2009, 22:16
Il perche' stampi sempre 3 con il tuo metodo e' presto detto.
Nonostante tu abbia lanciato ciascuno Thread dentro il ciclo, i Thread partono e processati schedulati quando vuole lo shcedulatore.
Il tuo output significa che da ciascuno viene raggiunta l'istruzione
MessageBox.Show("'i' corrente vale: "+i.ToString());
quando in realta' il ciclo chiamante e' gia' terminato, ovvero quando la i vale gia' 3.

Per come risolvere problemi come questo, ci sono tanti modi. Alcuni eleganti, alcuni efficienti, alcuni preferiti.


Relativamente a .net 2.0 / 3.5 puro, per eseguire un metodo piu' volte in maniera asincrona, non e' necessario scomodare i Thread, ma bastano i delegate (che tanto usi comunque per dichiarare il Thread, quindi un passo in meno)

Senza scomodare .Net4.0 o 3.5 con le parallel extensions, sotto 3.5 puro lo scrirverei cosi', (sotto 2.0 risulta solo un po' piu' verboso).

for (int i = 0; i < 3; i++)
{
Action<int> newAct = new Action<int>{ i=> MetodoDaEseguire(i)};
newAct.BeginInvoke(i, null, null);
}

Non stampare nulla a video nei MetodoDaEseguire (come quelle MessageBox)Se devi fare uscire qualcosa a video (tipicamente no), allora usa i Dispatcher per sincronizzare.
Male forzare ApartmentState, dato che non e' assicurato il risutalto, ora e' deprecata e occorrerebbe usare TrySetApartmentState (che come dice il nome non ne assicura di per se il risultato)

Per un compito come questo non userei Thread o ThreadPool, sebbene sia possibile.

Ho dimenticato di specificare ad inizio del thread che ho necessità di restare relegato a .NET 2.0.

L'istruzione MessageBox.Show chiaramente non c'è nel metodo vero e proprio che uso nella mia applicazione, ma è servito a me per capire il comportamente del metodo. ;)

Credo che l'uso dei thread sia necessario al mio caso, dal momento che metodoDaEseguire contiene codice per gestire DotNetZip, quindi ho necessità di dare all'utente la possibilità - ad esempio - di sospendere momentaneamente una compressione ZIP.

Grazie per la spiegazione del mio problema, ma comunque non capisco come faccia "i" ad assumere il valore "3" se il limite imposto nel ciclo di "for" è 2. Boh, mi sembra incomprensibile... :boh:

alex783
19-10-2009, 22:23
Non "un dato", un oggetto, con tutti i vantaggi che comporta.
Puoi passare una List di Objects e nel Metodo eseguire i cast opportuni. Elegante o meno, è la soluzione giusta per questo tipo di approccio.
Altra soluzione è avere l'istanza di una classe che esponga il Metodo. I campi della classe sono i "parametri", e una volta impostati, si lancia il Thread su quel Metodo, senza passare alcun parametro, e senza bisogno di ParameterizedThreadStart.

In ogni caso ParameterizedThreadStart è opzionale, in quanto il compilatore è in grado di utilizzarlo da solo, se rileva il passaggio di un argomento. Questo codice funziona ugualmente :

Thread T = new Thread(mio_metodo);
T.SetApartmentState(ApartmentState.STA);
this.listOfThreads.Add(T);
T.Start(i);

Grazie ancora! in effetti è molto interessante la possibilità di poter passare un oggetto e non un semplice tipo valore. Comunque devo rivedere un po' questa parte del mio programma, anzi, mi sa che la devo ripensare, anche perché in questo modo poi andrei a scontrarmi con una eccezione di cross-thread (o almeno immagino, visto che poi da quei thread, ho necessità di modificare i controlli, in particolare le progressbar , di una form istanziata da un thread diverso :D )

alex783
19-10-2009, 22:43
mi chiedevo... ma l'eventuale eccezione di cross-thread, potrei risolverlo con i delegati, giusto? :D

domani mattina provo, ora vado a nanna... grazie a tutti! ;)

gugoXX
19-10-2009, 23:07
Grazie per la spiegazione del mio problema, ma comunque non capisco come faccia "i" ad assumere il valore "3" se il limite imposto nel ciclo di "for" è 2. Boh, mi sembra incomprensibile... :boh:

Vediamo se la capisci cosi:

for (int i=0;i<3;i++)
{
//Fai qualunque cosa, volendo anche nulla
}
Console.WriteLine(i);


Cosa viene stampato a video?

tomminno
19-10-2009, 23:38
P.S: perché dovrei evitare le ArrayList? le ho sempre usate, mai un problema, e le trovo molto comode


Il principale motivo è che ArrayList è una collezione di object (retaggio del vecchio .NET 1.x), con tutto quello che comporta, boxing e unboxing di tipi valore, rischio di infilarci oggetti di natura differente.
Usa List<Thread> e non te ne pentirai.

alex783
20-10-2009, 08:08
Vediamo se la capisci cosi:

for (int i=0;i<3;i++)
{
//Fai qualunque cosa, volendo anche nulla
}
Console.WriteLine(i);


Cosa viene stampato a video?

Vero. E sì, perché il ciclo "for" funziona così:
i=0 --> è minore di 3? si, allora aggiungi +1
i=1 --> è minore di 3? si, allora aggiungi +1
i=2 --> è minore di 3? si, allora aggiungi +1
i=3 --> è minore di 3? no, esci dal ciclo

Grazie mille, non ci avevo proprio pensato. ;)


Il principale motivo è che ArrayList è una collezione di object (retaggio del vecchio .NET 1.x), con tutto quello che comporta, boxing e unboxing di tipi valore, rischio di infilarci oggetti di natura differente.
Usa List<Thread> e non te ne pentirai.

Capito. Beh, allora cambio, anche per imparare e usare qualcosa di nuovo :D
Grazie!