C#/Winforms Delegate Invoke gaat 2e keer fout.

Pagina: 1
Acties:

Acties:
  • 0 Henk 'm!

  • roy-t
  • Registratie: Oktober 2004
  • Laatst online: 08-09 11:33
Voor een wat uitgebreide WinForms applicatie heb ik de volgende opzet.

ListenServer (in appart thread) die wacht tot er een bericht komt, zodra er iets binnenkomt wordt dit bericht per event afgehandeld door Program.cs dat zelf weer een event stuurt naar het Form dat een abonnement heeft op dit event.

In dit event wordt vaak een form gesloten en een nieuw form gemaakt. Omdat de code om dit te doen niet in het Thread van Program.cs zit moet dit ge-invoked worden. Dat is natuurlijk allemaal vrij logisch:

C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
private delegate void VoidDelegate();

        private void Program_OnEv()
        {
            VoidDelegate j = delegate()
            {
                Program.currentForm = new Form2();
                Program.currentForm.Show();
                Program.OnEv -= Program_OnEv;
                this.Hide();
            };
            this.Invoke(j);
        }


Dit gebeurt in het mainForm (nu Program.mainForm).

Daarna wordt via een zelfde delegate in 'currentForm' het eigen scherm gesloten.
C#:
1
2
3
4
5
6
7
8
9
10
        private void Program_OnEv()
        {
            VoidDelegate j = delegate()
            {
                Program.mainForm.Reset();
                this.Close();                
            };

            this.invoke(j);
        }


Dit gaat de eerste keer goed. Het mainForm blijft gewoon actief (wordt niet gesloten). Uiteindelijk roept een event in het mainform weer dezelfde delegate als helemaal boven aan. Dit gaat ook goed. Het tweede forms' delegate wordt na een tijdje ook aangeroepen, maar nu gaat het fout. Ik krijg de volgende fout:

"Invoke or BeginInvoke cannot be called on a control until the window handle has been created."
Maar dit event dat de delegate triggered gebeurt pas 2,5 seconden nadat ik het form zie (er zelfs op kan klikken enzo). En ik gebruik this, wat niet kan bestaan als het form nog niet helemaal gemaakt is.

Om dit op te loseen heb ik de laatste regel vervangen (na flink wat geklooi).
C#:
1
2
this.Invoke(j); //oud
Program.mainForm.Invoke(j); //nieuw


Nu gaat het 'allemaal' goed, het tweede form wordt weer netjes weergegeven. Maar nu komt het rare, door deze verandering blijft het Form actief op een of andere manier. En er gebeuren allemaal rare dingen, in een van de delegates wordt ++variabele gedaan, en ondanks dat het form al vernietigd moet zijn en opnieuw gecreeerd wordt bij het opnieuw creeeren op een of andere manier toch deze variable weer geset op de allerlaatste waarde. Het lijkt wel alsof variabelen ook aangesproken in delegates worden hergebruikt (ondanks dat er duidelijk new staat in de aanroep van het form). Dit heb ik zelfs met debuggen ondervonden. Ook lijkt het form niet goed gesloten te worden, alsof de delegates het form niet kunnen sluiten.

Op een gegeven moment had ik een video op het form, de eerste keer ging altijd goed maar halverwege de 2e weergave werd telkens het paneel waar de video op zat zomaar gedisposed (was immers gesloten). Is er iets geks met delegates aan de hand waardoor ze geheugen hergebruiken? Ik gebruik nergens unsafe code, en in de aanroep staat echt new. Ik heb een testprogrammatje gemaakt (waar ik nu code uit geciteerd heb). En daar gaat bijna alles ook in fout, behalve dat variabele niet gereset worden. (Het scherm blijft wel actief want het blijft reageren op events.).

Om geen bugs over het hoofd te zien hier nog even de code uit de relevante scherm van het echt programma
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//Uit scherm dat het form maakt, wat zelf ook telkens opnieuw gemaakt wordt.
 DelegateNull j = delegate()
                {
                    Program.currentForm = new frmQuestion();                    
                    Program.currentForm.Show();
                    timer.Stop();
                    this.Close();
                };
                Program.debateForm.Invoke(j); //hoofdscherm dat er altijd is

//Uit frmQuestion zelf, dat zichzelf via deze delegate sluit en weer doorgaat naar een ander scherm.
DelegateNull j = delegate()
                {
                    frmResult result = new frmResult();
                    result.SetResults(message.Substring(8));
                    Program.currentForm = result;
                    result.Show();
                    this.Close();                    
                };
                Program.debateForm.Invoke(j);


