1

Есть код:

public async void Method1()
{
    var t2 = Method2();       // thread 1
    var t3 = await Method3(); // async Thread 2
    var t3 = await Method4(); // async Thread 2 or 3 ???
}

Где будет выполнен Method4 и как его заставить выполнятся в потоке 1 или потоке 2?

aepot
  • 49,560
Hel
  • 21
  • 1
  • Ну гарантировано такое в однопоточном приложении, думаю. – Aziz Umarov Apr 06 '21 at 13:46
  • 2
    Если ответ был полезен, не забудьте принять его, поставив галочку, расположенную слева от ответа. Так же будет неплохо, если вы примете наиболее полезный ответ в своем прошлом вопросе. – aepot Apr 06 '21 at 16:55
  • @aepot - Есть я. Я прихожу в вопрос, любуюсь Вашим ответом. я "плюсик" за ответ уже поставил. :-) – S.H. Apr 08 '21 at 19:53
  • @S.H. автор заходил, но 0 эмоций. Что-то больше не хочется отвечать незарегистрированным участникам. – aepot Apr 08 '21 at 19:55
  • 1
    @aepot - вообще, я Вас понимаю, иногда столько сил тратишь, а в ответ - тишина... Но это скорее от непонимания системы сайта и того, как устроен рейтинг. Мы то здесь сравнительно давно сидим, а новому человеку сразу въехать не получается – S.H. Apr 08 '21 at 19:58
  • Если ответ был полезен, вы можете принять его поставив зеленую галочку слева от ответа. – aepot Dec 03 '22 at 21:47

1 Answers1

6

Если вы используете WinForms или WPF, то там и так присутствует контекст синхронизации. Ничего дополнительно делать не нужно, просто запускайте, и все методы будут выполнены в одном потоке, асинхронно.

В консольных же приложениях контекста синхронизации по умолчанию нет, и код после await продолжается в том потоке, в котором вернулся колбэк из вызванного с ожиданием метода. Есть хорошая статья в девблоге Await, SynchronizationContext, and Console Apps, в которой рассказано как реализовать однопоточный контекст синхронизации. Я просто создал пример на основе кода из этой статьи.

class Program
{
    static async Task Main() 
    {
        Console.WriteLine("Результаты в формате: [номер потока, количество запусков]");
        Console.WriteLine("Запуск на текущем потоке");
        RunOnSingleThread(TestContextAsync);
    Thread thread = new Thread(() => RunOnSingleThread(TestContextAsync));
    Console.WriteLine($"Запуск на потоке номер {thread.ManagedThreadId}");
    thread.IsBackground = true;
    thread.Start();
    thread.Join();

    Console.WriteLine("Обычный запуск");
    await TestContextAsync();
}

static void RunOnSingleThread(Func<Task> method)
{
    var context = new SingleThreadSynchronizationContext();
    SynchronizationContext.SetSynchronizationContext(context);
    Task task = method();
    task.ContinueWith(_ => context.Complete());
    context.RunOnCurrentThread();
    SynchronizationContext.SetSynchronizationContext(null);
    task.GetAwaiter().GetResult(); // таск уже гарантированно завершен, этот вызов нужен чтобы выдернуть из него исключение, если возникло.
}

static async Task TestContextAsync()
{
    Dictionary<int, int> dict = new Dictionary<int, int>();

    for (int i = 0; i < 1000; i++)
    {
        int id = Thread.CurrentThread.ManagedThreadId;
        dict[id] = dict.TryGetValue(id, out int count) ? count + 1 : 1;
        await Task.Yield();
    }

    foreach (var pair in dict)
        Console.WriteLine(pair);
}

}

public sealed class SingleThreadSynchronizationContext : SynchronizationContext { private readonly BlockingCollection<(SendOrPostCallback, object)> _queue = new BlockingCollection<(SendOrPostCallback, object)>();

public override void Post(SendOrPostCallback callback, object state)
{
    _queue.Add((callback, state));
}

public void RunOnCurrentThread()
{
    foreach ((SendOrPostCallback callback, object state) in _queue.GetConsumingEnumerable())
        callback(state);
}

public void Complete() 
{ 
    _queue.CompleteAdding(); 
}

}

Вывод в консоль

Результаты в формате: [номер потока, количество запусков]
Запуск на текущем потоке
[1, 1000]
Запуск на потоке номер 6
[6, 1000]
Обычный запуск
[1, 1]
[4, 235]
[5, 10]
[8, 14]
[7, 4]
[10, 11]
[9, 173]
[12, 272]
[11, 280]

Чтобы воспользваться этим кодом, вам нужно только изменить сигнатуру своего метода

public async Task Method1()

В противном случае всё сломается, так как работа контекста синхронизации будет завершена раньше, чем работа метода. Избегайте async void в методах - это плохая практика.

aepot
  • 49,560
  • 1
    Уважаемый Aepot, мои самые тёпленькие слова восхищения! Только я хотел на автора вопроса поругаться - а тут Ваш ответ, и такой шикарный! – S.H. Apr 06 '21 at 17:04