I don’t know if it’s correct to ask such questions on this site ... In general, I’m not sure that the code below is correctly written. He performs his task, checked repeatedly. However, I have doubts about how this implementation can be considered correct. Perhaps on C # this kind of task can be implemented differently? I would like to receive criticism and clarification.

The program itself is an audio converter. It searches for suitable files in the folder and launches an external, console converter to convert them to 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); } 
  • four
    "is it correct to ask such questions on this site" is correct. Do not forget only the tag inspection code . - andreycha
  • one
    "It searches for files in the folder and launches ..." - for this it is easiest to use Directory.EnumerateFiles(@"c:\media\", "sound*.mp3").AsParallel().ForAll(file => { ... }); - An example here - Stack

4 answers 4

one.

Too many class level variables. Each of them needs to be rechecked separately - and I'm lazy. Much safer local variables and function parameters.

2

The variable fifi used in a dangerous way. It is better to abandon it altogether, and do parallelization with other methods.

For parallelization through tasks, I use it myself and recommend this structure to everyone:

 await TaskEx.WhenAll( from file in findFiles select Task.Run(() => ConvertDoWork(file)) ); 

I understand that you still need to limit the number of the number of simultaneously performed tasks to the number of cores. You can do it manually like this:

 class LimitedConcurrencyGuard { private readonly object _lock = new object(); private readonly Queue<TaskCompletionSource<object>> queue = new Queue<TaskCompletionSource<object>>(); private int slots; public LimitedConcurrencyGuard (int slots) { this.slots = slots; } public async Task<IDisposable> AcquireSlot() { TaskCompletionSource<object> tcs; lock(_lock) { if (slots > 0) { slots--; return new Slot(this); } tcs = new TaskCompletionSource<object>(); 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<object> 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); }) ); 

If you search Google for the query "limiting the number of simultaneously executed tasks", there will most certainly be other implementations. For example, implement the restriction on the side of the task scheduler.

Or you can first beat the original sequence of files into pieces, and then run them in parallel:

 static IEnumerable<IEnumerable<T>> SplitAndBatch<T> (ICollection<T> 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<current; i++) { it.MoveNext(); batch[i] = it.Current; } yield return batch; } } // ... await TaskEx.WhenAll( from batch in SplitAndBatch(findFiles, Environment.ProcessorCount) select Task.Run(() => { foreach (var file in batch) ConvertDoWork(file); }) ); 

In any case, it is better to take the logic of dividing files across processor cores beyond the limits of the ConvertDoWork method - this is called “responsibility sharing”.

Or maybe you need to completely abandon the use of tasks and use the Parallel API - where the limit on the number of cores has already been implemented.

3

Notifying the user of the current progress is a good thing, but it is done on outdated technology. Elementary: if the user closes the window, then the Invoke method will crash with an error.