Uiteindelijk kom je via frmResult weer terug in het nog openstaande frmDebate, dat via een nieuw frmJoin scherm weer een nieuw frmQuestion aan maakt. Dat nieuwe frmQuestion (dat echt niet gehide werd zoals te zien is). Houd oude variabele en wordt halverwege ineens gedisposed.


Heeft iemand enig idee waar dit rare gedrag door kan komen? (Door dat ik het thread waar Program.cs in draait gebruik misschien?) Waardoor de foutmelding komt. (Al gegoogled, maar daar komt niet echt wat relevants uit). Of nog beter, eigenlijk wil ik van deze hele Thread->Thread->Delegate structuur af. Hoe zorg ik ervoor dat ik (via events) berichten naar mijn forms kan sturen vanaf een appart thread, zonder dat ik via delegates allemaal rare dingen moet doen. Ik kan de berichten wel ergens anders gaan parsen ofzo, mocht dat alles makkelijker maken.

Zelf zie ik het probleem gewoon niet zitten, ook na een dagje denken niet.

[ Voor 5% gewijzigd door roy-t op 25-11-2009 22:23 . Reden: Meer info hergebruik variabele die in delegates voorkomen. ]

~ Mijn prog blog!


Acties:
  • 0 Henk 'm!

  • pedorus
  • Registratie: Januari 2008
  • Niet online
Eigenlijk begrijp ik nog niet helemaal welke threads en welke methodes er in nu het spel zijn. Het lijkt erop alsof dingen niet in de juiste volgorde gebeuren, of de verkeerde variabelen worden gebruikt of gewijzigd. Even voor je idee: wat denk je dat deze code doet?
C#:
1
2
3
            for (int i = 1; i < 10; i++)
                ThreadPool.QueueUserWorkItem(_ => Invoke((Action)(()=> 
                    MessageBox.Show(i.ToString()))));

Vitamine D tekorten in Nederland | Dodelijk coronaforum gesloten


Acties:
  • 0 Henk 'm!

  • roy-t
  • Registratie: Oktober 2004
  • Laatst online: 08-09 11:33
pedorus schreef op woensdag 25 november 2009 @ 22:56:
Eigenlijk begrijp ik nog niet helemaal welke threads en welke methodes er in nu het spel zijn. Het lijkt erop alsof dingen niet in de juiste volgorde gebeuren, of de verkeerde variabelen worden gebruikt of gewijzigd. Even voor je idee: wat denk je dat deze code doet?
C#:
1
2
3
            for (int i = 1; i < 10; i++)
                ThreadPool.QueueUserWorkItem(_ => Invoke((Action)(()=> 
                    MessageBox.Show(i.ToString()))));
Sorry welke volgordes zijn niet duidelijk? Schematisch gaat het zo:

ListenServer Thread->Program.cs Thread->Invoke op FormThread->Nieuw Form, (en dan begint het na een tijdje weer opnieuw).

Wat betreft jouw code, ik ben niet al te bekend met lambda's, maar ik neem aan dat er gewoon een messagebox gezien laat worden door het thread waarin deze code geschreven is. De te verwachten output zou 1,2,3,4,5,6,7,8,9,10 , 1,3,5,2,6,8,10,9.. (random dus) zijn.

Edit: had even niet goed nagedacht bij het voorbeeldje, tuurlijk komt alles random terug, maar dat zou niet mogen schelen in mijn geval (zo post verder op).

[ Voor 9% gewijzigd door roy-t op 25-11-2009 23:27 ]

~ Mijn prog blog!


Acties:
  • 0 Henk 'm!

  • pedorus
  • Registratie: Januari 2008
  • Niet online
Als Program.cs geen windows form thread is, en je hiernaartoe 'Post', kijk dan eens naar \[C#] Nested backgroundworkers geven out-of-order resultaten. Helaas niet het meest simpele onderwerp. ;)

Verder zijn lambda's gewoon een andere manier van opschrijven dan delegates. Je kan het ook zo opschrijven, met exact hetzelfde resultaat:
C#:
1
2
3
4
5
6
7
8
            for (int i = 1; i < 10; i++)
                ThreadPool.QueueUserWorkItem(delegate(object _)
                {
                    Invoke((Action)delegate()
                    {
                        MessageBox.Show(i.ToString());
                    });
                });

Op zich klopt je idee van terugposten, maar ik kan je alvast verklappen dat je antwoord in bijna geen enkel geval klopt (afgezien van die off-by-one). :)

