[C#] MultiThreaded MergeSort, WaitAll probleem

Pagina: 1
Acties:

Acties:
  • 0 Henk 'm!

  • roy-t
  • Registratie: Oktober 2004
  • Laatst online: 08-09 11:33
Zoals ik misschien al aangegeven heb vind ik het de laaste tijd interessant om van vrij simpele algoritmen een multithreaded variant te maken, gewoon als oefening.

Nu ben ik op dit moment bezig met MergeSort. Eerst even de singelthreaded variant gemaakt, die werkt prima. Daarna aan de slag gegaan met de multithreaded versie. Maar nu stuit ik op het volgende probleem.

-Elke keer splits MergeSort de huidige lijst in 2 lijsten, de twee nieuwe lijsten laat ik weer spliten door 2 nieuwe threads (enqueuen in een threadpool). Hier laat ik het vorige thread op wachten via WaitHandle.WaitAll,

Elke keer als ik iets in de wachtrij van de ThreadPool stop geef ik deze een nieuw manualResetEvent, zoals hier te zien is.

C#:
1
2
3
4
5
6
7
8
9
10
11
            ManualResetEvent[] doneEvents = new ManualResetEvent[2];
            doneEvents[0] = new ManualResetEvent(false);
            doneEvents[1] = new ManualResetEvent(false);

            MergeSort<T> contextLeft = new MergeSort<T>() { data = left, doneEvent = doneEvents[0] };
            MergeSort<T> contextRight = new MergeSort<T>() { data = right, doneEvent = doneEvents[1] };

            ThreadPool.QueueUserWorkItem(ThreadPoolCallback, contextLeft);
            ThreadPool.QueueUserWorkItem(ThreadPoolCallback, contextRight);                        
            
            WaitHandle.WaitAll(doneEvents);          


Uiteindelijk komt de mergesort 'in het diepste thread' uit op de volgende code. Die helemaal in het begin staat.
C#:
1
2
3
4
            if (data.Length <= 1)
            {                
                return data;
            }


Hier returned hij dus zonder 2 nieuwe threads aan te maken en in de wacht te schieten.
Het returnen komt uit op de volgende methode:
C#:
1
2
3
4
5
6
        private void ThreadPoolCallback(object threadContext)
        {
            MergeSort<T> context = (MergeSort<T>)threadContext;
            context.data = context.MultiThreaded(context.data);
            doneEvent.Set();
        }



Zoals te zien is signaleer ik hier via doneEvent.Set() dat dit thread klaar is. Als de 2 diepste threads een signaal hebben gegeven zou WaitHandle.WaitAll het thread net hier boven verder moeten laten gaan, en zo voorts.


Alleen blijkt nu dat de WaitHandle niet, of iig niet lang genoeg het bovenste thread in de wacht zet. Als ik namelijk na WaitHandle.WaitAll de volgende code zet:
C#:
1
Console.Out.WriteLine("DAT: " + left.Length + ", " + right.Length);

Print hij slechts 1x "DAT: 3,3" (ik start met een rij van 6 ints). Hij loopt dus meteen door, komt niet in de andere threads, of start iig de andere threads zonder te wachten en komt nooit voorbij de WaitHandle in de andere threads.

Ik heb zelf al even lopen kijken, en dus al aardig wat gedebugged, maar ik kom er niet uit waarom die WaitAll niet werkt. zoals gezecht maak ik telkens 2 nieuwe manualResetEvents.

De voorbeeld code die ik gebruik hebt is: http://msdn.microsoft.com/en-us/library/3dasc8as(VS.80).aspx waar ze haast dezelfde techniek gebruiken.

Ziet iemand het?

Voor de volledigheid nog even alle relevante code onder elkaar zodat ik niet net wat mis (geprobeerd zo kort mogelijk te houden).

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
56
57
58
59
60
//Helper variables
        protected T[] data;
        protected ManualResetEvent doneEvent;                       

        public T[] MultiThreaded(T[] data)
        {
            if (data.Length <= 1)
            {                
                return data;
            }

            int mid = data.Length / 2;
            T[] left = new T[mid];
            T[] right = new T[data.Length - mid];
            T[] result;
            for (int i = 0; i < mid; ++i)
            {
                left[i] = data[i];
            }
            for (int i = mid; i < data.Length; i++)
            {
                right[i - mid] = data[i];
            }

            //Interesting part
            ManualResetEvent[] doneEvents = new ManualResetEvent[2];
            doneEvents[0] = new ManualResetEvent(false);
            doneEvents[1] = new ManualResetEvent(false);

            MergeSort<T> contextLeft = new MergeSort<T>() { data = left, doneEvent = doneEvents[0] };
            MergeSort<T> contextRight = new MergeSort<T>() { data = right, doneEvent = doneEvents[1] };

            ThreadPool.QueueUserWorkItem(ThreadPoolCallback, contextLeft);
            ThreadPool.QueueUserWorkItem(ThreadPoolCallback, contextRight);                        
            
            WaitHandle.WaitAll(doneEvents);          

            Console.Out.WriteLine("DAT: " + left.Length + ", " + right.Length);

            left = contextLeft.data;
            right = contextRight.data;

            if (left[left.Length - 1].CompareTo(right[0]) > 0)
            {
                result = Merge(left, right);
            }
            else
            {
                result = Append(left, right);
            }

            return result;
        }

        private void ThreadPoolCallback(object threadContext)
        {
            MergeSort<T> context = (MergeSort<T>)threadContext;
            context.data = context.MultiThreaded(context.data);
            doneEvent.Set();
        }

~ Mijn prog blog!


Acties:
  • 0 Henk 'm!

  • RobIII
  • Registratie: December 2001
  • Niet online

RobIII

Admin Devschuur®

^ Romeinse Ⅲ ja!

(overleden)
Even los van 't feit dat ik niet heb gekeken wat je probleem zou kunnen zijn; bij een grote dataset heb je natuurlijk een shitload aan threads; dat levert geheid meer overhead dan het probleem wat slimmer aanpakken. Je wil echt geen thread starten om 2 items te sorteren. Dus die "diepste thread" heeft eigenlijk geen bestaansrecht; de overhead van 't ding maken, aanzwengelen en whatnot is duurder dan 't comparen en evt. swappen van die twee items. Daarbij heeft een default threadpool een max van (ik meen) 25. Then again: misschien had je dat zelf ook al bedacht en is dit nog maar een eerste opzet/test :P

[ Voor 8% gewijzigd door RobIII op 08-10-2009 18:42 ]

There are only two hard problems in distributed systems: 2. Exactly-once delivery 1. Guaranteed order of messages 2. Exactly-once delivery.

Je eigen tweaker.me redirect

Over mij


Acties:
  • 0 Henk 'm!

  • roy-t
  • Registratie: Oktober 2004
  • Laatst online: 08-09 11:33
RobIII schreef op donderdag 08 oktober 2009 @ 18:41:
Even los van 't feit dat ik niet heb gekeken wat je probleem zou kunnen zijn; bij een grote dataset heb je natuurlijk een shitload aan threads; dat levert geheid meer overhead dan het probleem wat slimmer aanpakken. Je wil echt geen thread starten om 2 items te sorteren. Dus die "diepste thread" heeft eigenlijk geen bestaansrecht; de overhead van 't ding maken, aanzwengelen en whatnot is duurder dan 't comparen en evt. swappen van die twee items. Daarbij heeft een default threadpool een max van (ik meen) 25. Then again: misschien had je dat zelf ook al bedacht en is dit nog maar een eerste opzet/test :P
Inderdaad wil ik niet overal 2 threads spawnen, daarom worden ze in de threadpool gestopt die het werk verdeelt over threads, uiteindelijk wil ik wat gaan kijken hoeveel threads nut hebben en natuurlijk proberen threads meer werk te laten doen. Dit is inderdaad een eerste opzet. Verder zorgt natuurlijk de threadpool er al voor dat ik niet voor het diepste thread weer een apart thread heb. Ook heb je met de threadpool minder overhead om een thread te starten.

het gaat sowieso pas nut hebben van heel veel items :).

