[C#] Events aanroepen vanuit verschillende threads

Pagina: 1
Acties:

Onderwerpen


Acties:
  • 0 Henk 'm!

  • Mnstrspeed
  • Registratie: Oktober 2007
  • Laatst online: 20:11
(jarig!)
Hee :)

Ik ben atm bezig met een simpele chatserver class library om te gebruiken in toekomstige projecten. Ik ben echter tegen een probleem aangelopen bij het gebruiken van meerdere threads: Zodra ik het StatusChanged() event aanroep vanuit een andere thread, komt deze ook in een andere thread aan @ form1.cs, waardoor het niet mogelijk is om formcontrols direct te bewerken. Ik wil echter voorkomen dat ik this.Invoke() moet gaan gebruiken in Form1.cs (om het simpel en overzichtelijk te houden), maar in chatserver.dll kan ik geen this.Invoke() gebruiken, omdat het geen form is... Is er een andere manier om ervoor te zorgen dat events via de main thread worden 'verzonden' vanuit chatserver.dll?

Hieronder een versimpelde/verkorte versie van het geheel:


C#: chatserver.dll
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
public delegate void StatusChangedEventHandler(object sender, StatusChangedEventArgs e);

public struct StatusChangedEventArgs
{
   public String Status;
   
   public StatusChangedEventArgs(string _status)
   {
      this.Status = _status;
   }
}

public class ChatServer
{
   public event StatusChangedEventHandler StatusChanged;

   public ChatServer()
   { }

   public void Start()
   {
      ThreadStart ts = delegate { StartServer(); };
      Thread td = new Thread(ts);
      td.Start();
   }

   private void StartServer()
   {
      StatusChanged(this, new StatusChangedEventArgs("Online"));
   }
}

C#: form1.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Form1()
{
   ChatServer chat = new ChatServer();
   chat.StatusChanged += new StatusChangedEventHandler(Form1_StatusChanged);
   chat.Start();
}

public void Form1_StatusChanged(object sender, StatusChangedEventArgs e)
{
   txtLog.AppendText("Status changed to " + e.Status);  //  txtLog is een RichTextBox

   // InvalidOperationException was unhandled
   //    Het is niet toegestaan een bewerking uit te voeren via verschillende threads: er
   //    werd vanaf een andere thread toegang gekregen tot het besturingselement logTxt
   //    dan de thread waarop het element is aangemaakt.
}


Ik hoop dat iemand hier me met dit probleem kan helpen, want ik zit er nu al redelijk lang mee te prutsen :X

Ehhh wat?


Acties:
  • 0 Henk 'm!

  • whoami
  • Registratie: December 2000
  • Laatst online: 01:56
Je moet er in je ChatServer voor zorgen dat je event op de correcte manier geraised wordt.
Dit kan je op verschillende manieren doen:
- door te checken of de Target van je delegate ISynchronizeInvoke implementeerd, en indien dit het geval is, checken of er moet ge-invoked worden.
- door gebruik te maken van de AsyncOperationManager & AsyncOperation classes.

(Zowiezo is je code nu ook niet goed, aangezien je die event zowiezo raised, ook als er geen eventhandler aan gekoppeld is, op die manier krijg je een NullRef exception).

Waarom maak je ook geen gebruik van de generische EventHandler<T> delegate ?
EventArgs classes moeten ook afgeleid worden van de EventArgs class.

