Подтвердить что ты не робот

Async/ждет другой идентификатор потока

Я недавно читал об async/wait, и я смущен тем, что многие из статей/сообщений, которые я читал, указывают, что новый поток не создается при использовании async wait (Пример).

Я создал простое консольное приложение, чтобы проверить его

class Program
    {
        static  void Main(string[] args)
        {
            Console.WriteLine("Main: " + Thread.CurrentThread.ManagedThreadId);
            MainAsync(args).Wait();
            Console.WriteLine("Main End: " + Thread.CurrentThread.ManagedThreadId);

            Console.ReadKey();
        }


        static async Task MainAsync(string[] args)
        {
            Console.WriteLine("Main Async: " + Thread.CurrentThread.ManagedThreadId);

            await thisIsAsync();
        }

        private static async Task thisIsAsync()
        {
            Console.WriteLine("thisIsAsyncStart: " + Thread.CurrentThread.ManagedThreadId);
            await Task.Delay(1);
            Console.WriteLine("thisIsAsyncEnd: " + Thread.CurrentThread.ManagedThreadId);

        }
    }

Вывод следующего кода:

Main: 8
Main Async: 8
thisIsAsyncStart: 8
thisIsAsyncEnd: 9
Main End: 8

Мне не хватает точки, или thisIsAsyncEnd имеет другой идентификатор потока, чем другие действия?

EDIT:

Я обновил код, как было предложено в ответе ниже, await Task.Delay(1), но я все еще вижу те же результаты.

Цитата из ответа ниже:

Rather, it enables the method to be split into multiple pieces, some of which may run asynchronously

Я хочу знать, где выполняется asynchronously часть, если нет других потоков, созданных? Если он работает в одном потоке, не следует ли его блокировать из-за длительного запроса ввода-вывода или компилятор достаточно умен, чтобы переместить это действие в другой поток, если он занимает слишком много времени, а новый поток используется после всех?

4b9b3361

Ответ 1

Я рекомендую вам прочитать сообщение async intro для понимания ключевых слов async и await. В частности, await (по умолчанию) будет захватывать "контекст" и использовать этот контекст для возобновления его асинхронного метода. Этот "контекст" представляет собой текущий SynchronizationContext (или TaskScheduler, если нет SynchronzationContext).

Я хочу знать, где выполняется асинхронная часть, если нет других потоков? Если он работает в одном потоке, не следует ли его блокировать из-за длительного запроса ввода-вывода или компилятор достаточно умен, чтобы переместить это действие в другой поток, если он занимает слишком много времени, а новый поток используется после всех?

Как я объясняю в своем блоге, действительно асинхронные операции нигде не запускаются. В этом конкретном случае (Task.Delay(1)) асинхронная операция основана на таймере, а не на потоке, заблокированном где-то с помощью Thread.Sleep. Большинство операций ввода-вывода выполняются одинаково. HttpClient.GetAsync, например, основан на перекрытии (асинхронном) вводе-выводе, а не на потоке, заблокированном где-то в ожидании завершения загрузки HTTP.


Как только вы поймете, как await использует свой контекст, проще пройти через исходный код:

static void Main(string[] args)
{
  Console.WriteLine("Main: " + Thread.CurrentThread.ManagedThreadId);
  MainAsync(args).Wait(); // Note: This is the same as "var task = MainAsync(args); task.Wait();"
  Console.WriteLine("Main End: " + Thread.CurrentThread.ManagedThreadId);

  Console.ReadKey();
}

static async Task MainAsync(string[] args)
{
  Console.WriteLine("Main Async: " + Thread.CurrentThread.ManagedThreadId);
  await thisIsAsync(); // Note: This is the same as "var task = thisIsAsync(); await task;"
}

private static async Task thisIsAsync()
{
  Console.WriteLine("thisIsAsyncStart: " + Thread.CurrentThread.ManagedThreadId);
  await Task.Delay(1); // Note: This is the same as "var task = Task.Delay(1); await task;"
  Console.WriteLine("thisIsAsyncEnd: " + Thread.CurrentThread.ManagedThreadId);
}
  • Основной поток запускает выполнение Main и вызывает MainAsync.
  • Основной поток выполняет MainAsync и вызывает thisIsAsync.
  • Основной поток выполняет thisIsAsync и вызывает Task.Delay.
  • Task.Delay делает свою вещь - начиная таймер и еще что-то - и возвращает неполную задачу (обратите внимание, что Task.Delay(0) вернет завершенную задачу, которая изменяет поведение).
  • Основной поток возвращается к thisIsAsync и ожидает задачу, возвращенную из Task.Delay. Поскольку задача неполна, она возвращает неполную задачу из thisIsAsync.
  • Основной поток возвращается к MainAsync и ожидает задачу, возвращенную из thisIsAsync. Поскольку задача неполна, она возвращает неполную задачу из MainAsync.
  • Основной поток возвращается к Main и вызывает Wait в задаче, возвращенной из MainAsync. Это заблокирует основной поток до тех пор, пока MainAsync не завершится.
  • Когда таймер, установленный с помощью Task.Delay, погаснет, thisIsAsync продолжит выполнение. Поскольку нет SynchronizationContext или TaskScheduler, захваченных этим await, он возобновляет выполнение в потоке пула потоков.
  • Поток пула потоков достигает конца thisIsAsync, который завершает свою задачу.
  • MainAsync продолжает выполнение. Поскольку в этом await нет контекста, он возобновляет выполнение в потоке пула потоков (фактически тот же самый поток, который выполнялся thisIsAsync).
  • Поток пула потоков достигает конца MainAsync, который завершает свою задачу.
  • Основной поток возвращается из своего вызова в Wait и продолжает выполнение метода Main. Поток пула потоков, используемый для продолжения thisIsAsync и MainAsync, больше не нужен и возвращается в пул потоков.

