3

Where can i create multiple long running background threads in Self Hosted Self Contained ASP.NET Core Microservice whose lifetime is same as micro-service lifetime? So that information retrieved from threads can be sent as a response to the requests.

Tried given code but it reduces http request performance when background threads are busy. Main method of Program.cs file is:

static void Main(string[] args)
{
    //Start background thread1
    //Start background thread2
    //Around 10 background threads
    //Start host
    var host = new WebHostBuilder()
        .UseKestrel()
        .UseUrls(ServerUrl)
        .UseConfiguration(config)
        .UseContentRoot(Directory.GetCurrentDirectory())
        .UseIISIntegration()
        .ConfigureServices(s => s.AddRouting())
        .Configure(app => app.UseRouter(r => { (new Router()).Route(r); }))
        .Build();
    host.Run();
}

Threads work in this way:

Thread t1 = new Thread(StartWork);
t1.IsBackground = true;
t1.Start();

public void StartWork()
{
    while (ApplicationIsRunning)
    {
        //Get database info >> login into remote devices (SSH) >> get information >> process information >> update application variables and database
        Thread.Sleep(10000);
    }
}

CPU utilization is only 1-5% when threads are busy but still http request performance is very bad. After going to sleep state performance again improves.

The issue is at connect method of SSH client connection. At some point connect method is not responding and it affect all other threads also. That is strange!

Renci.SshNet.SshClient sshClient = New Renci.SshNet.SshClient(sshConnectionInfo);
sshClient.Connect();

If one thread is busy in connection because of any reason it should not affect other threads.

Harit Kumar
  • 2,272
  • 2
  • 12
  • 24
  • I'm sorry but your question seems unclear to me and probably others. What are the background threads you are running and trying to achieve with them? The code you show is pretty much bog standard self host. – Steve Oct 31 '17 at 12:38
  • @Steve Added information about threads also. – Harit Kumar Oct 31 '17 at 13:02
  • 3
    "but it reduces http request performance when background threads are busy". Well, yeah, CPU isn't free. Unless your background threads are all thread pool threads running only `async` code when they're supposed to, any background work is going to reduce scalability a bit. Beyond having more physical cores, or decreasing the priority of the threads, there's not much you can do to avoid that. – Jeroen Mostert Oct 31 '17 at 13:04
  • 2
    "CPU utilization is only 1-5% when threads are busy but still http request performance is very bad." I/O isn't free either, nor is memory, nor is locking shared structures, nor a bunch of other things shared between threads. You will need to identify what it is that the other threads do, exactly, that slows down your HTTP requests, by using things like ETW traces and performance counters. Alternatively, you could consider performing the background stuff in a separate process, but depending on what it does, that still doesn't guarantee your core service keeps running smoothly. – Jeroen Mostert Oct 31 '17 at 13:21
  • Is it okay to use multiple background threads inside microservice? – Harit Kumar Oct 31 '17 at 13:28
  • 2
    Why not? Create all the background threads you like. Making sure things keep running smoothly is still your responsibility. The term "microservice" doesn't obligate you to do or not do anything, it just means "a service with a well-defined interface that performs one small task". It's still up to you to decide what that task is, and whether it requires background threads. A design where the background stuff is farmed out to another microservice for scalability is no more or less valid, it's just different. Fewer services = harder to scale, more services = harder to deploy. – Jeroen Mostert Oct 31 '17 at 13:37

2 Answers2

6

EDIT: code possibly from Steve Gordon's post at https://www.stevejgordon.co.uk/asp-net-core-2-ihostedservice ?

sure you can :) With IHostedService (out-of-the-box from .net core) you can implement the following:

public abstract class HostedService : IHostedService
    {

        private Task _executingTask;
        private CancellationTokenSource _cts;

        public Task StartAsync(CancellationToken cancellationToken)
        {
            // Create a linked token so we can trigger cancellation outside of this token's cancellation
            _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

            // Store the task we're executing
            _executingTask = ExecuteAsync(_cts.Token);

            // If the task is completed then return it, otherwise it's running
            return _executingTask.IsCompleted ? _executingTask : Task.CompletedTask;
        }

        public async Task StopAsync(CancellationToken cancellationToken)
        {
            // Stop called without start
            if (_executingTask == null)
            {
                return;
            }

            // Signal cancellation to the executing method
            _cts.Cancel();

            // Wait until the task completes or the stop token triggers
            await Task.WhenAny(_executingTask, Task.Delay(-1, cancellationToken));

            // Throw if cancellation triggered
            cancellationToken.ThrowIfCancellationRequested();
        }

        // Derived classes should override this and execute a long running method until 
        // cancellation is requested
        protected abstract Task ExecuteAsync(CancellationToken cancellationToken);
    }


then you can implement your abstract class:


public class DataRefreshService : HostedService
{
    private readonly RandomStringProvider _randomStringProvider;