Eigenlijk is er in dit forum al heel wat over dit onderwerp geschreven, maar allé:
manier 1:
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
protected void OnStatusChanged(string status)
{
   EventHandler<StatusChangedEventArgs> handler = StatusChanged;

   if( handler != null )
   {
        Delegate[] dels = handler.GetInvocationList();

        foreach( Delegate d in dels )
        {
             ISynchronizeInvoke target = d as ISynchronizeInvoke;

             if( target != null && target.InvokeRequired )
             {
                  target.BeginInvoke (d, new object[] {this, new StatusChangedEventArgs(status));
             }
             else
             {
                  d.DynamicInvoke ( ... );
             }
        }
   }
}

Zo ongeveer (ff uit het blote hoofd).

Bij manier 2 moet je een AsyncOperation creeëren, en daar dan met SendOrPostCallback je event gaan raisen.

https://fgheysels.github.io/


Acties:
  • 0 Henk 'm!

  • Matis
  • Registratie: Januari 2007
  • Laatst online: 17-09 18:39

Matis

Rubber Rocket

Toevallig liep ik vorige week tegen hetzelfde probleem aan.

Het probleem is dus, zoals jij schetst het feit dat je niet direct mag schrijven vanuit een thread naar een andere thread, ivm het readers/writers probleem.

Ik heb hiervoor hetvolgende gemaakt (gejat :P)

C#: PacketReceiver
1
2
 
GUI.SetControlPropertyValue(GUI.textBox1,"Text","text om te schrijven naar textbox 1");



C#: GUI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 
delegate void SetControlValueCallback(Control oControl, string propName, object propValue);
          public void SetControlPropertyValue(Control oControl, string propName, object propValue)
          {
               if (oControl.InvokeRequired)
               {
                    SetControlValueCallback d = new SetControlValueCallback(SetControlPropertyValue);
                    oControl.Invoke(d, new object[] { oControl, propName, propValue });
               }
               else
               {
                    Type t = oControl.GetType();
                    PropertyInfo[] props = t.GetProperties();
                    foreach (PropertyInfo p in props)
                    {
                         if (p.Name.ToUpper() == propName.ToUpper())
                         {
                              p.SetValue(oControl, propValue, null);
                         }
                    }
               }
          }


Zoals je misschien wel begrijpt roep ik vanuit de PacketReceiver klasse de methode SetControlPropertyValue() in de GUI klasse aan. De SetControlPropertyValue() methode draait in dezelfde thread als de GUI, waardoor er geen readers/writers probleem kan ontstaan.

Ik hoop dat dit jouw probleem oplost!

[ Voor 12% gewijzigd door Matis op 05-10-2008 22:12 ]

If money talks then I'm a mime
If time is money then I'm out of time


Acties:
  • 0 Henk 'm!

  • whoami
  • Registratie: December 2000
  • Laatst online: 01:56
Hmm, ik vind die method wel wat overkill eigenlijk ... Generiek is het wel, maar ik ga zowiezo liever al gaan bepalen in de class die de event raised, of de eventhandler moet geinvoked worden of niet.

https://fgheysels.github.io/


Acties:
  • 0 Henk 'm!

  • Devion
  • Registratie: Januari 2000
  • Laatst online: 28-02 15:59

Devion

Space for rent ;-)

Tja... het bestaat maar het valt buiten elke fatsoenlijke design en implementatie...

CheckForIllegalCrossedThreadCalls = false

Acties:
  • 0 Henk 'm!

  • whoami
  • Registratie: December 2000
  • Laatst online: 01:56
:X
Dat is de symptomen aanpakken ipv de oorzaak .... De vuiligheid onder mat vegen, en zeggen dat je huis proper is.

https://fgheysels.github.io/


Acties:
  • 0 Henk 'm!

  • writser
  • Registratie: Mei 2000
  • Laatst online: 16-09 10:48
Op dit moment ben ik bezig met een soortgelijk probleem. Ik ben wat aan het programmeren voor een multi-touch scherm (voor meer details zie hier: http://forums.xna.com/forums/t/18089.aspx). Jouw code ziet er interessant uit whoami. Ik heb 't als volgt geimplementeerd voor mijn project:
C#:
1
2
3
        // Raised whenever the multi-touch screen is touched
        public delegate void TouchMovedHandler(int id, float x, float y);
        public event TouchMovedHandler TouchMoved;

En dit event wordt als volgt gebruikt:
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
]
                    if (TouchMoved != null)
                    {
                        Delegate[] dels = TouchMoved.GetInvocationList();
                        foreach (Delegate d in dels)
                        {
                            ISynchronizeInvoke target = d as ISynchronizeInvoke;
                            if (target != null && target.InvokeRequired)
                            {
                                target.BeginInvoke (d, new object[] {id, x, y});
                            }
                            else
                            {
                                d.DynamicInvoke(new object[] {id, x, y });                                
                            }
                        }
                    }

In de andere context abonneer ik me op dit event:
C#:
1
            tp.TouchMoved += new TUIOprocessor.TouchMovedHandler(touchMoved);