Важным выводом здесь является то, что пул потоков используется, потому что нет контекста. Он не используется автоматически при необходимости. Если бы вы запускали один и тот же код MainAsync/thisIsAsync внутри приложения GUI, то вы видели бы очень различное использование потоков: потоки пользовательского интерфейса имеют SynchronizationContext, который планирует продолжения обратно в поток пользовательского интерфейса, поэтому все методы будут возобновите работу в этом же потоке пользовательского интерфейса.

Ответ 2

Создание метода с async не означает, что он создаст другой поток. Если RunTime видит, что ваш метод, который вызывается с помощью await в вашем методе async, задерживается, он выходит из этого метод и ждет после завершения ожидаемых методов, а затем продолжит этот метод с другим потоком. Попробуйте изменить Task.Delay(2000) на Task.Delay(0), и вы увидите, что он не создает новую тему.

RunTime будет считать его, если его нужно создать, он создаст, если нет - not.I попробовал ваш пример с 0 мс и получил все тот же поток:

Main: 1
Main Async: 1
thisIsAsyncStart: 1
thisIsAsyncEnd: 1
Main End: 1

Взято из блога Стивена Тууба:

Ключевое слово "Async"

Что делает ключевое слово "async" при применении к методу?

Когда вы отмечаете метод с ключевым словом "async" , вы действительно говорите в компиляторе две вещи:

  • Вы сообщаете компилятору, что хотите использовать ключевое слово "ожидание" внутри метода (вы можете использовать ключевое слово ожидания, если и только если метод или лямбда-код его помечен как асинхронный). В процессе поэтому вы сообщаете компилятору компилировать метод, используя состояние машины, чтобы метод мог приостановить, а затем возобновить асинхронно в ожидании точек.
  • Вы сообщаете компилятору "поднять" результат метода или любые исключения, которые могут возникнуть в возвращаемом типе. Для метода, который возвращает задачу или задачу, это означает, что любое возвращаемое значение или исключение который не обрабатывается внутри метода, сохраняется в задаче результата. Для метода, который возвращает void, это означает, что любые исключения распространяются в контекст вызывающих "SynchronizationContext" был текущим во время методов первоначальный вызов.

Использует ли использование ключевого слова "async" метода, чтобы все вызовы этого метода были асинхронными?

Нет. Когда вы вызываете метод, помеченный как "async" , он начинает работать синхронно на потоке тока. Итак, если у вас синхронный метод, который возвращает void, и все, что вы делаете для его изменения, отмечаете это как "async" , вызовы этого метода будут выполняться синхронно. Эта истинно независимо от того, оставите ли вы возвращаемый тип "void" или измените его на "Задача". Аналогичным образом, если у вас есть синхронный метод, который возвращает некоторый TResult, и все, что вы делаете, это отметить его как "асинхронный" и изменить тип возврата будет "Задача", вызовы этого метода будут по-прежнему выполняется синхронно.

Маркировка метода как "асинхронная" не влияет на то, работает ли метод завершение синхронно или асинхронно. Скорее, это позволяет метод разбивается на несколько частей, некоторые из которых могут выполняться асинхронно, так что метод может выполняться асинхронно. границы этих частей могут возникать только там, где вы явно кодируете один из которых использует ключевое слово "ожидание" , поэтому, если "ожидание" не используется вообще в методов, будет только одна часть, и поскольку эта часть будет начните синхронно, он (и весь метод с ним) будет полностью синхронно.

Ответ 3

Я точно задал вопрос. Для меня объяснения по MSDN были противоречивыми:

Асинхронные и ожидающие ключевые слова не вызывают дополнительных потоков создано. Асинхронные методы не требуют многопоточности , поскольку асинхронный метод не запускается в своем потоке.

MSDN: асинхронное программирование с асинхронным и ожидающим