    public DataRefreshService(RandomStringProvider randomStringProvider)
    {
        _randomStringProvider = randomStringProvider;
    }

    protected override async Task ExecuteAsync(CancellationToken cancellationToken)
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            await _randomStringProvider.UpdateString(cancellationToken);
            await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
        }
    }
}

and in your setup you only have to add the dependency:

services.AddSingleton<IHostedService, DataRefreshService>();

RandomStringProvider is just an example. You get the picture :)

.net core wires this automatically for you, works like a charm! Perfect to keep a rabbitmq-connection open!

give it a shot!

Tim Barrass
  • 4,813
  • 2
  • 29
  • 55
Roelant M
  • 1,581
  • 1
  • 13
  • 28
  • `CancellationTokenSource` should be disposed when you're using it to link up cancellation tokens, otherwise there's a [resource leak](https://stackoverflow.com/a/12474762/4137916). I don't know the lifetimes of `IHostedService` instances -- if they live for as long as the process, and this is a singleton (which both apply in this case, I suppose) the lack of disposing is not an issue, but it is something to keep in mind. (I don't know if it's easy to hook up the dispose logic without butchering the code either.) – Jeroen Mostert Nov 01 '17 at 12:05
  • @JeroenMostert `cancellationTokenSource` can't be disposed, because it is needed in the `StopAsync` to cancel the (long) running task(s). The `StopAsync` method is the 'closing' method of the application itself, so it will be disposed after it. And indeed, because it is a singleton - and needs to be- this is actually a non-issue, right? – Roelant M Nov 01 '17 at 12:25
  • If there is no way (or no expectation) that there will ever be explicitly created `IHostedService`s (I'm not an ASP.NET Core developer, mind you) then yes. (I don't see why disposing wouldn't be *possible*; you can dispose in the `StopAsync` if that's the last thing to be called, after you've read the state of the cancellation token for the last time. But, like I said, not a big thing.) – Jeroen Mostert Nov 01 '17 at 12:29
5

Tried given code but it reduces http request performance when background threads are busy.

The first thing to realize is that there is no true background work in the context of a web application. Web servers are designed to quickly service requests. There's a thread pool, usually composed of up to 1000 threads (often referred to as the "max requests" of the server, due to each request requiring a thread). That thread pool is a finite resource, and when you max it out, any further requests are queued until a thread becomes available again. Spinning up a new thread takes a thread from this pool. As a result, your one request is now consuming two threads instead of one. Do this type of stuff enough, and you can easily exhaust the thread pool with merely a handful of requests, which then brings your web server to its knees.

At least if you were doing some sort of async work, you might allow the main thread servicing the request to be returned to the pool while this new thread does its thing, but even then, you're simply trading one thread for another. However, here, you're not even doing that. Your code here is blocking, so you've now got threads sitting idle, when your thread pool is already being starved.

Long and short, don't do this. If there's any work that needs to be done that takes a not insignificant amount of time, that work should be offloaded to a background process, i.e. something running outside the context of your web application that can do the work without affecting the performance of your web application. You should pretty much never create a new thread in a web application. If you find yourself doing so, you need to rethink your design.

Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
  • 2
    "Spinning up a new thread takes a thread from this pool." This is not correct. Explicitly instantiating a thread with `new Thread` (as in the question) does not involve the thread pool at all. Even if this thread came from the pool, though, as long as the number of background threads is small and constant, rather than creating one for every request (two, in the question) the impact is still negligible (in terms of thread pool starvation, at least, resources are another matter). Explicitly creating a brand new thread (or two) on every request is indeed a very bad idea. – Jeroen Mostert Oct 31 '17 at 15:05
  • Yes @JeroenMostert background threads are limited. I am using around 10 background threads for different tasks and i am not creating thread on any request. – Harit Kumar Oct 31 '17 at 19:23
  • 1
    @HaritKumar: 10 is a lot more than 2, though. If these threads allocate a lot of memory when they're doing stuff, that would be a good explanation why you're seeing drops in throughput -- garbage collection "stops the world", and when there's a lot of objects to check for liveness, the world stops for a long time. Check your GC numbers. (But this goes back to the original advice of checking perf counters to see what's actually going on.) – Jeroen Mostert Oct 31 '17 at 19:37
  • Actually it's a self hosted self contained application running on Centos7. So it's bit difficult for me to check the GC, Process, Threads and Performance stuff. – Harit Kumar Oct 31 '17 at 19:48
  • I'll setup the environment and will find out the root cause. – Harit Kumar Oct 31 '17 at 19:52
  • 1
    @HaritKumar: Difficult but not impossible. See [here](https://github.com/dotnet/coreclr/blob/master/Documentation/project-docs/linux-performance-tracing.md). Also, assuming your code is portable, checking if the problems also happen on a Windows machine is 1) helpful to see if the problems are platform-specific and 2) if they're not, you'll have many more tools at your disposal. – Jeroen Mostert Oct 31 '17 at 19:53