3

I am wondering if I should be using Parallel.ForEach() for my case. For a bit of context: I am developing a small music player using the NAudio library. I want to use Parallel.ForEach() in a factory method to quickly access .mp3 files and create TrackModel objects to represent them (about 400). The code looks like this:

public static List<TrackModel> CreateTracks(string[] files)
{
    // Guard Clause
    if (files == null || files.Length == 0) throw new ArgumentException();

    var output = new List<TrackModel>();

    TrackModel track;

    Parallel.ForEach(files, file =>
    {
        using (MusicPlayer musicPlayer = new MusicPlayer(file, 0f))
        {
             track = new TrackModel()
             {
                 FilePath = file,
                 Title = File.Create(file).Tag.Title,
                 Artist = File.Create(file).Tag.FirstPerformer,
                 TrackLength = musicPlayer.GetLengthInSeconds(),
             };
         }

         lock (output)
         {
             output.Add(track);
         }
   });

   return output;
}

Note: I use lock to prevent multiple Threads from adding elements to the list at the same time.

My question is the following: Should I be using Parallel.ForEach() in this situation or am I better off writing a normal foreach loop? Is this the right approach to achieve better performance and should I be using multithreading in combination with file access in the first place?

3
  • stackoverflow.com/questions/66146811/… I'd suspect there's not much improvement if any, reading tags are really light on CPU Commented Jul 16, 2022 at 12:31
  • 1
    Parallelism would make sense here, if you can make use of async file IO. Although I wouldn't use Parallel.ForEach for that. Also, you seem to call File.Create twice? Commented Jul 16, 2022 at 14:55
  • @marsze Thank you so much for the observation! I have been doing it wrong all along by calling File.Create multiple times. I also realized that Taglib.File inherits from IDisposable, meaning I can add it to the using. Great suggestion! Commented Jul 17, 2022 at 11:20

2 Answers 2

4

You're better off avoiding both a foreach and Parallel.ForEach. In this case AsParallel() is your friend.

Try this:

public static List<TrackModel> CreateTracks(string[] files)
{
    if (files == null || files.Length == 0) throw new ArgumentException();

    return
        files
            .AsParallel()
            .AsOrdered()
            .WithDegreeOfParallelism(2)
            .Select(file =>
            {
                using (MusicPlayer musicPlayer = new MusicPlayer(file, 0f))
                {
                    return new TrackModel()
                    {
                        FilePath = file,
                        Title = File.Create(file).Tag.Title,
                        Artist = File.Create(file).Tag.FirstPerformer,
                        TrackLength = musicPlayer.GetLengthInSeconds(),
                    };
                }
            })
            .ToList();
}

This handles all the parallel logic and the locking behind the scenes for you.

Sign up to request clarification or add additional context in comments.

1 Comment

I would add .AsOrdered() after the .AsParallel(), the get the TrackModels in the original order of the files. I would also add .WithDegreeOfParallelism(2), because I consider highly unlikely for a parallelization level more than 2 to be beneficial, when we are talking about reading and writing from the file-system. Unless the hardware is not a HDD or a SSD, but some exotic storage device from the future. 😃
0

Combining the suggestion from comments and answers and adapting them to my code I was able to solve my issue with the following code:

public List<TrackModel> CreateTracks(string[] files) 
{
    var output = files
                     .AsParallel()
                     .Select(file =>
                     {
                         using MusicPlayer musicPlayer = new MusicPlayer(file, 0f);
                         using File musicFile = File.Create(file);

                         return new TrackModel()
                         {
                             FilePath = file,
                             Title = musicFile.Tag.Title,
                             Artist = musicFile.Tag.FirstPerformer,
                             Length = musicPlayer.GetLengthInSeconds(),
                         };
                     })
                     .ToList();
    return output;

}

Using AsParallel() helped significantly decrease the loading time which is what I was looking for. I will mark Enigmativity's answer as correct because of the clever idea. Initially, it threw a weird AggregateException, but I was able to solve it by saving the output in a variable and then returning it.

Credit to marsze as well, whose suggestion helped me fix a memory leak in the application and shave off 16MB of memory (!).

1 Comment

Whatever was the reason for the AggregateException, it's unlikely that the output variable solved the problem. It might be an intermittent error that you'll observe again in the future. Tip: the AggregateException has an InnerExceptions property, where the real exceptions are stored.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.