Vitamine D tekorten in Nederland | Dodelijk coronaforum gesloten


Acties:
  • 0 Henk 'm!

  • FireDrunk
  • Registratie: November 2002
  • Laatst online: 20:40
Volgens mij heeft het iig veel te maken met het interne threading model van .NET.
Dit is dus niet fifo, maar volgens mij Shortest-Service-Time first... (De thread die het minst te doen heeft, mag eerst). En soms komt er dus wel eens wat door elkaar ;)

Even niets...


Acties:
  • 0 Henk 'm!

  • roy-t
  • Registratie: Oktober 2004
  • Laatst online: 08-09 11:33
pedorus schreef op woensdag 25 november 2009 @ 23:18:
Als Program.cs geen windows form thread is, en je hiernaartoe 'Post', kijk dan eens naar \[C#] Nested backgroundworkers geven out-of-order resultaten. Helaas niet het meest simpele onderwerp. ;)

Verder zijn lambda's gewoon een andere manier van opschrijven dan delegates. Je kan het ook zo opschrijven, met exact hetzelfde resultaat:
C#:
1
2
3
4
5
6
7
8
            for (int i = 1; i < 10; i++)
                ThreadPool.QueueUserWorkItem(delegate(object _)
                {
                    Invoke((Action)delegate()
                    {
                        MessageBox.Show(i.ToString());
                    });
                });

Op zich klopt je idee van terugposten, maar ik kan je alvast verklappen dat je antwoord in bijna geen enkel geval klopt (afgezien van die off-by-one). :)
Ohja, doh! Tuurlijk komen ze niet in volgorde terug... Stom dat ik daar niet aan dacht. Maar dat is ook helemaal het probleem niet, ik wact in een form op 1 of 2 specifieke berichten, de andere discard ik, en zodra ik een bericht ontvang dat ik wil hebben reageer ik niet meer op andere berichten, misschien dat ik nog even een Monitor.Enter en een Monitor.Exit ergens neer kan zetten, maar ook in mijn test project gaat het fout, en daar wordt precies elke 2500ms een 'bericht' ontvangen, dus is het niet zo dat 2 berichten tegelijkertijd met delegates gaan klooien.

~ Mijn prog blog!


Acties:
  • 0 Henk 'm!

  • pedorus
  • Registratie: Januari 2008
  • Niet online
thijs_cramer schreef op woensdag 25 november 2009 @ 23:24:
Volgens mij heeft het iig veel te maken met het interne threading model van .NET.
Dit is dus niet fifo, maar volgens mij Shortest-Service-Time first... (De thread die het minst te doen heeft, mag eerst). En soms komt er dus wel eens wat door elkaar ;)
Helaas heeft .NET geen glazen bol en kan dit dus niet, of zie ik iets over het hoofd? :)
roy-t schreef op woensdag 25 november 2009 @ 23:26:
Ohja, doh! Tuurlijk komen ze niet in volgorde terug...
Dat zal veelal kloppen op een multi-core processor, maar welke getallen worden er nu weergegeven? Ik zou het toch maar even testen. ;)

Vitamine D tekorten in Nederland | Dodelijk coronaforum gesloten


Acties:
  • 0 Henk 'm!

  • roy-t
  • Registratie: Oktober 2004
  • Laatst online: 08-09 11:33
pedorus schreef op woensdag 25 november 2009 @ 23:33:
[...]

Helaas heeft .NET geen glazen bol en kan dit dus niet, of zie ik iets over het hoofd? :)

[...]

Dat zal veelal kloppen op een multi-core processor, maar welke getallen worden er nu weergegeven? Ik zou het toch maar even testen. ;)
Hmm 10,10,10... etc.. Zeer onverwacht tbh, na wat nadenken zou ik nog wel kunnen bedenken dat er NAN of 0 uit zou komen (of een exception), immers bestaat i niet meer in de context waar het thread wordt uitgevoerd, waar komt i dan vandaan?

Na even nadenken, kan ik het probleem hier helaas niet mee oplossen, (er is echt maar 1 thread wat af en toe die delegate called) Wel heb ik wat code aangepast na het zoeken op andere bronnen over delegates, en daar kwam ik op het volgende vreemde fenomeen, wat misschien mijn problemen veroorzaakt, maar ik snap niet helemaal hoe ik dit kan oplossen / omzeilen. Hier komt toch ook jouw voorbeeld weer aan bod.