Maar zoals je al gespot hebt is het wat gehobby, gespeel en vooral een work in progress.

~ Mijn prog blog!


Acties:
  • 0 Henk 'm!

  • pedorus
  • Registratie: Januari 2008
  • Niet online
Zonder de complete code heb ik niet zoveel zin om 'handmatig' te gaan debuggen. Zo op het oog lijkt me dit inderdaad gehobby, bijvoorbeeld dat loopje voor het kopieren van een deel van een array (Take/Array.Copy?). ;) Kijk eens naar deze code. :)

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 08 oktober 2009 @ 19:10:
Zonder de complete code heb ik niet zoveel zin om 'handmatig' te gaan debuggen. Zo op het oog lijkt me dit inderdaad gehobby, bijvoorbeeld dat loopje voor het kopieren van een deel van een array (Take/Array.Copy?). ;) Kijk eens naar deze code. :)
Leuk artikel over de pittfalls inderdaad, maar de oplossingen daar zijn het nog niet uitgegeven Parallel framework wat waarschijnlijk in .Net4.0 komt.

Verder heb ik het eerst zo simpel mogelijk gehouden om daarna uit te breiden, dit is zoals eerder gezecht slechts een eerste opzet en test. Natuurlijk ga je met meerdere iteraties over je code heen en pas je dingen aan. Wel had ik op dit moment niet gedacht aan Array.Copy e.d.

