Multithread answering for HttpListener
Asked Answered
B

3

6

I have single thread process which executes some long time. I need several users to have access to execute this process and I choose http protocol to manage invocation.

Naturally, when one process is working everybody else should wait till it's done. If process is available it executes. If not then BUSY answer is sent.

Here is implementation:

using System;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;

namespace simplehttp
{
    class Program
    {
        private static System.AsyncCallback task;
        private static System.Threading.ManualResetEvent mre = new System.Threading.ManualResetEvent(false);// Notifies one or more waiting threads that an event has occurred. 
        private static HttpListenerContext workingContext = null;

        public static bool isBackgroundWorking()
        {
            return mre.WaitOne(0);
        }
        static void Main(string[] args)
        {
            new Thread(() =>
            {
                Thread.CurrentThread.IsBackground = true;
                while (true)
                {
                    Console.WriteLine("    waitOne " + isBackgroundWorking());
                    mre.WaitOne(); // Blocks the current thread until the current WaitHandle receives a signal.
                    Console.WriteLine("    do job" + " [" + Thread.CurrentThread.Name + ":" + Thread.CurrentThread.ManagedThreadId + " ]\n");
                    HttpListenerRequest request = workingContext.Request;
                    HttpListenerResponse response = workingContext.Response;
                    string responseString = "WORK " + DateTime.Now ;
                    byte[] buffer = System.Text.Encoding.UTF8.GetBytes(responseString);
                    response.ContentLength64 = buffer.Length;
                    System.IO.Stream output = response.OutputStream;
                    Thread.Sleep(10000);
                    output.Write(buffer, 0, buffer.Length);
                    output.Close();
                    Console.WriteLine("    " + responseString + "\t" + DateTime.Now);
                    workingContext = null;
                    mre.Reset(); // Sets the state of the event to nonsignaled, causing threads to block.
                }
            }).Start();

            // Create a listener.
            HttpListener listener = new HttpListener();

            listener.Prefixes.Add("http://localhost:6789/index/");
            listener.Start();
            Console.WriteLine("Listening..." + " [" + Thread.CurrentThread.Name + ":" + Thread.CurrentThread.ManagedThreadId + " ]\n");

            task = new AsyncCallback(ListenerCallback);

            IAsyncResult resultM = listener.BeginGetContext(task,listener);
            Console.WriteLine("Waiting for request to be processed asyncronously.");

            Console.ReadKey();
            Console.WriteLine("Request processed asyncronously.");
            listener.Close();
        }

        private static void ListenerCallback(IAsyncResult result)
        {
            HttpListener listener = (HttpListener) result.AsyncState;

            //If not listening return immediately as this method is called one last time after Close()
            if (!listener.IsListening)
                return;

            HttpListenerContext context = listener.EndGetContext(result);
            listener.BeginGetContext(task, listener);

            if (workingContext == null && !isBackgroundWorking())
            {
                // Background work
                workingContext = context;
                mre.Set(); //Sets the state of the event to signaled, allowing one or more waiting threads to proceed.
            }
            else
            {
            HttpListenerRequest request = context.Request;
            HttpListenerResponse response = context.Response;
            string responseString = "BUSY "+ DateTime.Now + " [" + Thread.CurrentThread.Name + ":" + Thread.CurrentThread.ManagedThreadId;
            byte[] buffer = System.Text.Encoding.UTF8.GetBytes(responseString);
            response.ContentLength64 = buffer.Length;
            System.IO.Stream output = response.OutputStream;
            output.Write(buffer, 0, buffer.Length);
            output.Close();
            Console.WriteLine(responseString + "\t" + DateTime.Now);
            }
        }
    }
}

To test I do 2 http calls. I expect have 2 different answers WORK and BUSY. However I see that second request waits first to finish and then executes.

      waitOne False
Listening... [:10 ]

Waiting for request to be processed asyncronously.
      do job [:11 ]

      WORK 1/24/2016 10:34:01 AM  1/24/2016 10:34:11 AM
      waitOne False
      do job [:11 ]

      WORK 1/24/2016 10:34:11 AM  1/24/2016 10:34:21 AM
      waitOne False

What is wrong in my understanding how it should work?

Update (too many comments are not ecouraged by SO): My code looks awkward because it is replication of real process. In "my" application working process is main process which has has "courtesy" to run embedded C# code at some particular moments. So, I cannot run new task to process request and it must be asyncronious since working process does its own job and only invokes slave piece of code to notify clients when data is available. It is asyncroinious because code is invoked and should finish as soon as possible or it will block master application. I'll try to add additional thread with synchronous call and see hot it affects situation.

