Giter Club home page Giter Club logo

Comments (8)

giammin avatar giammin commented on August 16, 2024

Thread.Sleep(5000); is blocking the caller

in async await you should use Task.Delay(5000);

this works as expected

        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
            BadTask().SafeFireAndForget();
            Console.WriteLine("Hello World!");
            Console.ReadLine();
        }
        static async Task BadTask()
        {
            await Task.Delay(5000);
            Console.WriteLine("Hello World!");
        }

from asyncawaitbestpractices.

brminnick avatar brminnick commented on August 16, 2024

I agree that this is true, however, I'm not sure how to fix it. It may just be a limitation of .NET.

Expanding on your example, even if we use async/await to call await BadTask(), it will still run synchronously and Thread.Sleep will still block the thread. (I.e. In the provided example, SafeFireAndForget has the same behavior as await):

static Task BadTask()
{
    Thread.Sleep(5000);
    return Task.CompletedTask;
}

static async Task Main(string[] args)
{
    await BadTask();
    Console.WriteLine("Hello World!");
}

from asyncawaitbestpractices.

brminnick avatar brminnick commented on August 16, 2024

If anyone has ideas on how to best resolve this Issue, please do share them!

If it turns out that we cannot resolve this issue, I'll likely convert this to a Discussion so that we can continue the conversation.

from asyncawaitbestpractices.

Szer avatar Szer commented on August 16, 2024

@brminnick adding Task.Yield() at the start will push everything below to thread pool

 static async void HandleSafeFireAndForget<TException>(ValueTask valueTask, bool continueOnCapturedContext, Action<TException>? onException) where TException : Exception 
 { 
     try 
     { 
         await Task.Yield();
         await valueTask.ConfigureAwait(continueOnCapturedContext); 
     } 
     catch (TException ex) when (_onException != null || onException != null) 
     { 
         HandleException(ex, onException); 
  
         if (_shouldAlwaysRethrowException) 
             throw; 
     } 
 } 

from asyncawaitbestpractices.

brminnick avatar brminnick commented on August 16, 2024

I had the same thought, and tried adding await Task.Yeild() last night, but because of the way .NET handles Tasks, the Task is run synchronously until the first await keyword is reached.

This means that HandleSafeFireAndForget doesn't execute until the Task is executed. In other words, HandleSafeFireAndForget executes when the first await is reached.

For example, in the following code, Thread.Sleep executes synchronously, before await Task.Yield(); is reached in HandleSafeFireAndForget:

Task.Run(() => 
{
    Thread.Sleep(5000);
    await Task.Delay(1000); // This is when HandleSafeFireAndForget executes
}.SafeFireAndForget();

Here's another example, using a Unit Test:

Expected: not equal to 12
But was: 12

[Test]
public async Task SafeFireAndForget_NonAsyncMethodThreadTest()
{
    //Arrange
    Thread initialThread, workingThread, finalThread;
    var threadTCS = new TaskCompletionSource<Thread>();

    //Act
    initialThread = Thread.CurrentThread;

    NonAsyncMethod().SafeFireAndForget();

    finalThread = Thread.CurrentThread;

    workingThread = await threadTCS.Task;

    //Assert
    Assert.IsNotNull(initialThread);
    Assert.IsNotNull(workingThread);
    Assert.IsNotNull(finalThread);

    Assert.AreEqual(initialThread.ManagedThreadId, finalThread.ManagedThreadId);
    Assert.AreNotEqual(initialThread.ManagedThreadId, workingThread.ManagedThreadId);
    Assert.AreNotEqual(finalThread.ManagedThreadId, workingThread.ManagedThreadId);

    async Task NonAsyncMethod()
    {
        threadTCS.SetResult(Thread.CurrentThread);
        await Task.Delay(1000);
    }
}

from asyncawaitbestpractices.

Szer avatar Szer commented on August 16, 2024

In current design it's not possible without API break, because with any call like this one:
SomeTask().SafeFireAndForget();
We will always start SomeTask first and it will execute whatever it can inside that task synchronously on the caller thread, and only after it will reach continuation (first await) and return Task object it will be passed to SafeFireAndForget extension method.

The only way to fix eagerness of C# Tasks is to wrap them in thunk like

SafeFireAndForget(() => SomeTask);

This will introduce breaking change in API (Func<ValueTask> valueTask instead of ValueTask valueTask)

 static async void HandleSafeFireAndForget<TException>(Func<ValueTask> valueTask, bool continueOnCapturedContext, Action<TException>? onException) where TException : Exception 
 { 
     try 
     { 
         await Task.Yield();
         await valueTask().ConfigureAwait(continueOnCapturedContext); 
     } 
     catch (TException ex) when (_onException != null || onException != null) 
     { 
         HandleException(ex, onException); 
  
         if (_shouldAlwaysRethrowException) 
             throw; 
     } 
 } 

That way it will definitely execute Task.Yield or Task.Run first, jump to thread pool and then proceed safely.

It's not my call to make, but maybe just add "even more safer" API? :)

from asyncawaitbestpractices.

SIDOVSKY avatar SIDOVSKY commented on August 16, 2024

I thought that the purpose of this API is to provide an exception-safe and explicit way to forget about async (not background/parallel) task execution and nothing more.

Fire-and-forget is an alternative to await and should not be safe against thread-blocking because it's a responsibility of a task itself.

Modifying existing logic with thread switching under the hood will break the compatibility for applications that fire-and-forget tasks that interact with the UI thread.

Adding SafeFireAndForgetOnBackground may be a solution because it clearly indicates the threading aspect.
However, I dont think it's necessary because users which want to additionally switch the thread can just:

Task.Run(BadTask).SafeFireAndForget();

from asyncawaitbestpractices.

Szer avatar Szer commented on August 16, 2024

I thought that the purpose of this API is to provide an exception-safe and explicit way to forget about async (not background/parallel) task execution and nothing more.

That's why I've mentioned what's written in README.md:

SafeFireAndForget allows a Task to safely run on a different thread while the calling thread does not wait for its completion.

It's just not true :)

from asyncawaitbestpractices.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.