Nog verder, als je geen zin hebt om handmatig te debuggen, dan doe je dat toch lekker niet :).

Het probleem in dit geval is gewoon WaitAll in recursie, ik zie niet in waarom er iets mis gaat daar, of met de manualresets. Sorry als dit bot lijkt, maar misschien kunnen we zonder al te veel te letten op nog niet geoptimaliseerde code eerst even naar dat probleem kijken, daarna kunnen we natuurlijk vrolijk verder praten over het interessante probleem wat je hebt bij kleine (recursieve) code en thread overhead. Mede als optimalisaties van algemenere aard :).

~ Mijn prog blog!


Acties:
  • 0 Henk 'm!

  • pedorus
  • Registratie: Januari 2008
  • Niet online
Het is al tijden uitgegeven. Ok, Do is Invoke geworden. ;)

Deze methode is dus duidelijk absurd lastig te implementeren met ThreadPool (zie artikel), maar ik zie dat je QueueUserWorkItem ook verkeerd gebruikt (daar zit een bug). Deze retourneerd een boolean, en kan een delegate meekrijgen. Goed is dus "if (!ThreadPool.QueueUserWorkItem(contextLeft.ThreadPoolCallback)) {...}". Ik heb het voor de grap even getest met behulp van "return left.Concat(right).OrderBy(a=>a).ToArray();" bij gebrek aan complete code, en 10 items kost slechts 3.6 seconden, 50 kost 25.2 seconden (!!!). :)

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 08 oktober 2009 @ 22:37:
Het is al tijden uitgegeven. Ok, Do is Invoke geworden. ;)

Deze methode is dus duidelijk absurd lastig te implementeren met ThreadPool (zie artikel), maar ik zie dat je QueueUserWorkItem ook verkeerd gebruikt (daar zit een bug). Deze retourneerd een boolean, en kan een delegate meekrijgen. Goed is dus "if (!ThreadPool.QueueUserWorkItem(contextLeft.ThreadPoolCallback)) {...}". Ik heb het voor de grap even getest met behulp van "return left.Concat(right).OrderBy(a=>a).ToArray();" bij gebrek aan complete code, en 10 items kost slechts 3.6 seconden, 50 kost 25.2 seconden (!!!). :)
Ah ik zat al te zoeken, wel uitgegeven dus maar als extensie, tis nog wel slechts een CTP geen echte release. Even uitzoeken wat er gebeurt als iemand die extensie niet heeft. (Of compileerd dit gewoon naar bestande IL?)