C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//In currentForm zit nu deze code:
private void Program_OnEv()
        {
            if (this.InvokeRequired)
            {
                MethodInvoker mi = new MethodInvoker(Program_OnEv);
                this.Invoke(mi);
            }
            else
            {
                Monitor.Enter(this);

                Program.currentForm = new Form2();
                Program.currentForm.Show();
                Program.OnEv -= Program_OnEv;
                this.Hide();

                Monitor.Exit(this);
            }
        }
//In form2 zit nu deze:
private void Program_OnEv()
        {
            if (this.InvokeRequired) // || Program.mainForm.InvokeRequired) de 2e keer is deze or nodig
            {
                MethodInvoker mi = new MethodInvoker(Program_OnEv);
                this.Invoke(mi); 
//de tweede keer dat dit wordt weergeven hier een 'no handler exception' gegeven
//ondanks dat het nieuwe form2 nog niet zichtbaar was, en er nog geen bericht zou komen tot na 2500ms
            }
            else
            {
                Monitor.Enter(this);

                Program.mainForm.Reset(); //zorgt ervoor dat currentForm weer luistert naar events.
                Program.mainForm.Show();
                this.Close();

                Monitor.Exit(this);
            }            
        }


Hoe ik denk hoe het werkt:
Program.cs Thread -> Kijkt of er een invoke nodig is op currentForm (Ja). currentForm Thread maakt een nieuw form2, laat die zien en verbergt zichzelf.
----tijdje later
Program.cs Thread-> Kijkt of er een invoke nodig is op form2 (Ja). form2 Thread zorgt ervoor dat currentForm weer luistert naar berichten, showed deze en sluit daarna zichzelf.

Ik verwachte dat dit keer op keer zo door zou gaan.

Het lijkt er echter op dat:
Program.cs Thread -> Kijkt of er een invoke nodig is op currentForm (Ja). currentForm Thread maakt een nieuw form2, laat die zien en verbergt zichzelf. form2 is nu van het thread dat de delegate uitvoerde (currentForm dus)
----tijdje later
Program.cs Thread-> Kijkt of er een invoke nodig is op thread van nieuwe form2 (Ja) currentFormThread maakt een nieuwe delegate aan, die sluit form2 en showed form1.
--Weer opnieuw
Nu moet er weer een form2 gemaakt worden, dit gaat fout met een foutmelding in de delegate van form2, terwijl form2 er nog niet zou moeten zijn. Ook blijkt dat de form2 die er nog niet is geen window handle heeft (logisch) en dat dit mysterieuze form geen invoke nodig heeft, maar dat de delegate toch niet uitgevoerd kan worden omdat het thread van currentForm, wat blijkbaar een ander thread is wel een invoke nodig heeft en nog steeds de owner is van het mysterieuze form2.


De vraag is nu, hoe voorkom ik dit?

