Goliath2019
Goliath2019

Reputation: 117

can I update one entity in parallel threads c#

using: Asp.net Core, Entityframework Core, ABP 4.5

I have a user registration and initialization flow. But it takes a long time. I want to parallelize this. This is due to updating from the same entity, but with a different field.

My goal: 1. The endpoint should respond as soon as possible; 2. Long initialization is processed in the background;

Code-before (minor details omitted for brevity)

public async Task<ResponceDto> Rgistration(RegModel input)
{       
    var user = await _userRegistrationManager.RegisterAsync(input.EmailAddress, input.Password, false );
    var result = await _userManager.AddToRoleAsync(user, defaultRoleName);
    user.Code = GenerateCode();
    await SendEmail(user.EmailAddress, user.Code);
    await AddSubEntities(user);
    await AddSubCollectionEntities(user);
    await CurrentUnitOfWork.SaveChangesAsync();
    return user.MapTo<ResponceDto>();
}

private async Task AddSubEntities(User user)
{
    var newSubEntity = new newSubEntity { User = user, UserId = user.Id };
    await _subEntityRepo.InsertAsync(newSubEntity); 
    //few another One-to-One entities...
}

private async Task AddSubEntities(User user)
{
    List<AnotherEntity> collection = GetSomeCollection(user.Type);  
    await _anotherEntitieRepo.GetDbContext().AddRangeAsync(collection); 
    //few another One-to-Many collections...
}

Try change:

public async Task<ResponceDto> Rgistration(RegModel input)
{
    var user = await _userRegistrationManager.RegisterAsync(input.EmailAddress, input.Password, false );

    Task.Run(async () => {
        var result = await _userManager.AddToRoleAsync(user, defaultRoleName);          
    });

    Task.Run(async () => {
        user.Code = GenerateCode();
        await SendEmail(user.EmailAddress, user.Code);  
    });

    Task.Run(async () => {
    using (var unitOfWork = UnitOfWorkManager.Begin())
    {//long operation. defalt unitOfWork out of scope
        try
        {
            await AddSubEntities(user);                            
        }            
        finally
        {                
            unitOfWork.Complete();
        }
    }           
});

Task.Run(async () => {
    using (var unitOfWork = UnitOfWorkManager.Begin())
    {
        try
        {
            await AddSubCollectionEntities(user);                            
        }            
        finally
        {                
            unitOfWork.Complete();
        }
    }           
});
    await CurrentUnitOfWork.SaveChangesAsync();
    return user.MapTo<ResponceDto>();
}

Errors: here I get a lot of different errors related to competition. frequent:

  1. A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext.

  2. In few registratin calls: Cannot insert duplicate key row in object 'XXX' with unique index 'YYY'. The duplicate key value is (70). The statement has been terminated.

I thought on the server every request in its stream, but apparently not.

  1. or all users are successfully registered, but they don’t have some sub-entity in the database. it’s much easier not to register the user than to figure out where he was initialized incorrectly =(

how to keep the user entity “open” for updating and at the same time “closed” for changes initiated by other requests? How to make this code thread safe and fast, can anyone help with advice?

Upvotes: 1

Views: 1393

Answers (2)

Chris Pratt
Chris Pratt

Reputation: 239440

Task.Run is not the same as parallel. It takes a new thread from the pool and runs the work on that thread, and since you're not awaiting it, the rest of the code can move on. However, that's because you're essentially orphaning that thread. When the action returns, all the scoped services will be disposed, which includes things like your context. Any threads that haven't finished, yet, will error out as a result.

The thread pool is a limited resource, and within the context of a web application, it equates directly to the throughput of your server. Every thread you take is one less request you can service. As a result, you're more likely to end up queuing requests, which will only add to processing time. It's virtually never appropriate to use Task.Run in a web environment.

Also, EF Core (or old EF, for that matter) does not support parallelization. So, even without the other problems described above, it will stop you cold from doing what you're trying to do here, regardless.

The queries you have here are not complex. Even if you were trying to insert 100s of things at once, it should still take only milliseconds to complete. If there is any significant delay here, you need to look at the resources of your database server and your network latency, first.

More likely than not, the slow-down is coming from the sending of the email. That too can likely be optimized, though. I was in a situation once where it was taking emails 30 seconds to send, until I finally figured out that it was an issue with our Exchange server, where an IT admin had idiotically introduced a 30 second delay on purpose. Regardless, it is generally always preferable to background things like sending emails, since they aren't core to your app's functionality. However, that means actually processing them in background, i.e. queue them and process them via something like a hosted service or an entirely different worker process.

Upvotes: 3

Johnathan Barclay
Johnathan Barclay

Reputation: 20373

Using Task.Run in ASP.NET is rarely a good idea.

Async methods run on the thread pool anyway, so wrapping them in Task.Run is simply adding overhead without any benefit.

The purpose of using async in ASP.NET is simply to prevent threads being blocked so they are able to serve other HTTP requests.

Ultimately, your database is the bottleneck; if all these operations need to happen before you return a response to the client, then there's not much you can do other than to let them happen.

If it is possible to return early and allow some operations to continue running on the background, then there are details here showing how that can be done.

Upvotes: 4

Related Questions