C # Async без создания нового потока - PullRequest
0 голосов
/ 27 сентября 2019

Я видел много постов, объясняющих, что async / await в C # не создает новый поток, подобный этому: задачи все еще не являются потоками, а async не параллелен .Я хотел проверить это сам, поэтому я написал этот код:

private static async Task Run(int id)
{
    Console.WriteLine("Start:\t" + id + "\t" + System.Threading.Thread.CurrentThread.ManagedThreadId);

    System.Threading.Thread.Sleep(500);

    Console.WriteLine("Delay:\t" + id + "\t" + System.Threading.Thread.CurrentThread.ManagedThreadId);

    await Task.Delay(100);

    Console.WriteLine("Resume:\t" + id + "\t" + System.Threading.Thread.CurrentThread.ManagedThreadId);

    System.Threading.Thread.Sleep(500);

    Console.WriteLine("Exit:\t" + id + "\t" + System.Threading.Thread.CurrentThread.ManagedThreadId);
}

private static async Task Main(string[] args)
{
    Console.WriteLine("Action\tid\tthread");

    var task1 = Run(1);
    var task2 = Run(2);

    await Task.WhenAll(task1, task2);
}

Удивительно, но в итоге я получил такой вывод:

Action  id      thread
Start:  1       1
Delay:  1       1
Start:  2       1
Resume: 1       4  < ------ problem here
Delay:  2       1
Exit:   1       4
Resume: 2       5
Exit:   2       5

Из того, что я вижу, этодействительно создавать новые потоки и даже разрешать одновременную работу двух частей кода?Мне нужно использовать async / await в не поточной среде, поэтому я не могу позволить ему создавать новые потоки.Почему задача «1» может быть возобновлена ​​(после Task.Delay), когда задача «2» выполняется в данный момент?

Я пытался добавить ConfigureAwait(true) ко всем await, но это неничего не менять.

Спасибо!

Ответы [ 4 ]

3 голосов
/ 27 сентября 2019

По первому вопросу - async / await не создает потоки самостоятельно, что сильно отличается от того, что вы ожидаете от этого утверждения - потоки никогда не создаются, когда код использует async / await.

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

Второй момент - как обеспечить доступ к объектам, не защищенным от потоков, из других потоков: вам действительно нужно знать, какая операция может инициировать выполнение кода в других потоках.Т.е. System.Timers.Timer будет делать это, например, даже если это не похоже на создание потоков.Использование async / await в среде, которая гарантирует, что выполнение будет продолжаться в том же (WinForms / WPF) или хотя бы по одному (ASP.Net) потоке, может помочь вам в этом.

Если вам нужны более надежные гарантиивы можете следовать подходу WinForms / WPF, который обеспечивает принудительный доступ к методам в одном потоке (известный как основной поток пользовательского интерфейса), и предоставить руководство по использованию async / await с контекстом синхронизации, обеспечивающим продолжение выполнения в том же потоке, в котором он был await-ed (а также обеспечивает Invoke функциональность напрямую для других, чтобы сами управлять потоками).Скорее всего, это потребует обертывания классов, которые вам не принадлежат, с некоторыми прокси-серверами «принудительного исполнения потоков».

2 голосов
/ 27 сентября 2019

После просмотра ответа Alexei Levenkov и чтения этого похоже, что он должен работать правильно, если я не запускал свои тесты в win32 console app.Если я перенесу этот код в WinForm app и вызову его внутри события нажатия кнопки, он будет работать как задумано, дополнительный поток не будет создан / использован.Я также нашел эту библиотеку, которая исправила проблему для консольного приложения: Nito.AsyncEx.Использование Nito.AsyncEx.AsyncContext.Run создало искомое поведение и дало следующие результаты:

Action  id      thread
Start:  1       1
Delay:  1       1
Start:  2       1
Delay:  2       1
Resume: 1       1
Exit:   1       1
Resume: 2       1
Exit:   2       1

Таким образом, общее время меньше, чем выполнение задач 1 и 2 одна за другой, но все еще не требуетчтобы я стал потокобезопасным.

1 голос
/ 28 сентября 2019

Async-await использует потоки, и если случится так, что все потоки пула потоков будут заняты в то время, то создаются новые потоки.Поэтому говорить, что async-await не создает потоков, не совсем точно.Разница в том, как эти потоки обычно используются.В классическом многопоточном сценарии вы создаете новые потоки с намерением поддерживать их в течение некоторого времени.Вы не запускаете новый поток для выполнения рабочей нагрузки в 1 миллисекунду.Обычно вы выполняете вычисления с интенсивным использованием ЦП, либо читаете с диска большие файлы, либо вызываете какой-либо веб-метод, для ответа на который может потребоваться некоторое время.В некоторых из этих примеров потоки фактически не выполняют большую работу, они просто ждут большую часть времени, чтобы получить данные с диска или сетевых драйверов.И пока они ждут, они потребляют довольно большой кусок памяти, около 1 МБ каждого потока, просто для собственного существования.

Войдите в мир асинхронного ожидания, где очень малопотоки могут тратить ресурсы, будучи простаивающими.Это потоки общего пула потоков, готовые отвечать на события от драйверов, от часов компьютера или откуда-либо еще.Обычно каждая отдельная рабочая нагрузка потока пула потоков довольно мала.Это может быть одна миллисекунда или даже меньше.Инфраструктура .NET впечатляет своей способностью обрабатывать огромное количество запланированных задач:

var stopwatch = Stopwatch.StartNew();
var tasks = new List<Task>();
int sum = 0;
foreach (var i in Enumerable.Range(0, 100_000))
{
    tasks.Add(Task.Run(async () =>
    {
        await Task.Delay(i % 1000);
        Interlocked.Increment(ref sum);
    }));
}
Console.WriteLine($"Waiting, Elapsed: {stopwatch.ElapsedMilliseconds} msec");
await Task.WhenAll(tasks);
Console.WriteLine($"Sum: {sum}, Elapsed: {stopwatch.ElapsedMilliseconds} msec");

Ожидание, истекло: 296 мсек
Сумма: 100000, истекло: 1898 мсек

Это с .NET Framework 4.8..NET Core 3.0 работает еще лучше.

Чем больше вы работаете с задачами и async-await, тем больше вы доверяете инфраструктуре и даете ей все более и более детальную работу.Например, чтение строк из текстового файла.Вы бы сошли с ума, чтобы начать поток, просто чтобы прочитать одну строку из файла, но с задачами и async-await это совсем не сумасшествие: StreamReader.ReadLineAsync.

0 голосов
/ 27 сентября 2019

Вы пытаетесь использовать асинхронную работу, но в конце вы хотите работать как синхронная задача, даже если вы используете режим сна или задержку, вы не уверены, что задача к этому времени завершится.

Если вы не хотите использовать хорошие вещи, которые дает Asynchronous, используйте стандартный способ или: Поместите все задачи в метод run или (создайте метод и вызовите его из run несколько раз);используйте 1 поток, чтобы он не блокировал ваш компьютер, и все будет выполняться в 1 поток.

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