1
public static async void DoSomething(IEnumerable<IDbContext> dbContexts)
{
    IEnumerator<IDbContext> dbContextEnumerator = dbContexts.GetEnumerator();

    Task<ProjectSchema> projectSchemaTask = Task.Run(() => Core.Data.ProjectRead
        .GetAll(dbContextEnumerator.Current)
        .Where(a => a.PJrecid == pjRecId)
        .Select(b => new ProjectSchema
        {
            PJtextid = b.PJtextid,
            PJcustomerid = b.PJcustomerid,
            PJininvoiceable = b.PJininvoiceable,
            PJselfmanning = b.PJselfmanning,
            PJcategory = b.PJcategory
        })
        .FirstOrDefault());

    Task<int?> defaultActivitySchemeTask = projectSchemaTask.ContinueWith(antecedent =>
    {
        //This is where an exception may get thrown
        return ProjectTypeRead.GetAll(dbContextEnumerator.Current)
            .Where(a => a.PTid == antecedent.Result.PJcategory)
            .Select(a => a.PTactivitySchemeID)
            .FirstOrDefaultAsync().Result;
    }, TaskContinuationOptions.OnlyOnRanToCompletion);

    Task<SomeModel> customerTask = projectSchemaTask.ContinueWith((antecedent) =>
    {
        //This is where an exception may get thrown
        return GetCustomerDataAsync(antecedent.Result.PJcustomerid,
            dbContextEnumerator.Current).Result;
    }, TaskContinuationOptions.OnlyOnRanToCompletion);

    await Task.WhenAll(defaultActivitySchemeTask, customerTask);
}

The exception I am getting:

NotSupportedException: A second operation started on this context before a previous asynchronous operation completed. Use 'await' to ensure that any asynchronous operations have completed before calling another method on this context. Any instance members are not guaranteed to be thread safe.

The exception is only thrown about every 1/20 calls to this function. And the exception seems only to happen when I am chaining tasks with ContinueWith().

How can there be a second operation on context, when I am using a new one for each request?

This is just an example of my code. In the real code I have 3 parent tasks, and each parent has 1-5 chained tasks attached to them.

What am I doing wrong?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • `dbContextEnumerator` never has `MoveNext` called on it so I've no idea what this code is trying to do. I suspect you've abstracted the code in your question too far from your real code. – Damien_The_Unbeliever Apr 01 '20 at 10:45
  • It happens when `dbContexts` is using by 2 or more threads at a same time. Try test with `AddTransient` https://stackoverflow.com/a/48783504/3789481 – Tấn Nguyên Apr 01 '20 at 10:46
  • @Damien_The_Unbeliever Yes you are right, this is because I am bad at copy/pase. In real code I do a MoveNext(). – Simon Sondrup Kristensen Apr 01 '20 at 10:49

2 Answers2

4

yeah, you basically shouldn't use ContinueWith these days; in this case, you are ending up with two continuations on the same task (for defaultActivitySchemeTask and customerTask); how they interact is now basically undefined, and will depend on exactly how the two async flows work, but you could absolutely end up with overlapping async operations here (for example, in the simplest "continuations are sequential", as soon as the first awaits because it is incomplete, the second will start). Frankly, this should be logically sequential await based code, probably not using Task.Run too, but let's keep it for now:

ProjectSchema projectSchema = await Task.Run(() => ...);

int? defaultActivityScheme = await ... first bit
SomeModel customer = await ... second bit

We can't do the two subordinate queries concurrently without risking concurrent async operations on the same context.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • So I can await the 3 parent tasks first, and then await all the children tasks after? – Simon Sondrup Kristensen Apr 01 '20 at 11:02
  • 1
    as long as you're only starting them one after the other, sure (i.e. you only **start** the "next" piece once you've awaited the previous); you can't start multiple flows concurrently without *by definition* hitting concurrency problems – Marc Gravell Apr 01 '20 at 11:06
0

In your example you seem to be running two continuations in parallel, so there is a possibility that they will overlap causing a concurrency problem. DbContext is not thread safe, so you need to make sure that your asynchronous calls are sequential. Keep in mind that using async/await will simply turn your code into a state machine, so you can control which operations has completed before moving to the next operation. Using async methods alone will not ensure parallel operations but wrapping your operation in Task.Run will. So you you need to ask yourself is Task.Run is really required (i.e. is scheduling work in the ThreadPool) to make it parallel.

You mentioned that in your real code you have 3 parent tasks and each parent has 1-5 chained tasks attached to them. If the 3 parent tasks have separate DbContexts, they could run in parallel (each one of them wrapped in Task.Run), but their chained continuations need to be sequential (leveraging async/await keywords). Like that:

        public async Task DoWork()
        {
            var parentTask1 = Task.Run(ParentTask1);
            var parentTask2 = Task.Run(ParentTask2);
            var parentTask3 = Task.Run(ParentTask3);

            await Task.WhenAll(parentTask1 , parentTask2, parentTask3);
        }

        private async Task ParentTask1()
        {
            // chained child asynchronous continuations
            await Task.Delay(100);
            await Task.Delay(100);
        }

        private async Task ParentTask2()
        {
            // chained child asynchronous continuations
            await Task.Delay(100);
            await Task.Delay(100);
        }

        private async Task ParentTask3()
        {
            // chained child asynchronous continuations
            await Task.Delay(100);
            await Task.Delay(100);
        }

If your parent tasks operate on the same DbContext, in order to avoid concurrency you would need to await them one by one (no need to wrap them into Task.Run):

        public async Task DoWork()
        {
            await ParentTask1();
            await ParentTask2();
            await ParentTask3();
        }

rsobon
  • 1,012
  • 2
  • 12
  • 26