Debugger is not used in this example to not interfere with real time process and time stamps printed to Console. Debugging is great and necessary but in this case I try to substitute with output to avoid extra actor in synchronization/waiting scenario.

The application itself is not heavy loaded conversation. 1-3 clients seldom ask main application for answer. http protocol is used for convenience not for heavy or often conversations. It appears that some browsers like IE work fine (Windows to Windows conversation?) and some like Chrome (more system agnostic) replicate my application behavior. Look at the time stamps, Chrome, IE,IE,Chrome and last Chrome still went to WORK process. BTW, code is changed per conversation suggestion and now new request is placed immediately after retrieving previous one.

    HttpListenerContext context = listener.EndGetContext(result);
    listener.BeginGetContext(task, listener); 

enter image description here

Also, following suggestions, I had change asyncronious call to syncroniuos and result is still the same

private static void ListenerCallback(IAsyncResult result)
{
    HttpListener listener = (HttpListener) result.AsyncState;

    //If not listening return immediately as this method is called one last time after Close()
    if (!listener.IsListening)
        return;

    HttpListenerContext context = listener.EndGetContext(result);

    while (true)
    {
        if (workingContext == null && !isBackgroundWorking())
        {
            // Background work
            workingContext = context;
            mre.Set(); //Sets the state of the event to signaled, allowing one or more waiting threads to proceed.
        }
        else
        {
            HttpListenerRequest request = context.Request;
            HttpListenerResponse response = context.Response;
            string responseString = "BUSY " + DateTime.Now + " [" + Thread.CurrentThread.Name + ":" +
                                    Thread.CurrentThread.ManagedThreadId;
            byte[] buffer = System.Text.Encoding.UTF8.GetBytes(responseString);
            response.ContentLength64 = buffer.Length;
            System.IO.Stream output = response.OutputStream;
            output.Write(buffer, 0, buffer.Length);
            output.Close();
            Console.WriteLine(responseString + "\t" + DateTime.Now);
        }
        context=listener.GetContext();
    }
}
Blackfoot answered 24/1, 2016 at 15:56 Comment(0)
L
3

The code as posted works exactly like it should:

enter image description here

Can't reproduce. I guess that answers the question because apparently you're driving the test workload incorrectly. I drove it by repeatedly clicking the Fiddler composer send button.

Thanks for posting executable code, though. I should have tried that earlier!

