Why doesn't my function await the end of ForEachAsync? - c#

I'm trying to fill my model with data that I get from an asynchronous operation to my database. The problem is that the function returns the View (without the completed model), despite my await call.
I have tried to put a timer (I know that is not the solution), to be sure that the problem come from the asynchronous, I have also tried to put on comment some part of code inside my ForEachAsync, but it doesn't seem to help.
I get a list of project, that I fill with some additional information, finally, I assign my object to my model then return the View
public async Task<IActionResult> newProjetList(int GestionaireId, int VilleId)
{
ProjetListModel model = new ProjetListModel();
ProjetService projetService = new ProjetService(connectionString);
UserServices userServices = new UserServices(connectionString);
AvancementService avancementService = new AvancementService(connectionString);
VilleService villeService = new VilleService(connectionString);
List<Projet> projets = await projetService.NewProjetLine(GestionaireId, VilleId);
await projets.ToAsyncEnumerable().ForEachAsync(async p =>
{
int villeId = await villeService.getVilleIdByProjetId(p.ProjetId);
Ville ville = await villeService.GetById(villeId);
p.Ville = ville.VilleLabel;
p.GestionnaireProjet = await userServices.GetNameById(p.GestionnaireProjetId ?? 0);
await p.SousProjet.ToAsyncEnumerable().ForEachAsync(async sp =>
{
sp.Avancement = await avancementService.GetLabelById(sp.AvancementId);
});
});
model.projets = projets;
//System.Threading.Thread.Sleep(5000);
return View("ProjetList", model);
}
I expected an output with the missing information (here are the 'ville', 'gestionnairesProjet' and 'Avancement'

ForEachAsync only takes an Action<...>, not a Func<..., Task>, so the async lambda your code is passing to ForEachAsync is becoming an async void method. One of the primary reasons async void should be avoided is that it's not easy to determine when the method completes - and in fact, in this case, there is nothing ensuring that it will complete before sending the response.
I recommend doing what Marc suggested and just using foreach:
List<Projet> projets = await projetService.NewProjetLine(GestionaireId, VilleId);
foreach (var p in projects)
{
int villeId = await villeService.getVilleIdByProjetId(p.ProjetId);
Ville ville = await villeService.GetById(villeId);
p.Ville = ville.VilleLabel;
p.GestionnaireProjet = await userServices.GetNameById(p.GestionnaireProjetId ?? 0);
foreach (var sp in p.SousProject)
{
sp.Avancement = await avancementService.GetLabelById(sp.AvancementId);
}
}
model.projets = projets;
Or, if you want to use asynchronous concurrency, you can make use of Task.WhenAll:
List<Projet> projets = await projetService.NewProjetLine(GestionaireId, VilleId);
await Task.WhenAll(projects.Select(async p =>
{
int villeId = await villeService.getVilleIdByProjetId(p.ProjetId);
Ville ville = await villeService.GetById(villeId);
p.Ville = ville.VilleLabel;
p.GestionnaireProjet = await userServices.GetNameById(p.GestionnaireProjetId ?? 0);
await Task.WhenAll(p.SousProject.Select(async sp =>
{
sp.Avancement = await avancementService.GetLabelById(sp.AvancementId);
});
});
model.projets = projets;

Use ForEachAwaitAsync instead of ForEachAsync
Explanation: ForEachAsync can't wait since it is simply a multi-threaded execution of your loop (accepts Action). In fact you might have received a compiler warning by using async for your lambda return, because you intend to assign a Task to a void (unused)
ForEachAwaitAsync will wait because it accepts a Func<Task> and internally also awaits / asyncs accordingly.
For curious minds, you can see the source code here: https://github.com/dotnet/reactive/blob/main/Ix.NET/Source/System.Linq.Async/System/Linq/Operators/ForEach.cs mentioned as ForEachAwaitAsyncCore and ForEachAsync

Related

SemaphoreSlim vs Parallel Processing for API post calls in .NET

I have a chron job which calls a database table and gets about half a million records returned. I need to loop through all of that data, and send API post's to a third party API. In general, this works fine, but the processing time is forever (10 hours). I need a way to speed it up. I've been trying to use a list of Task with SemaphoreSlim, but running into issues (it doesn't like that my api call returns a Task). I'm wondering if anyone has a solution to this that won't destroy the VM's memory?
Current code looks something like:
foreach(var data in dataList)
{
try
{
var response = await _apiService.PostData(data);
_logger.Trace(response.Message);
} catch//
}
But I'm trying to do this and getting the syntax wrong:
var tasks = new List<Task<DataObj>>();
var throttler = new SemaphoreSlim(10);
foreach(var data in dataList)
{
await throttler.WaitAsync();
tasks.Add(Task.Run(async () => {
try
{
var response = await _apiService.PostData(data);
_logger.Trace(response.Message);
}
finally
{
throttler.Release();
}
}));
}
Your list is of type Task<DataObj>, but your async lambda doesn't return anything, so its return type is Task. To fix the syntax, just return the value:
var response = await _apiService.PostData(data);
_logger.Trace(response.Message);
return response;
As others have noted in the comments, I also recommend not using Task.Run here. A local async method would work fine:
var tasks = new List<Task<DataObj>>();
var throttler = new SemaphoreSlim(10);
foreach(var data in dataList)
{
tasks.Add(ThrottledPostData(data));
}
var results = await Task.WhenAll(tasks);
async Task<DataObj> ThrottledPostData(Data data)
{
await throttler.WaitAsync();
try
{
var response = await _apiService.PostData(data);
_logger.Trace(response.Message);
return response;
}
finally
{
throttler.Release();
}
}

.net - api call is blocking other api calls

I have an issue with an endpoint blocking calls from other endpoints in my app. When we call this endpoint, this basically blocks all other api calls from executing, and they need to wait until this is finished.
public async Task<ActionResult> GrantAccesstoUsers()
{
// other operations
var grantResult = await
this._workSpaceProvider.GrantUserAccessAsync(this.CurrentUser.Id).ConfigureAwait(false);
return this.Ok(result);
}
The GrantUserAccessAsync method calls set of tasks that will run on a parallel.
public async Task<List<WorkspaceDetail>> GrantUserAccessAsync(string currentUser)
{
var responselist = new List<WorkspaceDetail>();
try
{
// calling these prematurely to be reused once threads are created
// none expensive calls
var properlyNamedWorkSpaces = await this._helper.GetProperlyNamedWorkspacesAsync(true).ConfigureAwait(false);
var dbGroups = await this._reportCatalogProvider.GetWorkspaceFromCatalog().ConfigureAwait(false);
var catalogInfo = await this._clientServiceHelper.GetDatabaseConfigurationAsync("our-service").ConfigureAwait(false);
if (properlyNamedWorkSpaces != null && properlyNamedWorkSpaces.Count > 0)
{
// these methods returns tasks for parallel processing
var grantUserContributorAccessTaskList = await this.GrantUserContributorAccessTaskList(properlyNamedWorkSpaces, currentUser, dbGroups, catalogInfo).ConfigureAwait(false);
var grantUserAdminAccessTaskList = await this.GrantUserAdminAccessTaskList(properlyNamedWorkSpaces, currentUser, dbGroups, catalogInfo).ConfigureAwait(false);
var removeInvalidUserAndSPNTaskList = await this.RemoveAccessRightsToWorkspaceTaskList(properlyNamedWorkSpaces, dbGroups, currentUser, catalogInfo).ConfigureAwait(false);
var tasklist = new List<Task<WorkspaceDetail>>();
tasklist.AddRange(grantUserContributorAccessTaskList);
tasklist.AddRange(grantUserAdminAccessTaskList);
tasklist.AddRange(removeInvalidUserAndSPNTaskList);
// Start running Parallel Task
Parallel.ForEach(tasklist, task =>
{
Task.Delay(this._config.CurrentValue.PacingDelay);
task.Start();
});
// Get All Client Worspace Processing Results
var clientWorkspaceProcessingResult = await Task.WhenAll(tasklist).ConfigureAwait(false);
// Populate result
responselist.AddRange(clientWorkspaceProcessingResult.ToList());
}
}
catch (Exception)
{
throw;
}
return responselist;
}
These methods are basically identical in structure and they look like this:
private async Task<List<Task<WorkspaceDetail>>> GrantUserContributorAccessTaskList(List<Group> workspaces, string currentUser, List<WorkspaceManagement> dbGroups, DatabaseConfig catalogInfo)
{
var tasklist = new List<Task<WorkspaceDetail>>();
foreach (var workspace in workspaces)
{
tasklist.Add(new Task<WorkspaceDetail>(() =>
this.GrantContributorAccessToUsers(workspace, currentUser, dbGroups, catalogInfo).Result));
// i added a delay here because we encountered an issue before in production and this seems to solve the problem. this is set to 4ms.
Task.Delay(this._config.CurrentValue.DelayInMiliseconds);
}
return tasklist;
}
The other methods called here looks like this:
private async Task<WorkspaceDetail> GrantContributorAccessToUsers(Group workspace, string currentUser, List<Data.ReportCatalogDB.WorkspaceManagement> dbGroups, DatabaseConfig catalogInfo)
{
// This prevents other thread or task to start and prevents exceeding the number of threads allowed
await this._batchProcessor.WaitAsync().ConfigureAwait(false);
var result = new WorkspaceDetail();
try
{
var contributorAccessresult = await this.helper.GrantContributorAccessToUsersAsync(workspace, this._powerBIConfig.CurrentValue.SPNUsers).ConfigureAwait(false);
if (contributorAccessresult != null
&& contributorAccessresult.Count > 0)
{
// do something
}
else
{
// do something
}
// this is done to reuse the call that is being executed in the helper above. it's an expensive call from an external endpoint so we opted to reuse what was used in the initial call, instead of calling it again for this process
var syncWorkspaceAccessToDb = await this.SyncWorkspaceAccessAsync(currentUser, workspace.Id, contributorAccessresult, dbGroups, catalogInfo).ConfigureAwait(false);
foreach (var dbResponse in syncWorkspaceAccessToDb) {
result.ResponseMessage += dbResponse.ResponseMessage;
}
}
catch (Exception ex)
{
this._loghelper.LogEvent(this._logger, logEvent, OperationType.GrantContributorAccessToWorkspaceManager, LogEventStatus.FAIL);
}
finally
{
this._batchProcessor.Release();
}
return result;
}
The last method called writes the record in a database table:
private async Task<List<WorkspaceDetail>> SyncWorkspaceAccessAsync(string currentUser,
Guid workspaceId,
List<GroupUser> groupUsers,
List<WorkspaceManagement> dbGroups,
DatabaseConfig catalogInfo) {
var result = new List<WorkspaceDetail>();
var tasklist = new List<Task<WorkspaceDetail>>();
// get active workspace details from the db
var workspace = dbGroups.Where(x => x.PowerBIGroupId == workspaceId).FirstOrDefault();
try
{
// to auto dispose the provider, we are creating this for each instance because
// having only one instance creates an error when the other task starts running
using (var contextProvider = this._contextFactory.GetReportCatalogProvider(
catalogInfo.Server,
catalogInfo.Database,
catalogInfo.Username,
catalogInfo.Password,
this._dbPolicy))
{
if (workspace != null)
{
// get current group users in the db from the workspace object
var currentDbGroupUsers = workspace.WorkspaceAccess.Where(w => w.Id == workspace.Id
&& w.IsDeleted == false).ToList();
#region identify to process
#region users to add
// identify users to add
var usersToAdd = groupUsers.Where(g => !currentDbGroupUsers.Any(w => w.Id == workspace.Id ))
.Select(g => new WorkspaceAccess
{
// class properties
}).ToList();
#endregion
var addTasks = await this.AddWorkspaceAccessToDbTask(catalogProvider, usersToAdd, workspace.PowerBIGroupId, workspace.WorkspaceName).ConfigureAwait(false);
tasklist.AddRange(addTasks);
// this is a potential fix that i did, hoping adding another parallel thread can solve the problem
Parallel.ForEach(tasklist, new ParallelOptions { MaxDegreeOfParallelism = this._config.CurrentValue.MaxDegreeOfParallelism }, task =>
{
Task.Delay(this._config.CurrentValue.PacingDelay);
task.Start();
});
var processResult = await Task.WhenAll(tasklist).ConfigureAwait(false);
// Populate result
result.AddRange(processResult.ToList());
}
}
}
catch (Exception ex)
{
// handle error
}
return result;
}
I tried some potential solutions already, like the methods here are written with Task.FromResult before instead of async so I changed that. Reference is from this thread:
Using Task.FromResult v/s await in C#
Also, I thought it was a similar issue that we faced before when we are creating multiple db context connections needed when running multiple parallel tasks by adding a small delay on tasks but that didn't solve the problem.
Task.Delay(this._config.CurrentValue.DelayInMiliseconds);
Any help would be much appreciated.
I assume your this._batchProcessor is an instance of SemaphoreSlim. If your other endpoints somehow call
await this._batchProcessor.WaitAsyc()
that means they can't go further until semaphor will be released.
Another thing I'd like to mention: please avoid using Parallel.ForEach with async/await. TPL is not designed to work with async/await, here is good answer why you should avoid using them together: Nesting await in Parallel.ForEach

Calling Async method

I am trying calling the async method SendParkInfo method using the await operator like this
await Task.WhenAny(parkList);and await Task.WhenAny(parkInfo);
parkInfo has SendParkInfo method object
Here is some part of my code.
public async Task<AvailableParksResponse> GetAvailableParks(IEnumerable<string> parkRegionList)
{
//
var parkList = parkRegionList.Select(x => SendParkInfo(x)).ToList();
var parkListTask = await Task.WhenAny(parkList);
response.ParkInfoList = new List<Task<ParkInfo>> { parkListTask };
var parkInfo = SendParkInfo(id);
var parkTask = await Task.WhenAny(parkInfo);
response.ParkInfo = new List<Task<ParkInfo>> { parkTask };
//
}
public virtual async Task<ParkInfo> SendParkInfo(string id)
{
//
var apiResponse = await apiClient.GetAsync(RequestUri + id);
//
}
Is it ok to call SendParkInfo like the way I am calling and then using await operator with Task.WhenAny method. Or is there any better way of calling the Async SendParkInfo method
Any help or suggestion would be appreciated.
Thanks in advance
Task.WhenAny() will return a Task which is considered completed when at least one item in the list of tasks passed into WhenAny() has completed. This usually an appropriate option if you want to return data incrementally, as processing completes.
Alternatively, if your intent is to only return a result when all async tasks have completed, consider Task.WhenAll().

Async\Await methods

I am a new in the Async/Await functionality and I tried to use them in my MVC project.
So from the Controller I call the current method to initialize my model:
var model = this.quantService.GetIndexViewModel(companyIds, isMore, currentrole).Result;
In this GetIndexViewModel I use await:
public async Task<CompanyBoardViewModel> GetIndexViewModel(IEnumerable<int> parameter, bool isMore = false, bool currentRole = false)
{
return new CompanyBoardViewModel
{
TopJoinAplicant = await this.TopJointApplicant(parameter, isMore),
TopPriorityCountry = await this.TopPriorityCountry(parameter),
TopPublicationCountries = await this.TopPublicationCountries(parameter),
TopGrantedInventors = await this.TopGrantedInventors(parameter),
TopIPC = await this.TopIPC(parameter),
TopCPC = await this.TopCPC(parameter),
TopCitedInventors = await this.TopCitedInventors(parameter),
TopCitedPatents = await this.TopCitedPatents(parameter),
CGAR = await this.GetCGAR(parameter),
};
}
For the first method I use these code:
private async Task<QuantTableViewModel<TopFilterViewModel>> TopJointApplicant(IEnumerable<int> ids, bool isMore = false)
{
return await Task.Run(() => new QuantTableViewModel<TopFilterViewModel>
{
Tableid = "TopJointApplicants",
Title = "Top Joint Applicants",
FirstCol = "Position",
SecondCol = "Joint Applicant",
ThirdCol = "#",
IsSeeMore = isMore,
Data = this.cache.TopJointApplicant(ids).ToList()
});
}
In this method I call : Data = this.cache.TopJointApplicant(ids).ToList()
this method created a procedure and get information from the Database(the method is executed without any problems), but when I try to return the QuantTableViewModel<TopFilterViewModel> I stack(as I go in a death log).
I will be really happy if anyone know why this is happened.
I explain the deadlock you're seeing on my blog. In short, don't block on async code; instead, use async all the way.
But there are other problems with your approach. As others have noted, await Task.Run is an antipattern on ASP.NET. You may want to read my article on async ASP.NET.
Finally, one other tip: you're approaching the problem from the wrong direction. Instead of just choosing a method to "make async", you should first think about what your application is doing, and start converting I/O calls to async at the lowest level. Convert them to use async APIs instead of blocking APIs (i.e., no Task.Run). Then change their callers to async, and their callers to async, eventually changing your controller method(s) to async.
You don't need to use Task.Run when using the async/await pattern.
You can actually have an async controller so you don't need to call .Result which renders async operation to run synchronously.
something like:
public Task<ActionResult> Index(object parameter)
{
var model = await this.quantService.GetIndexViewModel(companyIds, isMore, currentRole);
return View(model);
}
In this case I would say that's it's enough that your public method is async, since there's not really any asyncronous going on in TopJointApplicant.
public async Task<CompanyBoardViewModel> GetIndexViewModel(IEnumerable<int> parameter, bool isMore = false, bool currentRole = false)
{
return new CompanyBoardViewModel
{
TopJoinAplicant = this.TopJointApplicant(parameter, isMore),
TopPriorityCountry = await this.TopPriorityCountry(parameter),
TopPublicationCountries = await this.TopPublicationCountries(parameter),
TopGrantedInventors = await this.TopGrantedInventors(parameter),
TopIPC = await this.TopIPC(parameter),
TopCPC = await this.TopCPC(parameter),
TopCitedInventors = await this.TopCitedInventors(parameter),
TopCitedPatents = await this.TopCitedPatents(parameter),
CGAR = await this.GetCGAR(parameter),
};
}
private QuantTableViewModel<TopFilterViewModel> TopJointApplicant(IEnumerable<int> ids, bool isMore = false)
{
var Data = this.cache.TopJointApplicant(ids).ToList();
return new QuantTableViewModel<TopFilterViewModel>
{
Tableid = "TopJointApplicants",
Title = "Top Joint Applicants",
FirstCol = "Position",
SecondCol = "Joint Applicant",
ThirdCol = "#",
IsSeeMore = isMore,
Data = this.cache.TopJointApplicant(ids).ToList()
});
}
I recommend you to fully embrace the await/async-pattern if you're going to use it. This means that your controller also should use await/async.
public class YourController : Controller
{
// Note the Task<ActionResult> and async in your controller.
public async Task<ActionResult> YourControllerMethod()
{
var model = await this.quantService.GetIndexViewModel(companyIds, isMore, currentrole);
return View(model); // Or something like this.
}
}
Also, consider your naming convention. For clarity, async methods should end with the suffix Async, such as GetIndexViewModelAsync.
EDIT:
Based on the comments I think I should clarify what await/async does. An operation will not execute faster simply because you use the await/async-pattern, rather the opposite. Async/await creates an overhead for the thread management which would likely cause your operation to execute slower.
Instead, there are 2 main advantages:
When using async/await you will not block the thread. This means that while you're application is waiting for something else (such as IO, DB or webservice call) the thread can be used for something else, such as executing another we request. This doesn't mean that an DB-call will be executed faster. But it will let the thread do something else while waiting. If you're using IIS the number of threads are limited. So instead of locking them with expensive IO, they can serve another request while waiting.
You may do more things at the same time. For example, you could send a request to you DB, while executing a slow webservice call at the same time. This may cause the total execution time to be faster, since you're doing more things at the same time. However, there are limitations. For instance, if you're using Entity Framework, only one thread may access the context at the time. But while waiting for the DB, you can do something else. For example:
public class MyThreadingClass
{
private Task ExecuteWebServiceCallAsync()
{
return await _myService.DoSomething();
}
private Task ExecuteDbQueryAsync()
{
return await _context.Customer.FirstOrDefaultAsync();
}
public void DoThingsWithWaitAll()
{
var tasks = new Task[2];
// Fire up first task.
tasks[0] = ExecuteWebServiceCallAsync();
// Fire up next task.
tasks[1] = ExecuteDbQueryAsync();
// Wait for all tasks.
Task.WaitAll(tasks);
}
public Task DoThingsWithWithAwaitAsync()
{
// Fire up first task.
var webServiceTask = ExecuteWebServiceCallAsync();
// Fire up next task.
var dbTask = ExecuteDbQueryAsync();
// Wait for all tasks.
await webServiceTask;
await dbTask;
}
}
So, to sum up. The reason why you should use await/async is when you can do it ALL THE WAY down to the execution of the slow operation (such as DB or webservice). Or if you wish to do several things at once.
In your particular case you can do something like this:
public async Task<CompanyBoardViewModel> GetIndexViewModel(IEnumerable<int> parameter, bool isMore = false, bool currentRole = false)
{
// Let the threads start processing.
var topApplicantTask = this.TopJointApplicant(parameter, isMore);
var topPriorityCountryTask = this.TopPriorityCountry(parameter);
var topPublicationContriesTask = this.TopPublicationCountries(parameter);
var topIPCTask = this.TopIPC(parameter);
var topCPCTask = this.TopCPC(parameter);
var topCitedInventorsTask = this.TopCitedInventors(parameter);
var topCitetPatentsTask = this.TopCitedPatents(parameter);
var getCGARTask = this.GetCGAR(parameter);
// Await them later.
return new CompanyBoardViewModel
{
TopJoinAplicant = await topApplicantTask,
TopPriorityCountry = await topPriorityCountryTask,
TopPublicationCountries = await topPublicationContriesTask,
TopGrantedInventors = await this.TopGrantedInventors(parameter),
TopIPC = await topIPCTask,
TopCPC = await topCPCTask,
TopCitedInventors = await topCitedInventorsTask,
TopCitedPatents = await topCitetPatentsTask,
CGAR = await getCGARTask,
};
}
But try to avoid Task.Run since it's considered an anti-pattern. Instead, try to use await/async all the way from the controller to the actual operation (DB, IO, webservice). Also, in the example above there's a lot of threading going on. It should likely be cleaned up a bit, but you can see it as a proof-of-concept more than the suggested solution.

Parallel.ForEach and async-await [duplicate]

This question already has answers here:
Nesting await in Parallel.ForEach [duplicate]
(11 answers)
Closed last year.
I had such method:
public async Task<MyResult> GetResult()
{
MyResult result = new MyResult();
foreach(var method in Methods)
{
string json = await Process(method);
result.Prop1 = PopulateProp1(json);
result.Prop2 = PopulateProp2(json);
}
return result;
}
Then I decided to use Parallel.ForEach:
public async Task<MyResult> GetResult()
{
MyResult result = new MyResult();
Parallel.ForEach(Methods, async method =>
{
string json = await Process(method);
result.Prop1 = PopulateProp1(json);
result.Prop2 = PopulateProp2(json);
});
return result;
}
But now I've got an error:
An asynchronous module or handler completed while an asynchronous operation was still pending.
async doesn't work well with ForEach. In particular, your async lambda is being converted to an async void method. There are a number of reasons to avoid async void (as I describe in an MSDN article); one of them is that you can't easily detect when the async lambda has completed. ASP.NET will see your code return without completing the async void method and (appropriately) throw an exception.
What you probably want to do is process the data concurrently, just not in parallel. Parallel code should almost never be used on ASP.NET. Here's what the code would look like with asynchronous concurrent processing:
public async Task<MyResult> GetResult()
{
MyResult result = new MyResult();
var tasks = Methods.Select(method => ProcessAsync(method)).ToArray();
string[] json = await Task.WhenAll(tasks);
result.Prop1 = PopulateProp1(json[0]);
...
return result;
}
.NET 6 finally added Parallel.ForEachAsync, a way to schedule asynchronous work that allows you to control the degree of parallelism:
var urlsToDownload = new []
{
"https://dotnet.microsoft.com",
"https://www.microsoft.com",
"https://twitter.com/shahabfar"
};
var client = new HttpClient();
var options = new ParallelOptions { MaxDegreeOfParallelism = 2 };
await Parallel.ForEachAsync(urlsToDownload, options, async (url, token) =>
{
var targetPath = Path.Combine(Path.GetTempPath(), "http_cache", url);
var response = await client.GetAsync(url, token);
// The request will be canceled in case of an error in another URL.
if (response.IsSuccessStatusCode)
{
using var target = File.OpenWrite(targetPath);
await response.Content.CopyToAsync(target);
}
});
Alternatively, with the AsyncEnumerator NuGet Package you can do this:
using System.Collections.Async;
public async Task<MyResult> GetResult()
{
MyResult result = new MyResult();
await Methods.ParallelForEachAsync(async method =>
{
string json = await Process(method);
result.Prop1 = PopulateProp1(json);
result.Prop2 = PopulateProp2(json);
}, maxDegreeOfParallelism: 10);
return result;
}
where ParallelForEachAsync is an extension method.
Ahh, okay. I think I know what's going on now. async method => an "async void" which is "fire and forget" (not recommended for anything other than event handlers). This means the caller cannot know when it is completed... So, GetResult returns while the operation is still running. Although the technical details of my first answer are incorrect, the result is the same here: that GetResult is returning while the operations started by ForEach are still running. The only thing you could really do is not await on Process (so that the lambda is no longer async) and wait for Process to complete each iteration. But, that will use at least one thread pool thread to do that and thus stress the pool slightly--likely making use of ForEach pointless. I would simply not use Parallel.ForEach...

Categories

Resources