2

У меня есть задание:

Разработать многопоточное приложение, выполняющее следующие действия. Для каждого файла в директории построить массив из 256 элементов, содержащий количество значений байт, составляющих файл, т.е. нулевой элемент массива должен содержать количество байт в файле равных 0, первый элемент – кол-во байт равных 1 и т.д.

Соответственно, каждый файл должен обрабатываться отдельным потоком. Результаты вывести (+сохранить в текстовый файл) построчно для каждого обработанного файла, в порядке завершения обработки. Каждая строка должна содержать имя файла, потраченное на обработку время, результирующий массив (элементы через запятую).

В программе должны быть реализованы:

  • выбор директории для обработки;
  • вывод результатов на экран по мере обработки.

В процессе написания программы я сделал обычный (линейный способ без потоков) для проверки работы в целом, затем сделал потоковый способ.

До этого я ни разу не сталкивался с многопоточным программированием, поэтому получились КОСТЫЛИ.

Есть класс FilesHandler:

class FilesHandler{
            private List<string> PathesOfFiles = new List<string>(); //Содержит пути к файлам
            static object locker = new object();
            public void HandleEachFile(RichTextBox RTB) //обработчик каждого файла 
            {
                foreach (string path in PathesOfFiles)  
                {
                /*Вариант без потоков*/
                //CalculateNew(RTB, path);
            /*Вариант с потоками*/
            new Thread(() =&gt; { CalculateThread(RTB, path); }).Start();
            }
        }

        private void CalculateNew(RichTextBox RTB, string tmpPath)
        {
        RTB.Text += DateTime.UtcNow.ToString(&quot;yyyy-MM-dd HH:mm:ss.fff&quot;, CultureInfo.InvariantCulture) + &quot;\t&quot;;
        Stopwatch stopwatch = new Stopwatch(); //Создаём секундомер
        stopwatch.Start(); //Запускаем секундомер
        Converter tmpobj = new Converter(); //Создаём объект &quot;файла&quot;
        tmpobj.CountBytes(tmpPath); //Метод, который читает файл поблочно и сразу обрабатывает блок(считает байты)
        List&lt;string&gt; res = new List&lt;string&gt;(); //создаём массив для результирующей строки
        res.Add(GetFileName(tmpPath)); //Записываем первым имя файла
        res.Add(&quot; {&quot;);
        for (int i = 0; i &lt; 256; i++) //Переписываем кол-во каждого байта
        {
            if (i != 255)
                res.Add(tmpobj.GetBytes()[i].ToString() + &quot;, &quot;);
            else
                res.Add(tmpobj.GetBytes()[i].ToString() + &quot;} &quot;);
        }
        stopwatch.Stop(); //Останавливаем секундомер, чтобы записать время обработки файла
        res.Add(stopwatch.ElapsedMilliseconds.ToString() + &quot; ms&quot;);
        RTB.Text += String.Join(&quot;&quot;, res) + '\n'; //RTB - richtextbox как консоль для вывода информации
        }

        private void CalculateThread(RichTextBox RTB, string tmpPath) //Для варианта с потокам всё вынесено в отдельную функцию
        {
            lock (locker)
            {
                Action action2 = () =&gt; RTB.Text += DateTime.UtcNow.ToString(&quot;yyyy-MM-dd HH:mm:ss.fff&quot;, CultureInfo.InvariantCulture) + &quot;\t&quot;;
                if (RTB.InvokeRequired)
                {
                    RTB.Invoke(action2);
                }
                else
                {
                    action2();
                }
                Stopwatch stopwatch = new Stopwatch(); //Создаём секундомер
                stopwatch.Start(); //Запускаем секундомер
                Converter tmpobj = new Converter(); //Создаём объект &quot;файла&quot;
                tmpobj.CountBytes(tmpPath); //Метод, который читает файл поблочно и сразу обрабатывает блок(считает байты)
                List&lt;string&gt; res = new List&lt;string&gt;(); //создаём массив для результирующей строки
                res.Add(GetFileName(tmpPath)); //Записываем первым имя файла
                res.Add(&quot; {&quot;);
                for (int i = 0; i &lt; 256; i++) //Переписываем кол-во каждого байта
                {
                    if (i != 255)
                        res.Add(tmpobj.GetBytes()[i].ToString() + &quot;, &quot;);
                    else
                        res.Add(tmpobj.GetBytes()[i].ToString() + &quot;} &quot;);
                }
                stopwatch.Stop(); //Останавливаем секундомер, чтобы записать время обработки файла
                res.Add(stopwatch.ElapsedMilliseconds.ToString() + &quot; ms&quot;);

                Action action3 = () =&gt; RTB.Text += String.Join(&quot;&quot;, res) + '\n';
                if (RTB.InvokeRequired)
                {
                    RTB.Invoke(action3);
                }
                else
                {
                    action3();
                }
            }
        }

}