For progress notifications, there are classes such as IProgress<T> / Progress<T> :

 public void ConvertDoWork(string file, IProgress<object> progress) { // ... progress.Report(null); // ... } // ... progressBar1.Maximum = findFiles.Length; var progress = new Progress<object>(_ => { progressBar1.Value = ++done; }); await TaskEx.WhenAll( from file in findFiles select Task.Run(() => ConvertDoWork(file, progress)) ); 

The advantage of this method is that

  • extra responsibility is taken from the ConvertDoWork method (progress indication);
  • the Progress class takes the task of transitioning back to the UI flow to itself.

By the way, how do you like this idea - to count files not 1 each, but proportionally to their size? ..

four.

Formation of Arguments best done through string.Format - this is much simpler in perception.

five.

It is better not to use Application.Exit just like this: if you later need to use the old code as part of a larger project, you will have to painfully and painfully search for all the places where it can close the entire program.

In this case, simply close the form.

PS about net 4.0

Many great classes are implemented in Mono and .NET Core. projects .NET Core.

Their licenses are free, the code is open - so 1-2 classes (the same Progress ) can often be copied directly from there to your project. Only if the project will be distributed - then the licenses will have to figure it out.

  • There was a problem with the IProgress class. When adding it to a project, Studio emphasizes the public event EventHandler<T> ProgressChanged; and EventHandler<T> changedEvent = ProgressChanged; Complaining about Error CS0314 Type "T" cannot be used as a parameter of type "TEventArgs" in the generic type or method "EventHandler <TEventArgs>". There is no package translate or type parameter conversion from "T" to "System.EventArgs". - Raf-9600
  • Also complains about m_synchronizationContext = SynchronizationContext.CurrentNoFlow ?? ProgressStatics.DefaultContext; m_synchronizationContext = SynchronizationContext.CurrentNoFlow ?? ProgressStatics.DefaultContext; Error CS0117 "SynchronizationContext" does not contain a definition for "CurrentNoFlow". Can you tell me how to fix this? - Raf-9600
  • one
    EventHandler<> - you can copy it in the same way (just be careful, because there is already a non-template version). And you can throw out this event altogether; nobody needs it. CurrentNoFlow - can be replaced by Current - Pavel Mayorov

There are a few comments / suggestions. Without a certain order and not only about multithreading:

  • Methods with an async void signature are suitable only for events or methods with similar semantics. In such methods, you usually need to do a global try/catch , because if an exception occurs, you will either not know about it, or this exception will kill the process (in the case of .NET 4.0 it will be like that).
  • Running the "asynchronous method" from the constructor is not the best idea. The constructor cannot be labeled async , which apparently caused the Conv method to make async void . Make this method public and call it separately after creating the form (not forgetting await 's call). If, when creating an object, it is necessary to execute some code that is asynchronous, then instead of the constructor, you need to use the factory method, which in turn will also be asynchronous.
  • As @Qwertiy already noted, increment is not a thread-safe operation. Use Interlocked.Increment() . And then lok at the removal of the file you will not need.
  • You use the same object ( locker ) to synchronize access to unrelated “resources”. This is not entirely correct, since if one thread takes the first “resource” and another thread needs the second “resource”, it should not wait. Each "resource" has its own object for synchronization.
  • The ConvertDoWork method ConvertDoWork not control the launch of new conversions. For this in the method Conv db. The following diagram: the main loop rotates until the list of files for conversion is not empty (get the List ), then you shedul p tasks, then you wait for await Task.WhenAny() , then you throw out the converted file from the list and shedulit task for the new file. In this case, the fifi counter can be eliminated altogether.
  • To notify progress, use IProgress. How - read the link . True, this is only for .NET 4.5 and higher.
  • Application.Exit() in the business code looks weird. Let the user cross the application or button separate.
  • IProgress seems to be available only in .net4.5, and for me it is critical that it be 4.0. I have async in my code thanks to the Microsoft.Bcl.Async library. Thank you for your feedback. I will try to comprehend and take into account everything you said. - Raf-9600
  • @ Raf-9600 yes, did not notice, I apologize. Removed this item. - andreycha
  • Progress can be copied to your project directly from the .NET Core repositories. MIT license there. - Pavel Mayorov

Instead of a piece of code to calculate the number of cores, the number of threads created, etc. I would advise for these purposes to look towards the Parallel class, which is designed to support the implementation of parallel tasks. She herself decides how many tasks to create, how many threads. Using the ForEach function, your "big" piece of code can be rewritten in this way.

 Parallel.ForEach(findFiles, (currentFile) => ConvertDoWork(currentFile); 

Thus, the declaration of the ConvertDoWork function needs to be changed so that it immediately ConvertDoWork file name.

 public void ConvertDoWork(string filename) { /// ваш код по обработке файла } 

Then you don’t have to bother with any fifi . The ForEach function will complete its execution as it will process all files.

     tasks[i] = new Task(() => ConvertDoWork(++fifi)); 

    Wrong. The increment is not atomic.
    If tasks are executed in parallel, they can get the same numbers.