Labia answered 24/1, 2016 at 23:47 Comment(11)
I had open 2 Chromes and click reload button in one and in 2 seconds in another. Both immedaitely went to "Waiting answer" status. Want screenshot? :) I agree - it works sometimes. Even most of times. The reason I started to dig is that in production we see it happens pretty often. I assume I did quite good work to extract code and to show that it doesn't work. Unless, of course, you think that it is Photoshop and I'm having fun confusing people who's answers I really appreciate.Blackfoot
Hm... Can you repro using Fiddler? That would be a cleaner way that's maybe easier to understand and repro. In fact I get the same behavior using Chrome but Fiddler shows that Chrome is making the requests one after the other. Don't know why but that's not an app bug.Labia
IE behaves as expected.Labia
I attached my screenshot. I did it again and behaves in similar way - wrong one. Browsers are only to emulate clients. The whole reason I do this - I see wrong behavior in production and was able to reproduce it with simple code. I really need ideas how I can prevent it in real life not to make some simple stupid code to run properly. Anyway, really appreciate your help.Blackfoot
I can reproduce the problem as described in the question pretty reliably with Chrome but not Microsoft Edge or raw HTTP requests queued in parallel from within the same process (which behave as per @usr's answer).Enterprise
OK, let's prove that this listener is basically capable of receiving concurrent requests. Replace the listener callback with EndGetContext; BeginGetContext;Sleep(10000);. This should result in all requests finishing after 10sec regardless of how many there are. Maybe we'll find that only one concurrent request can happen on your machine for some reason. That would explain it.Labia
@usr, I'm not arguing with your conclusions regarding the code as posted - on the contrary, I arrived at the same results independently - at least until I started testing with Chrome. I am inclined think that there's something else (something external) at play here: i.e. Chrome using a named mutex on a hashed URL or similar, to throttle dispatching requests to the same address.Enterprise
@KirillShlenskiy it definitely is, the requests just don't hit the code. Chrome is the wrong app to test this since it does this throttling. Either I have to be able to repro on my machine or he has to run experiments on his.Labia
@Labia Request did hit the code. You can see it in results with different time stamp. I changed code per suggestion as well as post. Result is the same and different for IE/Chrome. Looks that my application works as the Chrome,Blackfoot
OK. I noticed there's a bug now, two requests can simultaneously cause work to be started. A simple fix would be to call BGC after setting the event in the one branch and call BGC immediately in the other branch.Labia
Sorry, I see only one BGC call in the Task. I also changed to synchronous call and result is the same. See edited post.Blackfoot
L
2

I have not totally read and understood that code. It's structure is awkward. Here's a much cleaner structure that's easy to get right:

while (true) {
 var ctx = GetContext();
 Task.Run(() => ProcessRequest(ctx));
}

That simply dispatches all incoming work. Then:

object @lock = new object(); //is work being done?
void ProcessRequest(Context ctx) {
 if (!Monitor.Enter(@lock))
  SendBusy();
 else {
  ProcessInner(ctx); //actually do some work
  Monitor.Exit(@lock);
 }
}

That's really all that is necessary.

In particular it is pointless for you to use async IO. I assume you have copied that code or idea from somewhere. Common mistake. Async IO helps you nothing at all here and convoluted the code.

Labia answered 24/1, 2016 at 16:31 Comment(11)
Hm... that's important information. So what is real and what's fake about the code you posted? i don't know what changes I can suggest and where to look.Labia
One reason might be that should be calling BeginGetContext right after EndGC. As the code is right now there is a lot of work performed in between.Labia
Btw, did you use the debugger to see what happens?Labia
No, I didn't use debugger. Also I'm not confident with your suggestion. 1) Is it wise to call BeginGetContext before previous request is answered? 1.a) This is static class and method as well so only one instance executes at once. If next request appears in the middle of previous then it could be a trouble. I'm not sure how it will work. 2) It doesn't matter how long BUSY request is processed - it doesn't go there at all. WORK request takes time but it all in another thread. After WORK is initiated by simple mre.Set() then BeginGetContext is invoked immediately as next operator.Blackfoot
Yes, it is wise because you want requests to be concurrent. If not, you get this "work-work" situation. You can refactor task away, just inline it. workingContext will only be set by one request at a time (the working one). It's true that this will probably not solve the problem but since there are no other leads to go on you should make the change. I assume there is a reason you are using async IO for GetContext? If not, it's simpler to use a background thread calling GetC in a loop.Labia
Why did you not use a debugger? It should show you whats going on.Labia
You're spawning thread pool tasks in a tight loop (without awaiting the resulting Task instance). Can't see how that can possibly be good.Enterprise
@KirillShlenskiy the loop is throttled to run only when there's an incoming request. That's fine if not too many requests are outstanding at the same time, like 100s.Labia
In my example there are only 2 requests. It's not loaded interaction. 2 clients connect to one server. and what do you mean "awaiting results"? Request for listening is last operator the the method.Blackfoot
@usr, ah ok, I see now. In that case I'd cut out the middle man and just ThreadPool.QueueUserWorkItem(_ => ProcessRequest(ctx)).Enterprise
@KirillShlenskiy that's a legacy API and generally advised against (I certainly do). It's best to treat it as a code smell. The Task API is better because you can attach an error handling continuation or track a set of outstanding tasks so that shutdown can be orderlyLabia
P
-2

With this code, you are able to handle many requests and response them in the same time. I can download video from server since 2 differents requests in the same time. Nevermind the votes and try it.

using Microsoft.VisualBasic;
using System;
using System.Collections;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq.Expressions;
using System.Net;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Text;
using System.Threading;
using System.Xml.Linq;

public class HttpListenerExample
{
    static Int32 i;
    static String[] arraylist;
    static Hashtable hashtable;
    static Hashtable hashtable2;
    public static void Main(string[] args)
    {
        HttpListener listener = new HttpListener();
        listener.Prefixes.Add("https://192.168.0.154:443/");
        listener.Prefixes.Add("http://192.168.0.154:80/");
        i = 0;

        
        listener.Start();
        Console.WriteLine("Listening...");

        // Begin waiting for a client request
        listener.BeginGetContext(new AsyncCallback(ListenerCallback), listener);

        // Prevent the application from exiting immediately
        Thread.Sleep(Timeout.Infinite);
    }

    private static void ListenerCallback(IAsyncResult result)
    {
        HttpListener listener = (HttpListener)result.AsyncState;
        // Complete the asynchronous operation
        HttpListenerContext context = listener.EndGetContext(result);

        listener.BeginGetContext(new AsyncCallback(ListenerCallback), listener);
        // Obtain the request and response objects
        HttpListenerRequest request = context.Request;
        HttpListenerResponse response = context.Response;
        var writer = new StreamWriter(response.OutputStream);
        writer.Write("Hello World");
        writer.Close();
        response.OutputStream.Close();
}
}

Parcenary answered 6/9 at 22:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.