Асинхронный шаблон — ожидание события перед возвратом некоторого значения из метода

[Отказ от ответственности - этот код упрощен (намного) для легкого чтения, и я знаю, что он не соответствует обычным стандартам кода]

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

Вопрос в том, какой шаблон предпочтительнее для подобных ситуаций (конечно, фактическое решение было бы очень кстати).

Я экспериментировал с разными вещами вокруг TaskCompletionSource и т. д., но боюсь, что мое понимание отстает от того, чтобы найти (желательно) элегантное решение. Надеюсь, вы можете помочь.

public class AsyncEventTest
{
    // This is performed one a single (UI) thread. The exception to this is
    // a.B that might - at the calling time - get a asycronious update from the backend.
    // The update is syncronized into the calling context so Task.Wait etc. will effectivly
    // deadlock the flow.
    public static string CallMe(A a)
    {
        if (a.B.State != State.Ready)
        {
            // wait for a.B.State == State.Ready ... but how ...
            // await MagicMethod() ???;
        }

        // only execute this code after a.b.State == State.Ready
        return a.B.Text;
    }
}

public class A
{
    public B B { get; set; }
}

public class B
{
    public State State { get; private set; }
    public event Action StateChanged;
    public string Text { get; }
}

public enum State { Ready, Working, }

РЕДАКТИРОВАТЬ - пример того, что я пробовал. Интересно, является ли что-то подобное приемлемым подходом (или даже работает ли он)?

public class AsyncEventTest2
{
    public static string CallMe(A a)
    {
        return CallMe1(a).Result;
    }

    public async static Task<string> CallMe1(A a)
    {
        await CallMe2(a);
        return a.B.Text;
    }

    public static Task CallMe2(A a)
    {
        TaskCompletionSource<string> tcs = new TaskCompletionSource<string>();
        if (a.B.State != State.Ready)
        {
            a.B.StateChanged += () =>
                {
                    if (a.B.State == State.Ready)
                        tcs.SetResult(a.B.Text);
                };
        }
        else
        {
            tcs.SetResult(a.B.Text);
        }

        return tcs.Task;
    }
}

person Moberg    schedule 09.01.2013    source источник
comment
Какие классы находятся под вашим контролем? Т.е. что вы можете изменить?   -  person Henk Holterman    schedule 09.01.2013
comment
Затем замените событие на WaitHandle (ManualResetEvent).   -  person Henk Holterman    schedule 09.01.2013
comment
Хорошо, это потребует большого рефакторинга, но я погуглю, чтобы узнать, как он используется (хотя все еще надеюсь на альтернативу).   -  person Moberg    schedule 09.01.2013
comment
где вы меняете состояние на Готово?   -  person Tilak    schedule 09.01.2013
comment
это асинхронное обновление (из предыдущего вызова из другого места) из бэкэнда, которое синхронизируется с потоком пользовательского интерфейса.   -  person Moberg    schedule 09.01.2013


Ответы (1)


Вы можете зарегистрироваться на мероприятие StateChanged и использовать TaskCompletionSource.

public static Task WaitForReady(this B b)
{
    TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
    Action handler = null;
    handler = () =>
    {
        if (b.State == State.Ready)
        {
            b.StateChanged -= handler;
            tcs.SetResult(null);
        }
    };

    b.StateChanged += handler;
    return tcs.Task;
}

Остерегайтесь, что может возникнуть гонка, если событие может быть вызвано до регистрации обработчика.

person Lee    schedule 09.01.2013
comment
да, что-то такое я и имел в виду - а то пусть звонящий ждет. - person Moberg; 09.01.2013
comment
хороший момент в гонке, но, к счастью, я уверен, что это работает в одном потоке. - person Moberg; 09.01.2013