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

Вызов методов async из службы Windows

У меня есть служба Windows, написанная на С#, которая периодически запускает фоновые задания. Как правило, в любой момент времени параллельно работают несколько десятков сильно связанных задач ввода-вывода (загрузка больших файлов и т.д.). Служба работает на относительно занятом веб-сервере (необходимо на данный момент), и я думаю, что это может значительно помочь с точки зрения сохранения потоков для использования асинхронных API как можно больше.

Большая часть этой работы выполнена. Все задания теперь полностью асинхронны (использование HttpClient и т.д.), А также основной цикл работы (с большими дозами Task.Delay). Осталось выяснить, как правильно и безопасно запускать основной цикл из сервиса OnStart. Essentialy, это предостережение о дилемме "вызов-асинхронная синхронизация". Ниже я до сих пор (сильно упрощен).

в Program.cs:

static void Main(string[] args) {
    TaskScheduler.UnobservedTaskException += (sender, e) => {
        // log & alert!
        e.SetObserved();
    };
    ServiceBase.Run(new MyService());
}

в MyService.cs:

protected override void OnStart(string[] args) {
    _scheduler.StartLoopAsync(); // fire and forget! will this get me into trouble?
}

Это вызов, который вызывает StartLoopAsync, который касается меня. Я не могу просто Wait() вернуть возвращаемую задачу, потому что OnStart должен вернуться относительно быстро. (Петли задания должны запускаться по отдельному потоку.) Приходят в голову пару соображений:

  • Я хорошо освещен до незаметных исключений, разместив этот обработчик в Main?
  • Будет ли какая-либо польза от использования Task.Run, что-то вроде Task.Run(() => _scheduler.StartLoopAsync().Wait());?
  • Будет ли какая-нибудь польза для вызова _scheduler.StartLoopAsync().ConfigureAwait(false) здесь? (Я сомневаюсь, так как здесь нет await.)
  • Будет ли какая-либо польза от использования Stephen Cleary AsyncContextThread в этой ситуации? Я не видел никаких примеров использования этого, и поскольку я начинаю бесконечный цикл, я не знаю, что синхронизация обратно в какой-то контекст даже имеет значение здесь.
4b9b3361

Ответ 1

UnobservedTaskException будет вызван для всех незаметных исключений Task, поэтому это хорошее место для ведения журнала, как это. Однако это не очень удобно, потому что в зависимости от вашей логики программы вы можете видеть ложные сообщения; например, если вы Task.WhenAny и затем игнорируете более медленную задачу, тогда любые исключения из этой более медленной задачи должны игнорироваться, но они отправляются на UnobservedTaskException. В качестве альтернативы рассмотрим размещение ContinueWith в задаче верхнего уровня (тот, который возвращается из StartLoopAsync).

Ваш вызов StartLoopAsync выглядит хорошо для меня, считая его асинхронным. Вы можете использовать TaskRun (например, Task.Run(() => _scheduler.StartLoopAsync()) - no Wait), но единственным преимуществом было бы, если бы StartLoopAsync сам мог вызвать исключение (в отличие от отказа от возвращаемой задачи), или если он тоже задолго до первого await.

ConfigureAwait(false) полезен только при выполнении await, как вы предполагали.

My AsyncContextThread предназначен для такого рода ситуаций, но он также был разработан очень просто.:) AsyncContextThread предоставляет независимый поток с основным контуром, похожим на ваш планировщик, в комплекте с TaskScheduler, TaskFactory и SynchronizationContext. Однако это просто: он использует только один поток, и все точки планирования/контекста возвращаются к тому же потоку. Мне это нравится, потому что это значительно упрощает проблемы безопасности потоков, а также позволяет одновременные асинхронные операции, но не полностью использует пул потоков, поэтому, например, работа с привязкой к ЦП блокирует основной цикл (аналогичный сценарию потока пользовательского интерфейса).

В вашей ситуации кажется, что AsyncContextThread может позволить вам удалить/упростить часть кода, который вы уже написали. Но, с другой стороны, он не многопоточен, как ваше решение.

Ответ 2

Не ответ сам по себе, но через год после публикации этого вопроса мы переносим эту услугу на службу Azure Cloud. Я нашел шаблон Azure SDK, чтобы быть хорошим примером правильного вызова async из синхронизации, предоставления поддержки отмены, обработки исключений, и т.д. Это не совсем яблоки с яблоками с услугами Windows, поскольку последний не обеспечивает эквивалент метода Run (вам нужно начать свою работу в OnStart и сразу же вернуться), но для чего это стоит, вот он:

public class WorkerRole : RoleEntryPoint
{
    private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
    private readonly ManualResetEvent runCompleteEvent = new ManualResetEvent(false);

    public override void Run() {
        Trace.TraceInformation("WorkerRole1 is running");

        try {
            this.RunAsync(this.cancellationTokenSource.Token).Wait();
        }
        finally {
            this.runCompleteEvent.Set();
        }
    }

    public override bool OnStart() {
        // Set the maximum number of concurrent connections
        ServicePointManager.DefaultConnectionLimit = 12;

        // For information on handling configuration changes
        // see the MSDN topic at http://go.microsoft.com/fwlink/?LinkId=166357.

        bool result = base.OnStart();

        Trace.TraceInformation("WorkerRole1 has been started");

        return result;
    }

    public override void OnStop() {
        Trace.TraceInformation("WorkerRole1 is stopping");

        this.cancellationTokenSource.Cancel();
        this.runCompleteEvent.WaitOne();

        base.OnStop();

        Trace.TraceInformation("WorkerRole1 has stopped");
    }

    private async Task RunAsync(CancellationToken cancellationToken) {
        // TODO: Replace the following with your own logic.
        while (!cancellationToken.IsCancellationRequested) {
            Trace.TraceInformation("Working");
            await Task.Delay(1000);
        }
    }
}