Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use dotnet ForEachAsync implementation #15936

Merged
merged 2 commits into from
May 1, 2024
Merged

Use dotnet ForEachAsync implementation #15936

merged 2 commits into from
May 1, 2024

Conversation

sebastienros
Copy link
Member

@sebastienros sebastienros commented May 1, 2024

Summary by CodeRabbit

  • New Features
    • Enhanced extension loading by implementing parallel processing, improving performance.
    • Optimized background services by utilizing parallel task execution for shell operations.

@sebastienros sebastienros requested a review from Piedone May 1, 2024 19:04
Copy link
Contributor

coderabbitai bot commented May 1, 2024

Walkthrough

The recent changes in the OrchardCore project focus on improving the performance by adopting parallel processing techniques. This includes modifications in ExtensionManager.cs to utilize Parallel.ForEach for loading extensions, and in ModularBackgroundService.cs to use Parallel.ForEachAsync for handling shell operations. These enhancements are aimed at optimizing the execution flow and potentially reducing the time required for operations involving multiple tasks.

Changes

File Path Change Summary
.../OrchardCore/Extensions/ExtensionManager.cs Replaced ForEachAsync with Parallel.ForEach for loading extensions.
.../OrchardCore/Modules/ModularBackgroundService.cs Modified RunAsync to use Parallel.ForEachAsync for shell operations.

Assessment against linked issues

Objective Addressed Explanation
#15794: Resolve "SqlException: The transaction operation cannot be performed..." during setup The changes primarily focus on parallel processing and do not address SQL transaction issues directly.

The assessment reveals that while the code changes enhance performance through parallel processing, they do not directly address the SQL transaction issue mentioned in the linked issue #15794. Further investigation or adjustments may be required to specifically tackle the SQL exception problem during setup.


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