Также есть класс Converter, в котором полем является ulong[] Bytes = new ulong[256]; и метод (основной для объяснения), который считает вхождения байтов выбранного файла:

    public void CountBytes(string filename)
    {
        FileStream reader = File.OpenRead(filename);
        byte[] buffer = new byte[4096];
        int bytesRead = 0;
    while ((bytesRead = reader.Read(buffer, 0, buffer.Length)) &gt; 0)
    {
        for (int i = 0; i &lt; bytesRead; i++)
        {
            byte b = buffer[i];
            Bytes[b]++;
        }
    }
}

Идея моей реализации такова: в foreach при чтении пути каждого файла создается поток, который вызывает метод CalculateThread.

По сути же у меня каждый поток не имеет доступа к одному и тому же ресурсу, потому что объект Converter создается внутри метода, но без lock адекватно работать не будет, потому что на директории в 5гб работает молниеносно, нежели линейный способ, а если взять директорию на 50гб (с таким же кол-вом файлов), то серьезно отстает от линейного, а с lock +- одинаково, но дольше все равно.

Поток обрабатывает файлик, и потом сразу печатает в richtextbox результат.

Есть подозрения, что как раз из-за прерывания для записи результата эта задержка возникает.

UPD:

подскажите, пожалуйста, если делать с async await, это так должно выглядеть?

