[C#] zpracování kolekce vlákny

C++, C#, Visual Basic, Delphi, Perl a ostatní

Moderátor: Moderátoři Živě.cz

Odeslat příspěvekod milanc 9. 5. 2017 04:55

Ahoj,

mám naplněnou kolekci ConcurrentDictionary, která obsahuje klíče (adresářové cesty) a hodnoty (List<KeyValuePair<string, string>>).
První string v listu je název souboru, druhý hash, který budu počítat. Takto to mám naplněné (záměrně).

Nyní stojím řeším, jak rozhodit výpočet mezi více vláken. Vlastně s tím dělám poprvé, a tak úplně ani nevím, co hledat.

Představa je taková, že počet vláken/workerů bude omezen na počet jader (to mám, např. 8), tedy vždy poběží max. 8 vláken společně.

Jakmile se jedno vlákno ukončí, nastartuje se další s následujícím klíčem, stále dokola, než se zpracují všechny klíče slovníku.

Každý worker dostane na vstupu klíč (cestu), podle něj bude procházet List souborů, dopočítávat a vkládat hashe.

Představa asi jasná, ale nevím jak to udělat. :-)
Myslel jsem, že k tomuto slouží TreadPool, ale tam jsem nikde nenašel možnost nastavit maximum vláken.
Můžu si počet vláken uchovávat v proměnné, a v cyklu je případně startovat, ale to se mi moc nezdá. Předpokládám, že C# má pro toto nějaké udělátko. Vlastně je to asi model producent-konzumer, přičemž konzumerů je X a producent mi už slovník naplnil.

Jak byste to řešili? Děkuji.
milanc
Junior
Uživatelský avatar

Odeslat příspěvekod Nargon 9. 5. 2017 08:50

Já bych to řešil pomocí paralelního LINQ, to je krásně řešené a velice jednoduché na použití. Veškeré babrání s vlákny se změní na několik příkazů, kdy jen nastavíš počet vláken (to ale není povinné) a pak napíšeš funkci, která se pro každý prvek vykoná. Hotovo.
Viz následující příklad:
Kód: Vybrat vše
using System;
using System.Collections.Concurrent;
using System.Linq;

namespace Test
{
    class Program
    {
        static void Main(string[] args)
        {
            ConcurrentDictionary<string, string> data = new ConcurrentDictionary<string, string>();  //snad jsem tvojí strukturu odhadl správně
            foreach (var x in Enumerable.Range(1, 1000)) data.TryAdd(string.Format("test{0}", x), string.Format("HASH{0}", x));  //naplnění testovacími daty

            int X = Environment.ProcessorCount;    //zjištění počtu vláken ze systému
            data.AsParallel().WithDegreeOfParallelism(X).ForAll(element =>  //data se budou zpracovávat paralelně s omezením na X souběžně běžících vláken
                Console.WriteLine("Key: {0}\tValue: {1}",element.Key, element.Value)  //v mém testu jen data vypisuji, ty sem můžeš dát co potřebuješ
            );
        }
    }
}
Desktop: Ryzen 7 1800X (3.95GHz, 1.35V), Asus Crosshair VI Hero, 16GB DDR4 Ram (3200MHz), 128GB SSD + 3TB HDD, Nvidia GTX 1080
Notebook: Asus UL50VT 15.6" (SU7300@1.7GHz, 4GB ram, 500GB HDD, Intel GMA 4500MHD + nVidia G210M, dlouha vydrz cca 7+ hod)
Nargon
Moderátor

Odeslat příspěvekod milanc 9. 5. 2017 09:13

Ahoj, tohle řešení vypadá zajímavě.
Já se právě nechci těch vláken úplně vzdát, protože je to taková ukázka jak se s tím pracuje.
Nicméně tvé řešení určitě prozkoumám.

Já už to napsal přes ten ThreadPool, ale nějak mam dojem, že jsem se připravil o některé funkce, které bych měl s tupým polem instancí threadu. I když vlastně je to asi jedno. Dá se přes ten thread nějak přistupovat ke konkrétném vláknům?

ThreadPool.SetMaxThreads(threads, threads);
foreach (var subdir in files.OrderBy(i => i.Key))
{
ThreadPool.QueueUserWorkItem(new WaitCallback(DoWork), subdir);
}
milanc
Junior
Uživatelský avatar

Odeslat příspěvekod milanc 9. 5. 2017 14:41

Potřeboval bych ještě poradit, jak v mětodě aktualizovat slovník, který předávám v argumentu.

Kód: Vybrat vše
public static void DoWork(string threadname, string subdir, ConcurrentDictionary<string, List<KeyValuePair<string, string>>> dictionary) {
...
}


Mam něco takového, cílem je projít celý list v dictionary[subdir] a do druhé části (value) aktualizovat řetězec.

Dokážu vytvořit ten nový pár, ale neumím ho nahradit.
Kód: Vybrat vše
            foreach (KeyValuePair<string, string> element in dictionary[subdir])
            {
                KeyValuePair<string, string> updated_element = new KeyValuePair<string, string>(element.Key, Misc.GetSha160(Path.Combine(subdir, element.Key)));


Možná by to šlo nějak přímo a elegantněji přes LINQ?
milanc
Junior
Uživatelský avatar

Odeslat příspěvekod Nargon 9. 5. 2017 15:37

To ti řeknu rovnou. To nenahradíš.
Nejlepší je udělat si novou kolekci a tam zkopírovat klíč z původní kolekce a novou hodnotu to co jsi vypočítal. Jako výsledek pak vrátit novou kolekci.
Přes LINQ to jde trochu zkrátit pomocí Select, ale princip je zcela stejný. Vytvoříš novou kolekci.
Desktop: Ryzen 7 1800X (3.95GHz, 1.35V), Asus Crosshair VI Hero, 16GB DDR4 Ram (3200MHz), 128GB SSD + 3TB HDD, Nvidia GTX 1080
Notebook: Asus UL50VT 15.6" (SU7300@1.7GHz, 4GB ram, 500GB HDD, Intel GMA 4500MHD + nVidia G210M, dlouha vydrz cca 7+ hod)
Nargon
Moderátor

Odeslat příspěvekod milanc 9. 5. 2017 15:46

No mohlo by stacit kolekci prochazet, vzdy precist list do pomocne promenne, updatnout value a pak tu pomocnou priradit na stejne misto v listu? To by take mohlo jit?
milanc
Junior
Uživatelský avatar

Odeslat příspěvekod milanc 9. 5. 2017 21:17

Tak jsem to vyřešil tak, že jsem se vykašlal na ty KeyValuePair. S tím mi to za všech okolností psalo read-only, a value nešla měnit. Udělal jsem si místo toho messenger class, pak není problém to modifikovat v cyklu. Do metody vlákna pak posílám jen ten daný list ze slovníku.

Kód: Vybrat vše
        public static void DoWork(string threadname, string subdir, List<FileElement> list)
        {
            Thread.CurrentThread.Name = threadname;

            for (var i = 0; i < list.Count; i++)
            {
                list[i].Hash = Misc.GetSha160(Path.Combine(subdir, list[i].FileName));
            }
...


Díky.
milanc
Junior
Uživatelský avatar


Kdo je online

Uživatelé procházející toto fórum: Žádní registrovaní uživatelé a 0 návštevníků