Share
Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai generate interesting stats about this repository and render them as a table.
    • @coderabbitai show all the console.log statements in this repository.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (invoked as PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger a review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai help to get help.

Additionally, you can add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.

CodeRabbit Configration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@sebastienros
Copy link
Member Author

@MikeAlhayek can you try this branch? It's fixing the issue for me.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

@MikeAlhayek
Copy link
Member

@sebastienros this is a good change. But it did not fix the issue in reference.

@sebastienros
Copy link
Member Author

@MikeAlhayek 😢

@sebastienros
Copy link
Member Author

sebastienros commented May 1, 2024

Not a nice rabbit.

@MikeAlhayek
Copy link
Member

I did not think it would fix the issue here because the only thing that could impact the setup process is the change in the ExtensionManager. but event that, it should not impact it since none of the calls in that parallel logic use database call.

I tried to explain what I think is happening in the comment here #15794 (comment)

Copy link
Member

@Piedone Piedone left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does fix both issues, awesome!

@@ -298,11 +298,11 @@ private async Task EnsureInitializedAsync()
var loadedExtensions = new ConcurrentDictionary<string, ExtensionEntry>();

// Load all extensions in parallel
await modules.ForEachAsync((module) =>
Parallel.ForEach(modules, (module, cancellationToken) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use Parallel.ForEachAsync() here too?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the delegate is synchronous. No need for asynchronous

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EnsureInitializedAsync() is though so it can make use of async.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but that is done before the parallel call. The parallel call does not need async

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parallel.ForEach() blocks the calling thread, since it's synchronous. So, that thread will just wait, like synchronous IO does. Parallel.ForEachAsync() being async, the calling thread won't be blocked (and can thus be used to e.g. serve as one of the threads used for the loop).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the calling thread won't be blocked (and can thus be used to e.g. serve as one of the threads used for the loop)

But it won't because there is no async code in the parallel lambdas.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What will the thread of EnsureInitializedAsync() do until Parallel.ForEach() returns?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only other thing we could change is that since nothing is asynchronous we can remove the async pattern on this method. And replace the semaphore with a lock. To remove any assumption that this is actually async.

@MikeAlhayek
Copy link
Member

This does fix both issues, awesome!

It does not fix the issue for me. Strange. Let me try cleaning up my environment and test it out again.

@Piedone
Copy link
Member

Piedone commented May 1, 2024

FYI I tried a repro of both issues with a local SQL Server.

@MikeAlhayek
Copy link
Member

MikeAlhayek commented May 1, 2024

@sebastienros @Piedone I just tried it again and it fails every time. I am connecting to a remote service which may be why I am getting the issue since there is higher latency than when running on a local instance.

I basically added a new site on Azure SQL Database and tried to setup the tenant using Blog recipe and it fails.

image

@sebastienros
Copy link
Member Author

It was failing every time for me before this change, then worked twice in a row, maybe I was lucky? Will continue the investigations on my side. We can still merge it since it's less custom code.

@MikeAlhayek
Copy link
Member

It was failing every time for me before this change, then worked twice in a row, maybe I was lucky? Will continue the investigations on my side. We can still merge it since it's less custom code.

Did you try to write to a remote SQL server instead of a local instance? Just so you have higher latency

@MikeAlhayek
Copy link
Member

Oh and don't step through the code. remove all break points and let the code execute naturally otherwise you'll avoid the concurrency

@Piedone
Copy link
Member

Piedone commented May 1, 2024

With Azure SQL the setup fails for me as well, sadly, though differently (but MARS also hints at parallel queries):

System.InvalidOperationException: The connection does not support MultipleActiveResultSets.
   at Microsoft.Data.SqlClient.SqlInternalConnectionTds.ValidateConnectionForExecute(SqlCommand command)
   at Microsoft.Data.SqlClient.SqlInternalTransaction.Rollback()
   at Microsoft.Data.SqlClient.SqlInternalTransaction.Dispose(Boolean disposing)
   at Microsoft.Data.SqlClient.SqlInternalTransaction.Dispose()
   at Microsoft.Data.SqlClient.SqlTransaction.Dispose(Boolean disposing)
   at System.Data.Common.DbTransaction.DisposeAsync()
   at YesSql.Session.ReleaseTransactionAsync()
   at YesSql.Session.ReleaseConnectionAsync()
   at YesSql.Session.CommitOrRollbackTransactionAsync()
   at YesSql.Session.SaveChangesAsync()
   at OrchardCore.Data.Documents.DocumentStore.CommitAsync() in E:\Projects\OrchardForks\OrchardCore5\src\OrchardCore\OrchardCore.Data.YesSql\Documents\DocumentStore.cs:line 121
   at OrchardCore.Environment.Shell.Scope.ShellScope.BeforeDisposeAsync() in E:\Projects\OrchardForks\OrchardCore5\src\OrchardCore\OrchardCore.Abstractions\Shell\Scope\ShellScope.cs:line 397
   at OrchardCore.Environment.Shell.Scope.ShellScope.UsingAsync(Func`2 execute, Boolean activateShell) in E:\Projects\OrchardForks\OrchardCore5\src\OrchardCore\OrchardCore.Abstractions\Shell\Scope\ShellScope.cs:line 267
   at OrchardCore.Environment.Shell.Scope.ShellScope.UsingAsync(Func`2 execute, Boolean activateShell) in E:\Projects\OrchardForks\OrchardCore5\src\OrchardCore\OrchardCore.Abstractions\Shell\Scope\ShellScope.cs:line 269
   at OrchardCore.Recipes.Services.RecipeExecutor.ExecuteStepAsync(RecipeExecutionContext recipeStep) in E:\Projects\OrchardForks\OrchardCore5\src\OrchardCore\OrchardCore.Recipes.Core\Services\RecipeExecutor.cs:line 156
   at OrchardCore.Recipes.Services.RecipeExecutor.ExecuteAsync(String executionId, RecipeDescriptor recipeDescriptor, IDictionary`2 environment, CancellationToken cancellationToken) in E:\Projects\OrchardForks\OrchardCore5\src\OrchardCore\OrchardCore.Recipes.Core\Services\RecipeExecutor.cs:line 99
   at OrchardCore.Recipes.Services.RecipeExecutor.ExecuteAsync(String executionId, RecipeDescriptor recipeDescriptor, IDictionary`2 environment, CancellationToken cancellationToken) in E:\Projects\OrchardForks\OrchardCore5\src\OrchardCore\OrchardCore.Recipes.Core\Services\RecipeExecutor.cs:line 117
   at OrchardCore.Recipes.Services.RecipeExecutor.ExecuteAsync(String executionId, RecipeDescriptor recipeDescriptor, IDictionary`2 environment, CancellationToken cancellationToken) in E:\Projects\OrchardForks\OrchardCore5\src\OrchardCore\OrchardCore.Recipes.Core\Services\RecipeExecutor.cs:line 132
   at OrchardCore.Recipes.Services.RecipeExecutor.ExecuteAsync(String executionId, RecipeDescriptor recipeDescriptor, IDictionary`2 environment, CancellationToken cancellationToken) in E:\Projects\OrchardForks\OrchardCore5\src\OrchardCore\OrchardCore.Recipes.Core\Services\RecipeExecutor.cs:line 142
   at OrchardCore.Setup.Services.SetupService.SetupInternalAsync(SetupContext context) in E:\Projects\OrchardForks\OrchardCore5\src\OrchardCore\OrchardCore.Setup.Core\SetupService.cs:line 231
   at OrchardCore.Setup.Services.SetupService.SetupInternalAsync(SetupContext context) in E:\Projects\OrchardForks\OrchardCore5\src\OrchardCore\OrchardCore.Setup.Core\SetupService.cs:line 232
   at OrchardCore.Setup.Services.SetupService.SetupAsync(SetupContext context) in E:\Projects\OrchardForks\OrchardCore5\src\OrchardCore\OrchardCore.Setup.Core\SetupService.cs:line 95
   at OrchardCore.Setup.Services.SetupService.SetupAsync(SetupContext context) in E:\Projects\OrchardForks\OrchardCore5\src\OrchardCore\OrchardCore.Setup.Core\SetupService.cs:line 110
   at OrchardCore.Setup.Controllers.SetupController.IndexPOST(SetupViewModel model) in E:\Projects\OrchardForks\OrchardCore5\src\OrchardCore.Modules\OrchardCore.Setup\Controllers\SetupController.cs:line 176
   at lambda_method76(Closure, Object)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfActionResultExecutor.Execute(ActionContext actionContext, IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
   at Microsoft.AspNetCore.Localization.RequestLocalizationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at OrchardCore.Modules.ModularTenantRouterMiddleware.Invoke(HttpContext httpContext) in E:\Projects\OrchardForks\OrchardCore5\src\OrchardCore\OrchardCore\Modules\ModularTenantRouterMiddleware.cs:line 47
   at OrchardCore.Modules.ModularTenantContainerMiddleware.<>c__DisplayClass4_0.<<Invoke>b__0>d.MoveNext() in E:\Projects\OrchardForks\OrchardCore5\src\OrchardCore\OrchardCore\Modules\ModularTenantContainerMiddleware.cs:line 61
--- End of stack trace from previous location ---
   at OrchardCore.Environment.Shell.Scope.ShellScope.UsingAsync(Func`2 execute, Boolean activateShell) in E:\Projects\OrchardForks\OrchardCore5\src\OrchardCore\OrchardCore.Abstractions\Shell\Scope\ShellScope.cs:line 253
   at OrchardCore.Environment.Shell.Scope.ShellScope.UsingAsync(Func`2 execute, Boolean activateShell) in E:\Projects\OrchardForks\OrchardCore5\src\OrchardCore\OrchardCore.Abstractions\Shell\Scope\ShellScope.cs:line 258
   at OrchardCore.Environment.Shell.Scope.ShellScope.UsingAsync(Func`2 execute, Boolean activateShell) in E:\Projects\OrchardForks\OrchardCore5\src\OrchardCore\OrchardCore.Abstractions\Shell\Scope\ShellScope.cs:line 263
   at OrchardCore.Environment.Shell.Scope.ShellScope.UsingAsync(Func`2 execute, Boolean activateShell) in E:\Projects\OrchardForks\OrchardCore5\src\OrchardCore\OrchardCore.Abstractions\Shell\Scope\ShellScope.cs:line 268
   at OrchardCore.Environment.Shell.Scope.ShellScope.UsingAsync(Func`2 execute, Boolean activateShell) in E:\Projects\OrchardForks\OrchardCore5\src\OrchardCore\OrchardCore.Abstractions\Shell\Scope\ShellScope.cs:line 269
   at OrchardCore.Modules.ModularTenantContainerMiddleware.Invoke(HttpContext httpContext) in E:\Projects\OrchardForks\OrchardCore5\src\OrchardCore\OrchardCore\Modules\ModularTenantContainerMiddleware.cs:line 59
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

BTW neither previously nor now did I have breakpoints or anything that would inhibit the normal code flow, just running with Ctrl+F5.

@MikeAlhayek
Copy link
Member

BTW neither previously nor now did I have breakpoints or anything that would inhibit the normal code flow, just running with Ctrl+F5.

Yes I think the latency is the key here. If you connect locally "very low latency" and it would work. But when you connect to a remote Azure SQL Server which has higher latency, you encounter the issue.

Try to remove RolesStep from the IoC and try it.

@MikeAlhayek
Copy link
Member

Will continue the investigations on my side. We can still merge it since it's less custom code.

we should merge this as it is a good change. But please remove the referenced 2 issues since it does not really fix them.

@MikeAlhayek
Copy link
Member

@sebastienros here are the last few trace logs from my logfile just before we countered the issue "maybe the queries could give you more clues on what could be updating"

024-05-01 15:21:34.2271|ocblog29|00-f43e7e43218ecd49cc90714d6dea92f5-3b1e9d38f7a50749-00||OrchardCore.Recipes.Services.RecipeExecutor|DEBUG|Executing the step themes from blog.recipe.json was successful. 
2024-05-01 15:21:34.2271|ocblog29|00-f43e7e43218ecd49cc90714d6dea92f5-3b1e9d38f7a50749-00||OrchardCore.Recipes.Services.RecipeExecutor|DEBUG|Executing the step Roles from blog.recipe.json 
2024-05-01 15:21:34.2271|ocblog29|00-f43e7e43218ecd49cc90714d6dea92f5-3b1e9d38f7a50749-00||OrchardCore.Recipes.Services.RecipeExecutor|DEBUG|Executing Recipe using a shellScope. RequireNewScope; True 
2024-05-01 15:21:34.2692|ocblog29|00-f43e7e43218ecd49cc90714d6dea92f5-3b1e9d38f7a50749-00||OrchardCore.Recipes.Services.RecipeExecutor|INFO|Executing recipe step 'Roles'. 
2024-05-01 15:21:34.2722|ocblog29|00-f43e7e43218ecd49cc90714d6dea92f5-3b1e9d38f7a50749-00||YesSql|DEBUG|SELECT TOP (1) [ocblog29_Document].* FROM [ocblog29_Document] WHERE [ocblog29_Document].[Type] = @Type 
2024-05-01 15:21:34.3597|ocblog29|00-f43e7e43218ecd49cc90714d6dea92f5-3b1e9d38f7a50749-00||YesSql|DEBUG|SELECT TOP (1) [ocblog29_Document].* FROM [ocblog29_Document] WHERE [ocblog29_Document].[Type] = @Type 
2024-05-01 15:21:34.4523|ocblog29|00-f43e7e43218ecd49cc90714d6dea92f5-3b1e9d38f7a50749-00||YesSql|DEBUG|SELECT TOP (1) [ocblog29_Document].* FROM [ocblog29_Document] WHERE [ocblog29_Document].[Type] = @Type 
2024-05-01 15:21:34.6370|ocblog29|00-f43e7e43218ecd49cc90714d6dea92f5-3b1e9d38f7a50749-00||YesSql|TRACE|insert into [ocblog29_Document] ([Id], [Type], [Content], [Version]) values (@Id_1, @Type_1, @Content_1, @Version_1); 
2024-05-01 15:21:34.6370|ocblog29|00-f43e7e43218ecd49cc90714d6dea92f5-3b1e9d38f7a50749-00||OrchardCore.Recipes.Services.RecipeExecutor|INFO|Finished executing recipe step 'Roles'. 
2024-05-01 15:21:34.7262|ocblog29|00-f43e7e43218ecd49cc90714d6dea92f5-3b1e9d38f7a50749-00||YesSql|DEBUG|SELECT TOP (1) [ocblog29_Document].* FROM [ocblog29_Document] WHERE [ocblog29_Document].[Type] = @Type 
2024-05-01 15:21:35.5918|ocblog29|00-f43e7e43218ecd49cc90714d6dea92f5-3b1e9d38f7a50749-00||YesSql|TRACE|update [ocblog29_Document] set [Content] = @Content_1, [Version] = @Version_1 where [Id] = @Id_1; 
2024-05-01 15:21:35.7158|ocblog29|00-f43e7e43218ecd49cc90714d6dea92f5-3b1e9d38f7a50749-00||YesSql|DEBUG|SELECT TOP (1) [ocblog29_Document].* FROM [ocblog29_Document] WHERE [ocblog29_Document].[Type] = @Type 
2024-05-01 15:21:41.0712|ocblog29|00-f43e7e43218ecd49cc90714d6dea92f5-3b1e9d38f7a50749-00||OrchardCore.Recipes.Services.RecipeExecutor|DEBUG|Executing the step Roles from blog.recipe.json was unsuccessful. Error: Microsoft.Data.SqlClient.SqlException (0x80131904): The transaction operation cannot be performed because there are pending requests working on this transaction.

@Piedone
Copy link
Member

Piedone commented May 1, 2024

Without RolesStep it works indeed. I'm OK with this, once we have #15936 (comment) settled.

@sebastienros sebastienros merged commit 2b8ebf7 into main May 1, 2024
6 checks passed
@sebastienros sebastienros deleted the sebros/concurrent branch May 1, 2024 22:36
@Piedone
Copy link
Member

Piedone commented May 1, 2024

@sebastienros why did you merge?

@sebastienros
Copy link
Member Author

Got an approval, and your comment is not correct.

@Piedone
Copy link
Member

Piedone commented May 1, 2024

You asked for my review too, and we were in a conversation, what I was quite clear about I'd like to finish before the merge. So I feel sidestepped that you merged it despite that. It's no problem if we disagree, but if I put an effort into reviewing your PR, as you asked, then I'd expect you to engage with me as well, even if I ultimately prove to be wrong. I'm happy to learn. The goal is to have good code, what also comes from discussions like this. If not by changes to the code in this PR, then the reviewer learning something and doing it better in the next one.

So, can you then please tell what will the thread of EnsureInitializedAsync() do until Parallel.ForEach() returns if not be blocked? Because it looks to me that now we have synchronous code in an async method that blocks, while we could also have it fully async.

@sebastienros
Copy link
Member Author

Sorry, I had not seen this part:

I'm OK with this, once we have #15936 (comment) settled.

With both solutions the caller thread will block until the parallel operation is done because the body in the parallel operation is not async, it will never yield.

@Piedone
Copy link
Member

Piedone commented May 1, 2024

Why does it block with Parallel.ForeachAsync()? I know that it does a lot-lot more and differently, but it seems to me that it's roughly analogous to:

var tasks = new List<Task>();

foreach (var module in modules)
{
    tasks.Add(Task.Run(...));
}

await Task.WhenAll(tasks);

So, while the body lambda is executed with a given module on multiple threads in parallel, the caller is not blocked but waiting asynchronously.

@sebastienros
Copy link
Member Author

I don't think it's using Task.WhenAll at all. ThreadPool + TaskCompletionSource to detect all threads are done. Where is the documentation about ForEachAsync that would tell if it should always be used even when there is no async body?

@Piedone
Copy link
Member

Piedone commented May 2, 2024

It doesn't use Task.WhenAll, what I mean by it being roughly analogous is that it's in behavior similar to the above code. I.e. it runs the body in parallel and completes when all parallel bodies are completed.

I'm not saying Parallel.ForeachAsync() should always be used when there's no async body. Rather, that it's more suitable, regardless of the body, in this case, because the consumer is async. The body returning ValueTask and not Task BTW hints on the authors expecting a large number of body implementations to be sync.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants