четверг, 6 ноября 2025 г.

25.11.07, QWEN, RazorPageService, BackGroundServices, Channels, Projects

25.11.07, QWEN, RazorPageService, BackGroundServices, Channels, Projects

F:\Projects\VS\RazorPageService\RazorPageService.sln

F:\Projects\VS\RazorPageService\RazorPageService01\RazorPageService01.csproj

F:\Projects\VS\RazorPageService\RazorPageService02\RazorPageService02.csproj

https://chat.qwen.ai/s/9b293c77-63d7-4f49-a77d-ffeccf35dbd7?fev=0.0.235

Конечно, вот простой пример для проекта ASP.NET Core Razor Pages, который показывает, как зарегистрировать два разных BackgroundService, каждый из которых будет выполнять свою логику (TradeWork и Logging) в отдельных асинхронных потоках.

Это именно то, что вы просили: раздельные "ветви" выполнения, зарегистрированные через AddHostedService.

1. Создание классов логики (TradeWork, Logging)

Создайте два новых класса, например, в папке Services вашего проекта.

using Microsoft.Extensions.Hosting; using System.Threading; using System.Threading.Tasks; public class TradeWorkService : BackgroundService { private readonly ILogger<TradeWorkService> _logger; public TradeWorkService(ILogger<TradeWorkService> logger) { _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { // Здесь будет ваша логика торговли Console.WriteLine("Hello from Tradework"); _logger.LogInformation("Hello from Tradework"); // Имитация работы (например, обработка котировок каждые 2 секунды) await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken); } } } ---- using Microsoft.Extensions.Hosting; using System.Threading; using System.Threading.Tasks; public class LoggingService : BackgroundService { private readonly ILogger<LoggingService> _logger; public LoggingService(ILogger<LoggingService> logger) { _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { // Здесь будет ваша логика логирования Console.WriteLine("Hello Logging"); _logger.LogInformation("Hello Logging"); // Имитация работы (например, агрегация и запись логов каждые 5 секунд) await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); } } }  --- using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; var builder = WebApplication.CreateBuilder(args); // Добавляем Razor Pages builder.Services.AddRazorPages(); // --- Регистрация ваших Background Services --- builder.Services.AddHostedService<TradeWorkService>(); builder.Services.AddHostedService<LoggingService>(); // --- Конец регистрации --- var app = builder.Build(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.MapRazorPages(); app.Run();  ---- 

profile
Qwen3-Coder

Конечно, вот простой пример для проекта ASP.NET Core Razor Pages, который показывает, как зарегистрировать два разных BackgroundService, каждый из которых будет выполнять свою логику (TradeWork и Logging) в отдельных асинхронных потоках.

Это именно то, что вы просили: раздельные "ветви" выполнения, зарегистрированные через AddHostedService.

1. Создание классов логики (TradeWork, Logging)

Создайте два новых класса, например, в папке Services вашего проекта.

TradeWorkService.cs:

csharp
using Microsoft.Extensions.Hosting;
using System.Threading;
using System.Threading.Tasks;

public class TradeWorkService : BackgroundService
{
private readonly ILogger<TradeWorkService> _logger;

public TradeWorkService(ILogger<TradeWorkService> logger)
{
_logger = logger;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// Здесь будет ваша логика торговли
Console.WriteLine("Hello from Tradework");
_logger.LogInformation("Hello from Tradework");

// Имитация работы (например, обработка котировок каждые 2 секунды)
await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken);
}
}
}

LoggingService.cs:

csharp
using Microsoft.Extensions.Hosting;
using System.Threading;
using System.Threading.Tasks;

public class LoggingService : BackgroundService
{
private readonly ILogger<LoggingService> _logger;

public LoggingService(ILogger<LoggingService> logger)
{
_logger = logger;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// Здесь будет ваша логика логирования
Console.WriteLine("Hello Logging");
_logger.LogInformation("Hello Logging");

// Имитация работы (например, агрегация и запись логов каждые 5 секунд)
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}
}

2. Регистрация BackgroundService в Program.cs

Откройте файл Program.cs. Добавьте регистрацию ваших сервисов перед вызовом builder.Build().

csharp
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;

var builder = WebApplication.CreateBuilder(args);

// Добавляем Razor Pages
builder.Services.AddRazorPages();

// --- Регистрация ваших Background Services ---
builder.Services.AddHostedService<TradeWorkService>();
builder.Services.AddHostedService<LoggingService>();
// --- Конец регистрации ---

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.MapRazorPages();

app.Run();

Объяснение:

  1. TradeWorkService и LoggingService наследуются от BackgroundService. Это стандартный способ создания долгоживущих фоновых задач в .NET.
  2. Метод ExecuteAsync определяет, какую работу будет выполнять сервис в фоновом режиме. Он запускается автоматически при старте приложения.
  3. AddHostedService<TradeWorkService>() и AddHostedService<LoggingService>() регистрируют эти классы в DI-контейнере .NET как IHostedService. Это говорит ASP.NET Core запустить их при запуске приложения.
  4. CancellationToken stoppingToken позволяет корректно остановить выполнение задачи при выключении приложения.
  5. Каждый BackgroundService будет выполняться в своем собственном асинхронном "потоке" (точнее, в своей задаче Task, которая может использовать разные потоки из пула).

Теперь, при запуске вашего Razor Pages приложения, вы увидите в консоли (и в логах, если настроите) сообщения "Hello from Tradework" и "Hello Logging", появляющиеся с разной частотой, демонстрируя независимую работу двух фоновых процессов. ----- Отличный вопрос! Выбор инструментария для взаимодействия между потоками зависит от ваших требований к надежности, производительности и сложности обработки данных. Для внутрипроцессного взаимодействия (внутри одного приложения) в .NET, особенно если речь идет о передаче потоков данных (например, из TradeWork в Logging), наиболее современным и рекомендуемым подходом являются System.Threading.Channels. Почему Channels? Асинхронность: Они отлично работают с async/await, что позволяет не блокировать потоки ожиданием. Производительность: Оптимизированы для сценариев передачи потоков данных. Надежность: Обеспечивают надежную передачу сообщений между производителем (например, TradeWork) и потребителем (например, Logging). Гибкость: Можно легко настроить размер буфера (ограниченный/неограниченный). Простота: Проще и надежнее, чем ConcurrentQueue в асинхронной среде, и намного проще, чем Rx.NET для базового сценария. Альтернативы (и почему Channels предпочтительнее для вашего случая): ConcurrentQueue<T>: Потокобезопасная коллекция. Можно использовать, но требует ручной реализации ожидания, если очередь пуста (например, Task.Delay), что менее эффективно. Rx.NET (Reactive Extensions): Мощная библиотека для работы с потоками событий. Используется, если нужна сложная обработка потока данных (фильтрация, объединение, таймеры и т.д.). Для простой передачи сообщений — это избыточно. BlockingCollection<T>: Обертка над IProducerConsumerCollection, обеспечивающая потокобезопасность. Также требует ручного управления ожиданием и может быть менее гибкой и асинхронно-ориентированной, чем Channels. Пример использования Channels: Вот простой пример, демонстрирующий, как отправить сообщение из одного BackgroundService (имитирующего TradeWork) в другой BackgroundService (имитирующего Logging) через Channel, и вывести его в консоль. 1. Установите пакет (если его нет): Обычно System.Threading.Channels входит в стандартную библиотеку .NET 6+ и выше. Если вы используете .NET 5 или ниже, возможно, потребуется установить NuGet пакет System.Threading.Channels. 2. Создайте TradeWorkService.cs (отправляет сообщения): using Microsoft.Extensions.Hosting; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; public class TradeWorkService : BackgroundService { private readonly ILogger<TradeWorkService> _logger; private readonly ChannelWriter<string> _channelWriter; // Writer для отправки public TradeWorkService(ILogger<TradeWorkService> logger, Channel<string> channel) { _logger = logger; _channelWriter = channel.Writer; // Получаем writer из канала } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { int counter = 0; while (!stoppingToken.IsCancellationRequested) { string message = $"TradeMessage #{++counter}"; // Отправляем сообщение в канал await _channelWriter.WriteAsync(message, stoppingToken); _logger.LogInformation("Sent: {Message}", message); // Имитация работы торговли await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken); } } } --- using Microsoft.Extensions.Hosting; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; public class LoggingService : BackgroundService { private readonly ILogger<LoggingService> _logger; private readonly ChannelReader<string> _channelReader; // Reader для получения public LoggingService(ILogger<LoggingService> logger, Channel<string> channel) { _logger = logger; _channelReader = channel.Reader; // Получаем reader из канала } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // Цикл чтения из канала await foreach (var message in _channelReader.ReadAllAsync(stoppingToken)) { // Выводим сообщение в консоль и лог Console.WriteLine($"Received in Logging Thread: {message}"); _logger.LogInformation("Received in Logging Thread: {Message}", message); // Имитация обработки лога (например, запись в БД) // await ProcessLogAsync(message, stoppingToken); } } } --- using Microsoft.Extensions.Hosting; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; public class LoggingService : BackgroundService { private readonly ILogger<LoggingService> _logger; private readonly ChannelReader<string> _channelReader; // Reader для получения public LoggingService(ILogger<LoggingService> logger, Channel<string> channel) { _logger = logger; _channelReader = channel.Reader; // Получаем reader из канала } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // Цикл чтения из канала await foreach (var message in _channelReader.ReadAllAsync(stoppingToken)) { // Выводим сообщение в консоль и лог Console.WriteLine($"Received in Logging Thread: {message}"); _logger.LogInformation("Received in Logging Thread: {Message}", message); // Имитация обработки лога (например, запись в БД) // await ProcessLogAsync(message, stoppingToken); } } } ---- using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using System.Threading.Channels; // Создаем канал. Можно настроить емкость, например, new BoundedChannelOptions(100) // Используем Unbounded для простоты, но в реальности часто используют Bounded с Backpressure. var channel = Channel.CreateBounded<string>(new BoundedChannelOptions(50) { FullMode = BoundedChannelFullMode.DropOldest // Или DropNew, или Wait (по умолчанию) }); // var channel = Channel.CreateUnbounded<string>(); // Неограниченный (потенциально рискованно) var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorPages(); // Регистрируем наш канал как Singleton, чтобы оба сервиса использовали один и тот же экземпляр builder.Services.AddSingleton(channel); // Регистрируем Background Services builder.Services.AddHostedService<TradeWorkService>(); builder.Services.AddHostedService<LoggingService>(); var app = builder.Build(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.MapRazorPages(); app.Run(); ---- 

  1. В Program.cs создается один Channel<string>.
  2. Этот канал регистрируется как Singleton в DI-контейнере.
  3. TradeWorkService получает ChannelWriter<string> через DI и использует WriteAsync, чтобы отправить сообщение в канал.
  4. LoggingService получает ChannelReader<string> через DI и использует ReadAllAsync (внутри await foreach) для асинхронного получения и обработки сообщений из канала.
  5. Channels обеспечивают асинхронную, потокобезопасную передачу сообщений между двумя фоновыми сервисами, которые работают в разных потоках.

Это наиболее подходящий и стандартный способ для вашего сценария в современном .NET. Отличная новость! Рад, что предоставленный пример с BackgroundService и Channels помог и заработал в вашем проекте. Это действительно надежный и стандартный способ организации асинхронного взаимодействия между компонентами внутри одного приложения. Если у вас возникнут вопросы по дальнейшей разработке, например, как масштабировать архитектуру, внедрить Entity Framework для сохранения данных, или настроить более сложную логику обработки в LoggingService, — не стесняйтесь, задавайте. Отличный вопрос. Это важный аспект работы BackgroundService и IHostedService в .NET. Кто отправляет CancellationToken stoppingToken? CancellationToken stoppingToken, который передается в метод ExecuteAsync, не отправляется в него напрямую как сообщение. Вместо этого, сам .NET хост (Host) управляет этим токеном. Вот как это работает: Создание токена: При запуске приложения (.NET Host) создается CancellationTokenSource (обычно внутри IHost или IWebHost). Этот CancellationTokenSource связан с жизненным циклом всего приложения. Передача токена: Хост передает CancellationToken, полученный из этого CancellationTokenSource, методу ExecuteAsync вашего BackgroundService. Отмена: Когда приложение получает сигнал на остановку (например, нажатие Ctrl+C в консоли, получение сигнала SIGTERM в Linux, или завершение работы IIS), хост вызывает метод Cancel() на своем внутреннем CancellationTokenSource. Реакция: Как только CancellationTokenSource.Cancel() вызывается, stoppingToken.IsCancellationRequested становится true, и все задачи (Task), которые "прослушивают" этот токен (например, через Task.Delay(token), channel.Reader.WaitToReadAsync(token), channel.Writer.WriteAsync(token) и другие асинхронные методы, принимающие CancellationToken), получают сигнал об отмене. Они могут выбросить OperationCanceledException или просто проверить флаг IsCancellationRequested. Выход из цикла: В вашем await foreach (var message in _channelReader.ReadAllAsync(stoppingToken)) вызов ReadAllAsync знает о stoppingToken. Когда токен отменяется, ReadAllAsync завершает свою работу (обычно выходит из внутреннего цикла ожидания и чтения), и await foreach прекращает итерацию, позволяя методу ExecuteAsync завершиться. Вывод: CancellationToken stoppingToken автоматически предоставляется .NET хостом вашему BackgroundService. Хост отвечает за его отмену, когда приложение начинает останавливаться. Вам не нужно искать явный вызов ExecuteAsync с этим токеном в вашем коде — это инфраструктура ASP.NET Core/.NET делает это за кулисами, реализуя паттерн IHostedService. Это стандартный и надежный способ уведомления фоновых сервисов о необходимости корректно завершить свою работу. Когда вы регистрируете несколько классов, наследующихся от BackgroundService (или реализующих IHostedService), в .NET-приложении через builder.Services.AddHostedService<...>(), .NET хост управляет одним общим CancellationTokenSource для всего приложения. При остановке приложения (например, по сигналу Ctrl+C, SIGTERM или при graceful shutdown) хост вызывает Cancel() на этом общем CancellationTokenSource. В результате, все зарегистрированные BackgroundService, у которых есть метод ExecuteAsync(CancellationToken stoppingToken), получат один и тот же токен (полученный из общего CancellationTokenSource), и все они одновременно получат сигнал об отмене, как только хост решит остановить приложение. Каждый из ваших сервисов должен корректно обрабатывать stoppingToken.IsCancellationRequested или исключения OperationCanceledException, чтобы завершить свою работу. Итак, да, .NET автоматически предоставляет и одинаково распространяет один CancellationToken среди всех ваших BackgroundService для корректной координации их остановки.

 

 

Комментариев нет:

Отправить комментарий