Ik zal nog even de ThreadPool nakijken (gewoon om dat goed te snappen, wat vooral mijn doel was). Maar op het voorbeeld van de MSDN gebruiken ze die returnvalue inderdaad niet volgens mij niet. (MSDN ligt nu plat dus kan het niet controleren), stom natuurlijk om de verkeerde ThreadPoolCallBack mee te geven, dat ga ik vanmiddag meteen even fixen.

~ Mijn prog blog!


Acties:
  • 0 Henk 'm!

  • Phyxion
  • Registratie: April 2004
  • Niet online

Phyxion

_/-\o_

RobIII schreef op donderdag 08 oktober 2009 @ 18:41:
Even los van 't feit dat ik niet heb gekeken wat je probleem zou kunnen zijn; bij een grote dataset heb je natuurlijk een shitload aan threads; dat levert geheid meer overhead dan het probleem wat slimmer aanpakken. Je wil echt geen thread starten om 2 items te sorteren. Dus die "diepste thread" heeft eigenlijk geen bestaansrecht; de overhead van 't ding maken, aanzwengelen en whatnot is duurder dan 't comparen en evt. swappen van die twee items. Daarbij heeft een default threadpool een max van (ik meen) 25. Then again: misschien had je dat zelf ook al bedacht en is dit nog maar een eerste opzet/test :P
ThreadPool werkt veel sneller voor relatief weinig code. Zelf een nieuwe thread heeft veel overhead maar ThreadPools niet.
Los van het feit dat ik ook niet met threads zou sorteren natuurlijk :)

'You like a gay cowboy and you look like a gay terrorist.' - James May


Acties:
  • 0 Henk 'm!

  • .oisyn
  • Registratie: September 2000
  • Laatst online: 17-09 14:05

.oisyn

Moderator Devschuur®

Demotivational Speaker

Phyxion schreef op vrijdag 09 oktober 2009 @ 08:00:
[...]

ThreadPool werkt veel sneller voor relatief weinig code. Zelf een nieuwe thread heeft veel overhead maar ThreadPools niet.
Wat nog niet betekent dat het zinvol is om voor 1 of 2 items een job te starten - je overhead blijft teveel. Het is natte vinger werk wat even nagemeten moet worden, maar op het eerste gezicht zou ik voor een workload van minder dan 256 items sowieso geen nieuwe job starten maar dat gewoon meteen quicksorten.
Los van het feit dat ik ook niet met threads zou sorteren natuurlijk :)
Waarom niet? Dat kan een stuk sneller zijn als je veel te doen hebt.

Give a man a game and he'll have fun for a day. Teach a man to make games and he'll never have fun again.


Acties:
  • 0 Henk 'm!

  • roy-t
  • Registratie: Oktober 2004
  • Laatst online: 08-09 11:33
Nouja en het gaat natuurlijk ook om de tijd die de CompareTo() duurt. (nu is dat voor intjes bijna nul, maar voor wat grotere klassen zou dat natuurlijk ook wat langer kunnen duren wat natuurlijk helpt in de overhead weg werken.)

Ik zit nu nog op de uni te klooien met Haskell, maar vanmiddag zal ik eens even kijken wat er gebeurt met de correcte implementatie erbij, hoe zich dit verhoud enzo :).

(En zelfs met te grote overhead zijn dit soort opdrachtjes natuurlijk altijd goede oefeningen om te lere hoe je taken kunt parraleliseren).