private async void Method(RichTextBox RTB, string path)
    {
        List<string> res = new List<string>();
        await Task.Run(() =>
        {
            new Thread(() => {
                Stopwatch stopwatch = new Stopwatch(); //Создаём секундомер
                stopwatch.Start(); //Запускаем секундомер
                Converter tmpobj = new Converter(); //Создаём объект "файла"
                tmpobj.CountBytes(path); //Метод, который читает файл поблочно и сразу обрабатывает блок(считает байты)
                res.Add(String.Concat(GetFileName(path), " {", String.Join(", ", tmpobj.GetBytes()), "}")); // Join сам применяет ToString!
                stopwatch.Stop(); //Останавливаем секундомер, чтобы записать время обработки файла
                res.Add(stopwatch.ElapsedMilliseconds.ToString() + " ms");
            }).Start();
        });
        RTB.Text += String.Join("", res) + '\n';
    }
  • Как ускорить работу потоков, чтобы был большой отрыв по времени от обычного способа???? – StepanVyacheslavovich Jul 28 '23 at 18:04
  • Потоки вам не нужны, вам нужна асинхронность (async/await), да и вообще нынче потоки очень редко используют. Ну а вообще, я надеюсь вы осознаете тот факт, что вы ограничены возможностью диска? "дисковый ввод-вывод — по существу последовательная операция". То есть, создав вы хоть миллиард потоков, файлы будут считываться диском последовательно, только если это не какой либо RAID массив, где дисков несколько, и то там свои тонкости. Поэтому, ваша цель по разбитию чтения файлов с диска на множество потоков, немного бессмысленна. Делайте Task, пусть ждет действия от диска, но потоки... – EvgeniyZ Jul 28 '23 at 18:15
  • @EvgeniyZ я придерживался задания. Повторюсь, до этого с многопоточностью не работал и не знаю в каких ситуациях как лучше действовать. Спасибо за совет, попробую Task – StepanVyacheslavovich Jul 28 '23 at 18:37
  • 1
    Многопоточность при работе с диском категорически противопоказана, я бы сказал. Не дай бог HDD будет гонять головку туда-сюда. – rotabor Jul 28 '23 at 18:39
  • Буфер нужно увеличить, совершенно точно. 4096 - это ничто. Тогда и от многопоточности может толк появиться. Нужно поэкспериментировать с размером буфера. – rotabor Jul 28 '23 at 19:53
  • FileStream нужно закрывать, см. ответ. – rotabor Jul 28 '23 at 20:09
  • 1
    Если нужно реальное ускорение, то 1) для чтения файлов надо использовать memory mapping, т.о. вы не будете пересылать данные из файлового кэша ОС в user space 2) количество потоков должно соответствовать не количеству файлов, а количеству ядер в процессоре, т.е. однажды запущенный поток обрабатывает несколько файлов 3) алгоритм должен быть таким, чтобы потоки во время работы не требовали синхронизации (mutex, semaphore и т.п.), т.е. области памяти с исходными данными и результатами работы потока нужно подготовить заранее. Тогда все заработает действительно быстро – avp Jul 28 '23 at 21:33
  • @avp хочу уточнить п. 2. Это касается только чисто вычислительных задач. Если речь идёт о вводе-выводе, то на устройство нужно выделить один поток, а остальное согласно п. 2. Что можно распарралелить? Подсчёт байтов. Только, к сожалению, это самая быстрая часть задачи. Основное время занимает чтение файлов. – rotabor Jul 29 '23 at 16:02
  • @rotabor, в целом, согласен, но тут есть системозависимая специфика. Для такой задачи теоретически достаточно 2-х потоков (один инициирует чтение, второй подсчитывает уже скачанные с диска байты). Конечно, при таком подходе возникнут еще и дополнительные расходы на синхронизацию. Но, это без учета специфики оптимизации порядка операций IO в OS. Понятно, что последовательные запросы из разных процессов можно переупорядочивать, минимизируя перемещение головок диска ("лифт Линуса" в linux). Делает ли OS то же самое для запросов к разным файлам из одного процесса? Не знаю – avp Jul 29 '23 at 16:51
  • Вам тут много всего написали, дальше уже давайте как-то сами. Или задавайте более конкретные вопросы. Можете так же поделиться результатами своей работы. – rotabor Jul 30 '23 at 17:55

2 Answers2

2

Тут есть над чем поработать:

    public void CountBytes(string filename)
    {
        byte[] buffer = new byte[4096]; // нужно завести один буфер, чтобы не тратить каждый вызов метода время на его создание
        // но это не для многопоточного варианта
        // 4096 - это ничто, нужно кратно увеличить, тогда и от многопоточности может толк появиться 
        int bytesRead = 0; // это тоже как бы лишнее, но ни на что не влияет
        using (FileStream reader = File.OpenRead(filename))
            while ((bytesRead = reader.Read(buffer, 0, buffer.Length)) > 0)
                for (int i = 0; i < bytesRead; Bytes[buffer[i++]]++);
                // byte b = buffer[i]; - это лишнее, значение используется один раз.
                // и не факт, что будет оптимизировано
    }

Вместо этого

            res.Add(GetFileName(tmpPath)); //Записываем первым имя файла
            res.Add(" {");
            for (int i = 0; i < 256; i++) //Переписываем кол-во каждого байта
            {
                if (i != 255)
                    res.Add(tmpobj.GetBytes()[i].ToString() + ", ");
                else
                    res.Add(tmpobj.GetBytes()[i].ToString() + "} ");
            }

написать

return String.Concat(
    GetFileName(tmpPath), " {"
    , String.Join(", ", tmpobj.GetBytes()) // Join сам применяет ToString!
    , "}");

Можно сделать обработку файла задачей

        string CalculateNew(string tmpPath) {
            var a = new byte[5] { 11, 34, 23, 56, 74 }; // это пример такой
            return String.Join(", ", a);
        }
    ...
    RTB.Text += DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture) + "\t";
    RTB.Text += Task.Run(() => CalculateNew(tmpPath)).Result) + "\t";