Ожидающее выражение не блокирует поток, на котором он выполняется. [..] Когда задача завершается, она вызывает ее продолжение, а выполнение метода async возобновляется там, где оно было остановлено.

wait (С# -Referenz)

Я не понимал, как исходный поток не может быть заблокирован, без использования дополнительных потоков. Кроме того, формулировка "invoke" предполагает, что существует несколько потоков, используемых где-то и как-то.

Но потом я понял, что все написано правильно, что не используются никакие другие потоки по этим ключевым словам. По дизайну класса Task для создания механизмов, которые могут использовать разные потоки - или нет.

В то время как stephen-cleary прекрасно объяснил эти механизмы для метода Task.Delay(), я расширил пример MSDN, чтобы узнать, как await ведет себя с Task.Run():

private async void ds_StartButton_Click(object sender, EventArgs e)
{
    textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Started MSDN Example ..." + Environment.NewLine);

    // Call the method that runs asynchronously.
    string result = await WaitAsynchronouslyAsync();

    // Call the method that runs synchronously.
    //string result = await WaitSynchronously ();

    // Do other Schdaff
    textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #1 ..." + Environment.NewLine);
    textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #2 ..." + Environment.NewLine);
    textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #3 ..." + Environment.NewLine);
    textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #4 ..." + Environment.NewLine);
    textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #5 ..." + Environment.NewLine);
    textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #6 ..." + Environment.NewLine);
    textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #7 ..." + Environment.NewLine);

    // Display the result.
    textBox1.Text += result;
}

// The following method runs asynchronously. The UI thread is not
// blocked during the delay. You can move or resize the Form1 window 
// while Task.Delay is running.
public async Task<string> WaitAsynchronouslyAsync()
{
    Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Entered WaitAsynchronouslyAsync()");
    await Task.Delay(10000);
    Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Task.Delay done, starting random string generation now ...");

    await Task.Run(() => LongComputation());

    Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Leaving WaitAsynchronouslyAsync() ...");
    return DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Finished MSDN Example." + Environment.NewLine;
}

// The following method runs synchronously, despite the use of async.
// You cannot move or resize the Form1 window while Thread.Sleep
// is running because the UI thread is blocked.
public async Task<string> WaitSynchronously()
{
    // Add a using directive for System.Threading.
    Thread.Sleep(10000);
    return DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Finished MSDN Bad Ass Example." + Environment.NewLine;
}

private void ds_ButtonTest_Click(object sender, EventArgs e)
{
    textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Started Test ..." + Environment.NewLine);
    Task<string> l_Task = WaitAsynchronouslyAsync();
    //WaitAsynchronouslyAsync();

    //textBox1.AppendText(l_Result);
}

private void LongComputation()
{
    Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Generating random string ...");

    string l_RandomString = GetRandomString(10000000);

    Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Random string generated.");
}

/// <summary>Get random string with specified length</summary>
/// <param name="p_Length">Requested length of random string</param>
/// <param name="p_NoDots">Use case of this is unknown, but assumed to be importantly needed somewhere. Defaults to true therefore.
/// But due to huge performance implication, added this parameter to switch this off.</param>
/// <returns>Random string</returns>
public static string GetRandomString(int p_Length, bool p_NoDots = true)
{
    StringBuilder l_StringBuilder = new StringBuilder();
    string l_RandomString = string.Empty;

    while (l_StringBuilder.Length <= p_Length)
    {
        l_RandomString = (p_NoDots ? System.IO.Path.GetRandomFileName().Replace(".", string.Empty) : System.IO.Path.GetRandomFileName());
        l_StringBuilder.Append(l_RandomString);
    }

    l_RandomString = l_StringBuilder.ToString(0, p_Length);
    l_StringBuilder = null;

    return l_RandomString;
}

Как видно из вывода, используется несколько потоков - не через async/await, а Task.Run():

04.11.2016 12:38:06 [10] Entered WaitAsynchronouslyAsync()
04.11.2016 12:38:17 [10] Task.Delay done, starting random string generation now ...
04.11.2016 12:38:17 [12] Generating random string ...
04.11.2016 12:38:21 [12] Random string generated.
04.11.2016 12:38:21 [10] Leaving WaitAsynchronouslyAsync() ...

Это так же, как и когда-либо, но мне лично нужен этот явный пример, чтобы понять, что происходит и отделять то, что сделано async/await и что сделано Task.

Ответ 4

Очень хорошее объяснение вашего вопроса здесь https://blogs.msdn.microsoft.com/pfxteam/2012/01/20/await-synchronizationcontext-and-console-apps/

При вызове главного метода консоли приложения, SynchronizationContext.Current вернет значение null. Это означает, что если вы вызываете асинхронный метод в своем консольном приложении, если только не выполняете что-то особенное, ваши асинхронные методы не будут иметь поток сродство: продолжение в рамках этих асинхронных методов могло бы "в любом месте".