Alleen, als ik dit run is target altijd gelijk aan null en wordt steeds de DynamicInvoke aangeroepen. Deze draait, voorzover ik kan zien, nog steeds in dezelfde thread als de raiser van het event. Wat dus precies is wat ik niet wil :P. Wat doe ik hier verkeerd?

Onvoorstelbaar!


Acties:
  • 0 Henk 'm!

  • Flard
  • Registratie: Februari 2001
  • Laatst online: 12-09 16:16
Blijkbaar is InvokeRequired dus false.
En InvokeRequired is true als de thread van de caller niet gelijk is aan de UI thread. Blijkbaar wordt de event dus aangeroepen vanuit dezelfde thread.
Weet je zeker dat je vanuit een andere thread callt? (Dus in je debugger gecheckt?)

Acties:
  • 0 Henk 'm!

  • whoami
  • Registratie: December 2000
  • Laatst online: 01:56
Wat is TUIOprocessor ? Welke class is dat ?
Implementeert deze class ISynchronizeInvoke ?
Als die dat niet doet, dan kan je niet casten naar ISynchronizeInvoke natuurlijk; vandaar dat je 'target' dan null is. (WinForm controls implementeren ISynchronizeInvoke).

Wat je zou kunnen doen, is gebruik maken van de AsyncOperation & AsyncOperationManager classes die abstractie maken van de 'application-context', en zowiezo ervoor zorgen dat je eventhandler op de juiste thread uitgevoerd wordt. (De manier die ik met m'n voorbeeldcode toon, werkt bv ook niet in WPF, aangezien WPF controls ook ISynchronizeInvoke niet implementeren).

AsyncOperation - manier ziet er ongeveer zo uit:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
AsyncOperation _operation;
...

public void DoWork()
{
    _operation = AsyncOperationManager.CreateOperation();
   
    Thread t = new Thread(delegate()
    {
          // ...
          _operation.Post(new SendOrPostCallback(delegate(object o ) )
          {
                if( TheEvent != null )
                        TheEvent ( ... );
          }, null);
    });

    t.Start();
}

De AsyncOperationManager creeërt dus een AsyncOperation die geschikt is voor de application-context. Die AsyncOperation kan je dan gebruiken om je events op te gaan callen.

Meer info:
MSDN AsyncOperation
klik

[ Voor 6% gewijzigd door whoami op 06-10-2008 11:13 ]

https://fgheysels.github.io/


Acties:
  • 0 Henk 'm!

  • writser
  • Registratie: Mei 2000
  • Laatst online: 16-09 10:48
Flard schreef op maandag 06 oktober 2008 @ 11:03:
Blijkbaar is InvokeRequired dus false.
En InvokeRequired is true als de thread van de caller niet gelijk is aan de UI thread. Blijkbaar wordt de event dus aangeroepen vanuit dezelfde thread.
Weet je zeker dat je vanuit een andere thread callt? (Dus in je debugger gecheckt?)
Ik weet zeker dat ik vanuit een andere thread call. Het probleem is niet dat InvokeRequired false is (misschien komt dat later :), maar dat deze cast altijd null oplevert.
ISynchronizeInvoke target = d as ISynchronizeInvoke;
Waardoor de DynamicInvoke wordt aangeroepen. En die is niet threadsafe, correct? Het "target" van d is XNATutorial.Game1, de klasse waarin de event-loop van XNA loopt.
Wat is TUIOprocessor ? Welke class is dat ?
Implementeert deze class ISynchronizeInvoke ?
TUIOprocessor is een (zelfgemaakte) klasse die input van multi-touch schermen verwerkt en deze omzet naar events. Deze implementeert dus geen ISynchronizeInvoke, ik dacht juist dat methodes als BeginInvoke() etc. al ingebouwd zaten in c#. Bedankt voor je tips, ik zal even verder prutsen nu.

[ Voor 20% gewijzigd door writser op 06-10-2008 11:20 ]

Onvoorstelbaar!


Acties:
  • 0 Henk 'm!

  • writser
  • Registratie: Mei 2000
  • Laatst online: 16-09 10:48
Whoami, ik heb nu de volgende oplossing. In de event handler (die in een andere thread moet draaien) heb ik alle code in een SendOrPostCallback gestopt. De asyncOp maak ik aan in de constructor van de klasse waarop alle events worden afgevuurd. Dit werkt: het ManagedThreadId is het ID van de main thread, niet het ID van de TUIOprocessor. Is dit een enigszins nette oplossing?
C#:
1
2
3
4
5
6
7
8
9
       public void touchDeleted(object sender, int id)
        {
            SendOrPostCallback so = new SendOrPostCallback(delegate
            {
                Console.WriteLine("thread = " + Thread.CurrentThread.ManagedThreadId + ", id = " + id);
                // Doe hier alle processing
                
            });
            asyncOp.Post(so, null);

Ik zou liever dit soort "hacks" willen uitvoeren in de klasse die de events vuurt, zodat iemand die zich heeft geabonneerd op events zich niet bezig hoeft te houden met threading. Maar de implementatie daarvan zie ik nog niet meteen voor me.


edit: deze methode is in elk geval gruwelijk inefficient nu hij alle rekenintensieve code ook in de renderthread doet. :) De events worden redelijk vaak afgevuurd, misschien kan ik beter een synchronized queue gebruiken in plaats van elke seconde een aantal asynchronous posts?

[ Voor 31% gewijzigd door writser op 06-10-2008 12:48 ]

Onvoorstelbaar!


Acties:
  • 0 Henk 'm!

  • whoami
  • Registratie: December 2000
  • Laatst online: 01:56
Ik zou het zo doen:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Worker
{

      AsyncOperation _operation;

     public void DoWork()
     {
           // Hier wel nog checken of we reeds 'running' zijn of niet.
           // indien je meerdere keren DoWork kunt aanroepen, moet
           // je hier ook de nodige implementatie voor voorzien.

            _operation = AsyncOperation.CreateOperation ( ... );

           Thread t = new Thread(DoWorkCore);
           t.Start();

     }

     private void DoWorkCore()
     {
          _operation.Post (new SendOrPostCallback(.....));
     }
}

https://fgheysels.github.io/


Acties:
  • 0 Henk 'm!

  • writser
  • Registratie: Mei 2000
  • Laatst online: 16-09 10:48
Whoami, bedankt voor je snelle reactie. Ik heb inmiddels al een werkend stuk code. Er zijn twee dingen die ik nog niet begrijp aan jouw implementatie:

1. De klasse worker, bedoel je daarmee, vertaald naar mijn programma, de klasse die de events genereert of de klasse die de events moet verwerken? Ik neem aan het laatste, want de AsyncOperation hoort bij de context waarin de callback moet gaan draaien, correct?

2. Waarom start je een nieuwe thread op om de post te aan te roepen? Blijft de thread die het event verstuurt anders wachten tot de post is uitgevoerd?

De performanceproblemen bleken achteraf erg mee te vallen: deze werden veroorzaakt door een groot aantal diagnostische Console.WriteLine's die niet erg efficient bleken te zijn :).

Onvoorstelbaar!


Acties:
  • 0 Henk 'm!

  • whoami
  • Registratie: December 2000
  • Laatst online: 01:56
1. De 'worker' class is de class die de events genereerd, en die dus taken uitvoert op een andere thread. De AsyncOperation wordt gecreeërd voordat de 'worker' thread gestart wordt.

2. Mijn code is misschien niet geheel duidelijk. De 'DoWorkCore' method is de worker method die dus een aantal 'dingen' uitvoert in een andere thread. Deze method bevat dus jouw logica die op een andere thread moet uitgevoerd worden. In deze method (die dus op een andere thread wordt uitgevoerd), ga ik dus ook de events gaan raisen (bv om de vooruitgang van je taak te gaan rapporteren).

https://fgheysels.github.io/


Acties:
  • 0 Henk 'm!

  • shades
  • Registratie: September 2001
  • Laatst online: 14-07 13:45

shades

huh ?

Wordt heel duidelijk uitgelegd hoe je van een bestaand component een multithreaded component kan maken. Zelfs ik snap het :o :+ _/-\o_

Walkthrough: Authoring a Simple Multithreaded Component with Visual C#

https://k1600gt.nl

Pagina: 1