(Dit was wel een erg sneaky bug, pas na een tijdje had ik door dat het al fout gaat bij de 2e keer creeren van het form2, ipv bij het handlen van de message door de nieuwe form2, maar blijkbaar kom ik per ongeluk verkeerd uit, op het GC thread ofzo?... |:( )

Edit: om mijn vermoeden te bevestigen heb ik in de delegate van form2 die het form sluit ook de regel:
C#:
1
Program.OnEv -= this.Program_OnEv;

toegevoegd, nu blijft het heen en weer gaan wel goed gaan, maar dit zit me niet lekker, want blijkbaar wordt het form niet goed verwijdert/gesloten als een delegate dit doet, immers zou close er voor moeten zorgen dat het form verdwijnt en ook geen berichten/events what so ever kan ontvangen Nog even extra .dispose() roepen werkt ook niet.. hoe kan dit?

Edit2: nog even alle referenties gechecked, alles genulled en dispose() en GC.Collect() maar zelfs dan gaat het door.

[ Voor 19% gewijzigd door roy-t op 26-11-2009 00:05 ]

~ Mijn prog blog!


  • pedorus
  • Registratie: Januari 2008
  • Niet online
Misschien toch eens nalezen hoe closures enzo nou werken in c#, maar dat was denk ik toch niet het probleem. ;) Je hebt denk ik iets als:
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
using System;
using System.Windows.Forms;
using System.Threading;

static class Program
{
    public static event Action Ev;
    public static FormA mainForm = new FormA() { Text = "a" };
    public static FormB currentForm;
    public static int i;

    public class FormA : Form
    {
        public void On_Ev()
        {
            if (this.InvokeRequired)
            {
                Invoke((Action)On_Ev);
                return;
            }
            Hide();
            (currentForm = new FormB() { Text = (++i).ToString() }).Show();
            Ev -= On_Ev;
            Ev += currentForm.On_Ev;
        }
    }
    public class FormB : Form
    {
        public void On_Ev()
        {
            if (this.InvokeRequired)
            {
                Invoke((Action)On_Ev);
                return;
            }
            Close();
            mainForm.Show();
            //Ev -= On_Ev;
            Ev += mainForm.On_Ev;
        }
    }
    static void Main()
    {
        Ev += mainForm.On_Ev;
        new Thread(() =>
        {
            while (true)
            {
                Ev();
                Thread.Sleep(1000);
            }
        }) { IsBackground=true }.Start();
        Application.Run(mainForm);
    }
}

Zolang er maar 1 handler tegelijkertijd verbonden is aan het event Ev gaat dit goed, nadat je regel 38 hebt geuncomment. De Monitors die je gebruikt zijn niet nodig, omdat je al in de UI-Thread zit, en de andere Thread op dat moment aan het wachten is op de UI-Thread. Sowieso kun je beter gewoon lock gebruiken. Maar dit kan alsnog problemen opleveren, als je bijvoorbeeld FormB wegklikt. Het is beter om het event gelijk vanuit de UI-Thread aan te roepen. Dus:
C#:
49
                mainForm.Invoke(Ev); 

Lijkt me sowieso een veel betere oplossing, want dan heb je ook geen InvokeRequired meer nodig, en ontstaat er ook geen probleem als je FormB wegklikt. Verder lijkt me hier een event en -= en += niet handig, en kun je beter een gewone Action en '=' is gebruiken. Er wordt overigens ook niet automatisch -= op events uitgevoerd als er een handler gedisposed wordt.

Overigens kan dit voorbeeld theoretisch eindigen in een crash, als je de mainForm precies op het juiste moment wegklikt (even getest, en het kan als je Sleep() en Hide() weghaalt.).

Vitamine D tekorten in Nederland | Dodelijk coronaforum gesloten


Acties:
  • 0 Henk 'm!

  • roy-t
  • Registratie: Oktober 2004
  • Laatst online: 08-09 11:33
pedorus schreef op donderdag 26 november 2009 @ 23:18:
Misschien toch eens nalezen hoe closures enzo nou werken in c#, maar dat was denk ik toch niet het probleem. ;) Je hebt denk ik iets als:
C#:
1
...

Zolang er maar 1 handler tegelijkertijd verbonden is aan het event Ev gaat dit goed, nadat je regel 38 hebt geuncomment. De Monitors die je gebruikt zijn niet nodig, omdat je al in de UI-Thread zit, en de andere Thread op dat moment aan het wachten is op de UI-Thread. Sowieso kun je beter gewoon lock gebruiken. Maar dit kan alsnog problemen opleveren, als je bijvoorbeeld FormB wegklikt. Het is beter om het event gelijk vanuit de UI-Thread aan te roepen. Dus:
C#:
49
                mainForm.Invoke(Ev); 

Lijkt me sowieso een veel betere oplossing, want dan heb je ook geen InvokeRequired meer nodig, en ontstaat er ook geen probleem als je FormB wegklikt. Verder lijkt me hier een event en -= en += niet handig, en kun je beter een gewone Action en '=' is gebruiken. Er wordt overigens ook niet automatisch -= op events uitgevoerd als er een handler gedisposed wordt.

Overigens kan dit voorbeeld theoretisch eindigen in een crash, als je de mainForm precies op het juiste moment wegklikt (even getest, en het kan als je Sleep() en Hide() weghaalt.).
Bedankt Pedorus voor het nog verder uitzoeken en duidelijker maken van het probleem. Ik zal inderdaad in Program.cs een methode maken die ervoor zorgt dat er geinvoked wordt zodat dat maar op 1 plek gebeurd ipv overal nu (nasty) ook zal ik er voor zorgen dat de eventslisteners meteen stoppen met luisteren nadat ze zichzelf proberen te sluiten, toch frapant dat ik daar niet eerder aan gedacht had, maarja ik dacht natuurlijk dat het form al lang weg zou zijn voor het nieuwe event. (Aan de andere kant, een += doen bij een event geeft natuurlijk ook een referentie naar dat form, dus die kan niet opgeruimd worden).

Bedankt voor je hulp!

~ Mijn prog blog!

Pagina: 1