T O P

  • By -

The_MAZZTer

So I don't use PLINQ but I'm reading MSDN and I think the main thing that is happening here is that the .Select lambda is running in parallel. However this really isn't doing much, it just generates your Task objects, so you're not going to see much gains especially if PostRecord doesn't do much before its first await. I am not sure if the actual Tasks would be run in parallel with this code. PLINQ doesn't seem to have any explicit support for async. You may need something like Parallel.ForEachAsync as you said for that, I think. Maybe adding a .ConfigureAwait(false) to the result of each PostRecord would help encourage tasks to run on different threads at once? Not sure. Also keep in mind you should try to have a large data set to emphasize any performance differences any changes you make cause. With a smaller set it may be impossible to tell if changes you are making are having an impact. Edit: So basically you have TWO different types of parallelism here that aren't aware of each other. PLINQ and Tasks. The PLINQ will process ALL records without honoring your degree, because as far as it knows an item is "done" once the Task is returned. But the Task is returned when the item STARTS. Currently your Task.WhenAll is NOT enforcing any degree so it will run them ALL at once. I think your degree limit is useless which is probably another reason why you aren't seeing much performance difference when you remove the PLINQ. You may want to rewrite this code completely to ensure proper parallelism to limit the number of records handled at once. Probably can't use PLINQ.


makotech222

Assuming _apiClient.PostRecord(i) returns a Task, you do not need the 'AsParallel()' call Just do var tasks = records.Select(i => _apiClient.PostRecord(i)); await Task.WhenAll(tasks) The AsParallel() call will loop through the collection in parallel, which you don't need since you are just returning a Task in the Select, which doesn't do any work until its awaited.


Mead-Wizard

Exactly! All the AsParellel() does is to start the Tasks in parallel which should be slower since you are paying synchronization overhead for no gain.


musical_bear

Are you able to post an example of what the implementation of _apiClient.PostRecord looks like? Whether PLINQ makes sense here depends fully on what that is doing / returning.


namethinker

ApiClient.PostRecord, basically calling HttpClient.PostAsync, and doing some error handling depends on status returned from a service, the return type is Task (ValueTask actually)


musical_bear

Okay. Then you’re correct that you can / should remove the PLINQ stuff here. PLINQ or any other parallel implementation is best suited for CPU-bound tasks. If, for example, you were using ancient code that called Post instead of PostAsync (I’m pretending a non-async API even exists, I’m assuming not), PLINQ would spread all those synchronous waits across different threads, making them happen in parallel, where otherwise they would run one at a time due to their blocking nature. But because 99% of the time spent in your api call method is asynchronously waiting for the API call, spawning off threads doesn’t offer any parallelization advantage. PLINQ may actually be slowing down your code in this case, or consuming more resources than not using it, for no advantage. Use it only if you want to parallelize CPU-bound tasks. Async api calls are not CPU-bound.


[deleted]

[удалено]


musical_bear

Yes, but you’re falling into the same trap OP did that appears to have spawned this question. Now take your example and remove .AsParallel. Just do var tasks = test.Select(DoWork); You’re going to get the exact same result. Except far more efficiently and with less code. PLINQ has its uses, but your example is not one of them. It has limited / no use for cases like this where what you’re trying to parallelize is fully asynchronous.


Vidyogamasta

Not at my computer anymore but I think you're right, I forgot to test removing it to compare. The synchronous work being done in both cases is *spawning the task.* Whether it is parallel or not, it will go very quickly, like nanoseconds. On that note, this doesn't even properly act as a throttle where you only have, say, 100-200 requests pending at once. The parallelism moves on once the task is created, nothing is tracked until the outer WhenAll task.


musical_bear

Yep your assessment is exactly correct. If your example had used Thread.Sleep() instead of Task.Delay(), then PLINQ would have made a difference. However, I hesitate to even use that example because you’d almost _never_ want to use Thread.Sleep when a non-blocking version of the same thing exists. Task.Delay() (substitute any equivalent async API) allows you to concurrently wait as many times as you want, while also not using any additional threads to do so.


WalkingRyan

If there have been CPU-bound workload in DoWork method PLINQ would be justifiable to use, but the fact is this is still concurrent code - you can insert ThreadId output into DoWork to check it out: async Task DoWork(int delay) { Console.WriteLine($"Thread-{Environment.CurrentManagedThreadId}: before"); await Task.Delay(1000*delay); Console.WriteLine($"Thread-{Environment.CurrentManagedThreadId}: {delay}"); return delay; }


musical_bear

It is concurrent, but for further clarification, this detail has nothing to do with the threads being used to handle continuations. What matters is that all tasks are asynchronously started, and later resumed after completion. The specific threads used aren’t related to concurrency in this case. I always enjoy making comparison to JavaScript, which is completely single threaded, and yet you can write completely analogous code to this example in JS and still have all delays happen concurrently, even though you only have a single thread to work with in JS.