rotabor
  • 4,251
  • Мой куратор сказал удалить обращение к компонентам формы из потока и что можно обратный вызов сделать в компоненте потока, например но я не понимаю, как это сделать – StepanVyacheslavovich Jul 28 '23 at 18:40
  • Разберёмся. Это нужно убрать от "Action action3 = ()..." и далее. – rotabor Jul 28 '23 at 18:47
  • я исправил ваши замечания, кроме вывода "лога". Оставил только вычислительные процессы, где просто файл обрабатывается, но все равно по времени потоки не опережают обычный способ. Я уже просто с секундомером сижу и замеряю :)))) – StepanVyacheslavovich Jul 30 '23 at 17:39
  • я отслеживаю работу потоков по их выполнению в "выводе", где пишется о завершении потока, но все равно потоки не быстрее – StepanVyacheslavovich Jul 30 '23 at 17:41
2

Просто вкину альтернативный вариант.

Накпишем функцию, которая читает файл с буфером

private ulong[] GetBytesStats(string file)
{
    var ret = new ulong[256];
    var buffer = new byte[4096];
    var len = 0;
    using (var stream = new BufferedStream(File.OpenRead(file), 10 * 1024 * 1024))
        while ((len = stream.Read(buffer, 0, buffer.Length)) > 0)
            for (int i = 0; i < len; i++) ret[buffer[i]]++;
    return ret;
}

Перевод статистики в строку

private string ToString(ulong[] data)
{
    return string.Join(", ", data);
}

Метод, который обрабатывает файл и выводит его на консоль

private void ProcessFile(string file)
{   
    Console.WriteLine($"Stats for {file} = {ToString(GetBytesStats(file))}");   
}

запускаем метод параллельно

var files = new List<string>();
files.Add(@"C:\Dev\Temp\some_file.png");
files.Add(@"C:\Dev\Temp\some_file.png");
files.Add(@"C:\Dev\Temp\some_file.png");
files.Add(@"C:\Dev\Temp\some_file.png");
files.Add(@"C:\Dev\Temp\some_file.png");
files.Add(@"C:\Dev\Temp\some_file.png");
Parallel.ForEach(files, f=>ProcessFile(f));

Результат

Stats for C:\Dev\Temp\some_file.png = ......
Stats for C:\Dev\Temp\some_file.png = ......
Stats for C:\Dev\Temp\some_file.png = ......
Stats for C:\Dev\Temp\some_file.png = ......
Stats for C:\Dev\Temp\some_file.png = ......
Stats for C:\Dev\Temp\some_file.png = ......

Можно это всё и асинхронно сделать - но это уже как домашнее задание.

tym32167
  • 32,857
  • у меня не консольное приложение, а оконное. В кастве "консоли" richtextbox. Из-за этого нужно прерывание для вывода инф-ии на richtextbox – StepanVyacheslavovich Jul 30 '23 at 16:51
  • Если уже умеете пробрасывать вызов в UI поток, то адаптировать пример под себя вам труда не составит – tym32167 Jul 30 '23 at 17:13
  • я делаю прерывание через action или это плохо пример? – StepanVyacheslavovich Jul 30 '23 at 17:36
  • это называется проброс вызова в UI поток, а не прерывание. 2) action - это просто указатель на функцию, а не способ проброса вызова 3) Как правильно делать нписано тут, если вкратце, то myRichTechBox.Invoke(....)
  • – tym32167 Jul 30 '23 at 18:05
  • я добавил изменения в вопросе, посмотрите, пожалуйста – StepanVyacheslavovich Jul 31 '23 at 12:27
  • На ваш изначальный вопрос уже ответили. Если у вас новый вопрос - задавайте его отдельно. Вы не можете просто бесконечно менять вопрос, чтобы получить несколько ответов - иаче авторы ответов не получат свои плюсики. Потому придерживайтесь правила: 1 вопрос - 1 ответ. Новый вопрос - задавайте отдельно. – tym32167 Jul 31 '23 at 17:03