Помогите "ускорить" потоки C#

Рейтинг: 2Ответов: 2Опубликовано: 28.07.2023

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

Разработать многопоточное приложение, выполняющее следующие действия. Для каждого файла в директории построить массив из 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(() => { CalculateThread(RTB, path); }).Start();
                }
            }

            private void CalculateNew(RichTextBox RTB, string tmpPath)
            {
            RTB.Text += DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture) + "\t";
            Stopwatch stopwatch = new Stopwatch(); //Создаём секундомер
            stopwatch.Start(); //Запускаем секундомер
            Converter tmpobj = new Converter(); //Создаём объект "файла"
            tmpobj.CountBytes(tmpPath); //Метод, который читает файл поблочно и сразу обрабатывает блок(считает байты)
            List<string> res = new List<string>(); //создаём массив для результирующей строки
            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() + "} ");
            }
            stopwatch.Stop(); //Останавливаем секундомер, чтобы записать время обработки файла
            res.Add(stopwatch.ElapsedMilliseconds.ToString() + " ms");
            RTB.Text += String.Join("", res) + '\n'; //RTB - richtextbox как консоль для вывода информации
            }

            private void CalculateThread(RichTextBox RTB, string tmpPath) //Для варианта с потокам всё вынесено в отдельную функцию
            {
                lock (locker)
                {
                    Action action2 = () => RTB.Text += DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture) + "\t";
                    if (RTB.InvokeRequired)
                    {
                        RTB.Invoke(action2);
                    }
                    else
                    {
                        action2();
                    }
                    Stopwatch stopwatch = new Stopwatch(); //Создаём секундомер
                    stopwatch.Start(); //Запускаем секундомер
                    Converter tmpobj = new Converter(); //Создаём объект "файла"
                    tmpobj.CountBytes(tmpPath); //Метод, который читает файл поблочно и сразу обрабатывает блок(считает байты)
                    List<string> res = new List<string>(); //создаём массив для результирующей строки
                    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() + "} ");
                    }
                    stopwatch.Stop(); //Останавливаем секундомер, чтобы записать время обработки файла
                    res.Add(stopwatch.ElapsedMilliseconds.ToString() + " ms");

                    Action action3 = () => RTB.Text += String.Join("", 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)) > 0)
        {
            for (int i = 0; i < 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';
    }

Ответы

▲ 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";
▲ 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 = ......

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