77

I'm calling a slow webservice in parallel. Things were great until I realized I need to get some information back from the service. But I don't see where to get the values back. I can't write to the database, HttpContext.Current appears to be null inside of a method called using Parallel.ForEach

Below is a sample program (in your mind, please imagine a slow web service instead of a string concatenation)

using System;
using System.Threading.Tasks;

class Program
{
    static void Main(string[] args)
    {
        WordMaker m = new WordMaker();
        m.MakeIt();
    }
    public class WordMaker
    {
        public void MakeIt()
        {
            string[] words = { "ack", "ook" };
            ParallelLoopResult result = Parallel.ForEach(words, word => AddB(word));
            Console.WriteLine("Where did my results go?");
            Console.ReadKey();
        }
        public string AddB(string word)
        {
            return "b" + word;
        }
    }

}
7
  • A different overload of Parallel.ForEach may be what you want: msdn.microsoft.com/en-us/library/dd991486.aspx Commented Sep 26, 2012 at 21:45
  • Unfortunately that's not really something you can do like that. Parallel.Foreach() just wasn't built for keeping track of returns. I would, however, suggest using ref parameters in your AddB function. That might do it. Commented Sep 26, 2012 at 21:46
  • @PhillipSchmidt: Not with the overload used in the example anyway... Commented Sep 26, 2012 at 21:47
  • @AustinSalonen What's another overload that would work? I'm not arguing, I just wasn't aware of anything that would do what he wants. Commented Sep 26, 2012 at 21:49
  • 1
    @AustinSalonen you expect me to use my eyes before asking stupid questions? Jeez. Just kidding - reading it now. Commented Sep 26, 2012 at 21:55

6 Answers 6

86

You've discarded it in here.

ParallelLoopResult result = Parallel.ForEach(words, word => AddB(word));

You probably want something like,

ParallelLoopResult result = Parallel.ForEach(words, word =>
{
    string result = AddB(word);
    // do something with result
});

If you want some sort of collection at the end of this, consider using one of the collections under System.Collections.Concurrent, like ConcurrentBag

ConcurrentBag<string> resultCollection = new ConcurrentBag<string>();
ParallelLoopResult result = Parallel.ForEach(words, word =>
{
    resultCollection.Add(AddB(word));
});

// Do something with the result
Sign up to request clarification or add additional context in comments.

10 Comments

I think the ParallelLoopResult does nothing useful here. +1 though
.AsParallel() in LINQ would be a lot better
@eranotzap That's the issue that is solved by using a ConcurrentBag instead of a List.
@NineTails so it is an issue. only adding not adding and removing. Is only adding to a list a concurrency issue. maybe it's done in an atomic way. meaning that the index in the backing array might be incremented atomically
@PatrickfromNDependteam your latest edit (revision 6) deviates a lot from the original answer, to a point that invalidates the voting system of the site. I would suggest to post your contribution as a separate answer instead.
|
47

Your may consider using AsParallel extension method of IEnumerable, it will take care of the concurrency for you and collect the results.

words.AsParallel().Select(AddB).ToArray()

Synchronisation (e.g. locks or concurrent collections that use locks) are usually bottleneck of concurrent algorithms. The best is to avoid synchronisation as much as possible. I am guessing that AsParallel uses something smarter like putting all the items produced on single thread into a local non-concurrent collection and then combining these at the end.

6 Comments

This is significantly better.
When I try this the code in select doesn't seems running in different threads. But Parallel.Foreach does it.
Are you sure you're using AsParallel? What makes you think it's not using more threads?
Optionally you can also add .AsOrdered() after the .AsParallel(), to receive the results in the order of the input values.
Is there a way to do this, for a method AddB that takes 3 arguments, of which only the first is the IEnumerable?
|
15

Do not use ConcurrentBag to collect results as it is slower. Use local lock instead.

var resultCollection = new List<string>();
object localLockObject = new object();

Parallel.ForEach<string, List<string>>(
      words,
      () => { return new List<string>(); },
      (word, state, localList) =>
      {
         localList.Add(AddB(word));
         return localList;
      },
      (finalResult) => { lock (localLockObject) resultCollection.AddRange(finalResult); }
); 

// Do something with resultCollection here

3 Comments

Do you have any stats to show that ConcurrentBag is slower than using our own object lock? I just want to know how slow it is, since it makes my code look cleaner than using object lock.
@dineshygv IMHO Difference is negligible stackoverflow.com/questions/2950955/…
Or don't use any locking at all ;-)
8

This seems safe, fast, and simple:

    public string[] MakeIt() {
        string[] words = { "ack", "ook" };
        string[] results = new string[words.Length];
        ParallelLoopResult result =
            Parallel.For(0, words.Length, i => results[i] = AddB(words[i]));
        return results;
    }

2 Comments

This is likely to cause cache ping-pong, although still substantially better than concurrent collection.
Very very wise approach! @overlord-zurg
6

In the particular cases where the size of the collection is know upfront - which is often the case in practice - an array can be use instead of an expensive concurrent collection. There is no risk of collision since each loop accesses its own slot in the ouputs array. As a bonus outputs are stored with the same order as inputs:

const int NB_WORDS = 1000;
var inputs = new string[NB_WORDS];
for(var i= 0; i < NB_WORDS; i++) { inputs[i] = i.ToString(); }

var outputs = new string[NB_WORDS];

Parallel.For(0, NB_WORDS, index => {
   string word = inputs[index];
   string result = word + word; // Operation on word
   outputs[index] = result; // No need of a concurrent collection to store the result!
});

Debug.Assert(outputs.All(result => !string.IsNullOrEmpty(result)));

2 Comments

Instead of enumerating an Enumerable.Range with the Parallel.ForEach, isn't it simpler to use the Parallel.For method?
@TheodorZoulias sure thanks
3

How about something like this:

public class WordContainer
{
    public WordContainer(string word)
    {
        Word = word;
    }

    public string Word { get; private set; }
    public string Result { get; set; }
}

public class WordMaker
{
    public void MakeIt()
    {
        string[] words = { "ack", "ook" };
        List<WordContainer> containers = words.Select(w => new WordContainer(w)).ToList();

        Parallel.ForEach(containers, AddB);

        //containers.ForEach(c => Console.WriteLine(c.Result));
        foreach (var container in containers)
        {
            Console.WriteLine(container.Result);
        }

        Console.ReadKey();
    }

    public void AddB(WordContainer container)
    {
        container.Result = "b" + container.Word;
    }
}

I believe the locking or concurrent objects isn't necessary unless you need the results to interact with one another (like you were computing a sum or combining all the words). In this case ForEach neatly breaks your original list up and hands each thread its own object that it can manipulate all it wants without worrying about interfering with the other threads.

2 Comments

Yes, that would work for Console apps, but event for Console apps you might want to aggregate them first in a collection, or else you get interleaved results to the Console window.
The Console.WriteLine commands are running synchronously on the main thread and it will print the results in the order they were defined in the original List after Parallel.ForEach finishes processing all the list items and returns. If I were calling WriteLine from within the Parallel.ForEach then yes the results would be interleaved.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.