[ Voor 15% gewijzigd door roy-t op 09-10-2009 11:58 ]

~ Mijn prog blog!


Acties:
  • 0 Henk 'm!

  • Phyxion
  • Registratie: April 2004
  • Niet online

Phyxion

_/-\o_

.oisyn schreef op vrijdag 09 oktober 2009 @ 10:52:
[...]

Wat nog niet betekent dat het zinvol is om voor 1 of 2 items een job te starten - je overhead blijft teveel. Het is natte vinger werk wat even nagemeten moet worden, maar op het eerste gezicht zou ik voor een workload van minder dan 256 items sowieso geen nieuwe job starten maar dat gewoon meteen quicksorten.


[...]

Waarom niet? Dat kan een stuk sneller zijn als je veel te doen hebt.
ThreadPool hoeft niet trager te zijn als er nog threads in suspended state zijn, hangt ervan af hoevaak die functies gebruikt worden.
Als je erg veel moet sorteren kan het uiteraard handig zijn maar ik heb niet het idee dat TS dat gebruikt.
Al met al hangt het af van de situatie waarin het gebruikt wordt.

'You like a gay cowboy and you look like a gay terrorist.' - James May


Acties:
  • 0 Henk 'm!

  • roy-t
  • Registratie: Oktober 2004
  • Laatst online: 08-09 11:33
Hmm ik kom allemaal interessante problemen tegen. Zo te zien komt de code (nu gefixed) af en toe uit op een deadlock. Ik had verwacht dat WaitHandle.WaitAll(doneEvents); het huidige thread/taak zou halten en dat je dan vrolijk verder zou gaan met de volgende wachtende taak. Helaas blijkt dit niet zo te zijn en bij minder threads dan dat er totaal aangemaakt zouden moeten worden loopt ie dan ook voor geen meter. Hierdoor is de overhead veel groter dan ik dacht. Ik had gehoopt dat ik op deze manier gewoon lekker x threads ofzo als max in kon stellen zodat je niet teveel overhead krijgt.

Ik ga er nog eens lekker mee stoeien iig :).

~ Mijn prog blog!


Acties:
  • 0 Henk 'm!

  • Spockz
  • Registratie: Augustus 2003
  • Laatst online: 15-09 09:15

Spockz

Live and Let Live

Kan je niet gebruik maken van iets als een BlockingQueue uit java oid? Daarbij is het oproepen van je tasks dan wel synchronized (en dus relatief duur) voor kleine worksets maar je weet in elke geval zeker dat je geen deadlocks e.d. kan krijgen. Dus in principe krijg je zo ongeveer wat je nu hebt maar dan laat je de synchronisatie op 1 plek doen, wat misschien een wat overzichtelijker geheel geeft.

Een andere optie is misschien om een wachtvrije implementatie te maken.

C'est le ton qui fait la musique. | Blog | @linkedin
R8 | 18-55 IS | 50mm 1.8 2 | 70-200 2.8 APO EX HSM | 85 1.8


Acties:
  • 0 Henk 'm!

  • .oisyn
  • Registratie: September 2000
  • Laatst online: 17-09 14:05

.oisyn

Moderator Devschuur®

Demotivational Speaker

Phyxion schreef op vrijdag 09 oktober 2009 @ 13:36:
[...]

ThreadPool hoeft niet trager te zijn als er nog threads in suspended state zijn
De vergelijking hier gaat om het resumen van een thread en het direct uitvoeren van code in de huidige thread. Voor het resumen zijn hoe dan ook kernel calls nodig, wat het per definitie trager maakt. De enige manier om enigszins vergelijkbare performance te krijgen is om de wachtende thread te laten spinlocken op een variabele die door de andere thread gezet wordt, maar dat is in de grand scheme of things meestal niet aan te raden.

Give a man a game and he'll have fun for a day. Teach a man to make games and he'll never have fun again.

Pagina: 1