Страницы

Поиск по вопросам

вторник, 9 октября 2018 г.

Насколько верна такая реализация многопоточности?

Не знаю, корректно ли на этом сайте такие вопросы задавать... В общем я не уверен что код ниже верно написан. Свою задачу он выполняет, проверял неоднократно. Однако меня гложут сомнения в том, насколько эта реализация может считаться правильной. Быть может на C# такого рода задачу принято реализовывать иначе? Хотелось бы получить критику и разъяснения.
Сама программа - аудио конвертер. Ищет в папке подходящие файлы и запускает внешний, консольный конвертер для их преобразования в wav.
public partial class Form1 : Form { static object locker = new object(); string[] findFiles; int fifi = -1;
public Form1() { InitializeComponent(); Conv(); {
async void Conv() { findFiles = Directory.GetFiles(Application.StartupPath, "*.ape", SearchOption.AllDirectories).Union(Directory.GetFiles(Application.StartupPath, "*.ogg", SearchOption.AllDirectories)).ToArray();
// Поддерживаем не более 8 ядер. int p; if (Environment.ProcessorCount < 8) p = Environment.ProcessorCount; else p = 8;
int t; // Если количество файлов больше чем ядер, то устанавливаем столько потоков, сколько ядер. if(findFiles.Length > p) t = p; else // Если файлов меньше чем ядер, то потоков устанавливаем сколько файлов. t = findFiles.Length;
// Устанавливаем количество потоков. Task[] tasks = new Task[t];
// Запускаем потоки. for (int i = 0; i <= p - 1; i++) { if(i <= findFiles.Length - 1) { tasks[i] = new Task(() => ConvertDoWork(++fifi)); tasks[i].Start(); } }
// Ждём выполнения всех задач. await TaskEx.WhenAll(tasks);
Application.Exit(); }
int p, pc; public void ConvertDoWork(int num) { int n = num;
if ((findFiles.Length == 0) || (n > findFiles.Length)) return;
string s = findFiles[n];
ProcessStartInfo startInfo = new ProcessStartInfo(); if (Path.GetExtension(s).Equals(".ogg")) { startInfo.FileName = "oggdec.exe"; startInfo.Arguments = "-Q \"" + s + "\""; } else if (Path.GetExtension(s).Equals(".ape")) { startInfo.FileName = "MAC.exe"; startInfo.Arguments = "\"" + s + "\" \"" + Path.ChangeExtension(s, ".wav") + "\" -d"; } startInfo.WorkingDirectory = Application.StartupPath; startInfo.CreateNoWindow = true;
Process processReg = new Process(); processReg.StartInfo = startInfo; processReg.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; processReg.Start(); processReg.WaitForExit();
lock (locker) File.Delete(s);
int percentComplete = (int)Math.Round((double)(100 * n) / findFiles.Length); pc = percentComplete / 10;
try { this.Invoke((MethodInvoker)delegate { progressBar1.Value = percentComplete;
// Если не выбрано не отображать прогресс, то отображаем всплывашку. if ((notifyIcon1.Visible) && (!chkNotNotify.Checked) && (!pc.Equals(p))) { notifyIcon1.ShowBalloonTip(300, "Прогресс", "Выполнено " + percentComplete + "% работы", ToolTipIcon.Info); p = pc; }
});
} catch (System.Exception ex) {
}
lock (locker) ++fifi; if (fifi < findFiles.Length) ConvertDoWork(fifi); }


Ответ

1.
Слишком много переменных уровня класса. Каждую из них надо отдельно перепроверять - а мне лень. Куда безопаснее локальные переменные и параметры функции.
2.
Переменная fifi используется опасным образом. Лучше от нее вовсе отказаться, а распараллеливание делать другими методами.
Для распараллеливания через задачи я сам использую и всем рекомендую вот такую конструкцию:
await TaskEx.WhenAll( from file in findFiles select Task.Run(() => ConvertDoWork(file)) );
Вам, я так понимаю, нужно еще ограничить число число одновременно выполняемых задач числом ядер. Вручную это можно сделать вот так:
class LimitedConcurrencyGuard { private readonly object _lock = new object(); private readonly Queue> queue = new Queue>(); private int slots;
public LimitedConcurrencyGuard (int slots) { this.slots = slots; }
public async Task AcquireSlot() { TaskCompletionSource tcs; lock(_lock) { if (slots > 0) { slots--; return new Slot(this); } tcs = new TaskCompletionSource(); queue.Enqueue(tcs); } await tcs.Task; return new Slot(this); }
private class Slot : IDisposable { private readonly LimitedConcurrencyGuard owner; public void Slot(LimitedConcurrencyGuard owner) { this.owner = owner; }
public void Dispose() { TaskCompletionSource tcs; lock(owner._lock) { if (owner.queue.Count > 0) tcs = owner.queue.Dequeue(); else { owner.slots++; return; } } tcs.SetResult(null); } } }
// ...
var guard = new LimitedConcurrencyGuard(Environment.ProcessorCount); await TaskEx.WhenAll( from file in findFiles select Task.Run(async () => { using (await guard.AcquireSlot()) ConvertDoWork(file); }) );
Если поискать в гугле по запросу "ограничение числа одновременно выполняемых задач" - наверняка найдутся и другие реализации. Например, реализующие ограничение на стороне планировщика задач.
Или же можно сначала побить исходную последовательность файлов на куски, а потом запустить их параллельно:
static IEnumerable> SplitAndBatch (ICollection items, int batches) { var count = items.Count; var it = items.GetEnumerator(); while (count > 0) { var current = (count + batches - 1) / batches; // Это на самом деле частное, округленное вверх count -= current;
var batch = new T[current]; for (var i=0; i// ...
await TaskEx.WhenAll( from batch in SplitAndBatch(findFiles, Environment.ProcessorCount) select Task.Run(() => { foreach (var file in batch) ConvertDoWork(file); }) );
В любом случае, логику разделения файлов по ядрам процессора лучше вынести за пределы метода ConvertDoWork - это называется "разделение ответственности".
А может быть, нужно вообще отказаться от использования задач и использовать Parallel API - где ограничение по числу ядер уже реализовано.
3.
Уведомление пользователя о текущем прогрессе - дело хорошее, но сделано оно на устаревшей технологии. Элементарно: если пользователь закроет окно - то метод Invoke вылетит с ошибкой.
Для уведомлений о прогрессе есть такие классы как IProgress/Progress
public void ConvertDoWork(string file, IProgress progress) { // ... progress.Report(null); // ... }
// ...
progressBar1.Maximum = findFiles.Length; var progress = new Progress(_ => { progressBar1.Value = ++done; }); await TaskEx.WhenAll( from file in findFiles select Task.Run(() => ConvertDoWork(file, progress)) );
Достоинство данного способа - в том, что
у метода ConvertDoWork забирается лишняя ответственность (индикация хода выполнения); класс Progress берет задачу перехода обратно в поток UI на себя.
Кстати, как вам такая идея - считать файлы не по 1 каждый, а пропорционально их размеру?..
4.
Формирование Arguments лучше делать через string.Format - это куда проще в восприятии.
5.
Лучше не использовать Application.Exit просто так: если в дальнейшем понадобится использовать старый код как часть большего проекта, то придется больно и мучительно искать все места, где он может закрыть всю программу.
В данном случае достаточно просто закрыть форму.
PS по поводу net 4.0
Многие замечательные классы реализованы в проектах Mono и .NET Core.
Лицензии у них свободные, код открытый - так что 1-2 класса (тот же Progress) зачастую можно прямо оттуда скопировать к себе в проект. Только если проект будете распространять - то с лицензиями придется разобраться.

Комментариев нет:

Отправить комментарий