Re: Async TCPClients



Hi Shak,

Inline:

Actually, I should have written "don't call Begin*" while on a ThreadPool
Thread since it is the Begin* methods that spawn a ThreadPool Thread, not
the End* methods. The reason you shouldn't call into an asynchronous
method from an asynchronous method where both threads will use the
ThreadPool is that it can cause dead-locks because there is a limit to
the number of ThreadPool Threads provided to your application (related to
the number of physical processors). However, for a simple RPC app,
especially if there will be a limited number of client connections, it is
acceptable to call BeginAsync from the AsyncCallback.

I think that's how I had read it anyway! I do understand the danger here,
but if the re-call to Begin* is the last thing done in the callback (ie
the callback has no more oppurtunity to block) then wouldn't it be ok,
even in the worst case? I think I'll have no more than a handful of
clients anyway, but I'll think about moving to a loop I ever need to scale
it up.

I see your point that waiting for the call to complete before accepting the
next client should solve the problem of ThreadPool Thread usage and
dead-locking, however the recommendation exists because it may be out of
your control how many other blocks of code will be using a ThreadPool Thread
to call into your library and you can't say for sure that even the .NET
framework code isn't using ThreadPool Threads internally. In other words,
it's not safe to assume that the number of ThreadPool Threads being used by
your application can be managed simply be rearranging the order of async
calls in your code.

If you were to use a synchronous loop you could spawn your own Thread
using a ThreadStart object to prevent dead-locks, but there may be a
slight performance cost. I have not compared the two methods. A problem
that you must work out when using a synchronous loop is that one client
should not have to wait for another client's RPC to complete before it
can establish a connection, so some asynchronous code is still required.
Preferably, code not executing on a ThreadPool Thread.

Hmm. But if a socket has already been established (and has it's own thread
on which it is reading on), surely it wouldn't matter? Going back to my
Async model:

The syncronous model is actually sync/async. Clients are accepted within a
synchronous loop and the code spawns a non-ThreadPool Thread to process the
request. There is no chance for dead-locking due to the physical ThreadPool
quantity, only in explicit synchronization code such as with the use of
Monitor.Lock or WaitHandles.

//somewhere else
o.BeginAsync(completeAsync, o)
...

void completeAsync(IAsyncResult ar)
{
Class o = (Class)ar.SyncState;
o.EndAsync();
//do stuff for this async cycle
//resestablish wait

The problem here is that all clients are queued even though your app should
be able to handle more than one request simealtaneously. You should call
o.BeginAsync immediately after o.EndAsync to start listening for another
client request before processing the current request. A problem that would
arise in a scaled-up application may be that too many subsequent requests
from different clients might block the first request from ever completing if
o.BeginAsync steadily completes synchronously. Just FYI.

o.BeginAsync(completeAsync, o)
}

This is as opposed to a sync loop:

//somewhere else

StartAThread(o.Work) //(1)
...

void Work()
{
while (true)
{
o.Begin(); // blocks
//do stuff for this cycle.
}
}

Where (1) can be replaced by your thread-kicking-off code of choice.
Presumeably for the TCPClient connecting case, "do stuff for this cycle"
means to establish a further thread to poll on read (either via async or
kicking off another thread manually). I don't think either introduce the
possibility of deadlock, although I'm now seeing the latter as being more
efficient since it doesn't create new threads. Do both these models
fulfill your condition that "one client should not have to wait for
another client's RPC to complete before it can establish a connection",
though?

Well, your example is not exactly the complete story for the sync/async
client-acceptance model.

// Here's a quick example that I wouldn't use in a real app as-is. Try an
OO approach instead.
// NOTE: this code was not tested and may not build
class Server : System.ComponentModel.Component
{
private readonly TcpListener listener = new TcpListener();
private bool running;

public void Start()
{
running = true;

// start listening for client connections
Thread listeningThread = Thread(new ThreadStart(ListenAsync));
listeningThread.Start();
}

private void ListenAsync()
{
while (running)
{
// block until a client connects
// TODO: handle exception if listener is disposed in a different
Thread; fail silently
TcpClient client = listener.AcceptTcpClient();

if (!running)
// if, while blocking, a different Thread changed the 'running'
state of the class but did not dispose of the listener
{
DisconnectClient(client);
break;
}

// As soon as the client is connected spawn another thread to
handle the request.
// This way, another connection will not be blocked until the
current request has been handled.
// Even if you were to call listener.BeginAcceptTcpClient here
the current request would
// still have to complete before the next connection would be
accepted, like the "queue"
// I mentioned above, since you wouldn't be listening for
another client until the end of the method.
Thread requestHandler = Thread(new
ParameterizedThreadStart(HandleRequestInternal))
requestHandler.Start(client);

/*
ASP.NET, I believe, uses a custom ThreadPool to do exactly what
I've done here,
which has either a manageable limit to the number of concurrent
requests or no limit.
Spawning a new Thread for each request might be too costly for
some applications
if they are not pooled.
*/
}
}

private void HandleRequestInternal(object info)
{
using (TcpClient client = (TcpClient) info)
{
// TODO: prepare request

try
{
HandleRequest(client);
}
finally
{
DisconnectClient(client);
}
}
}

private void HandleRequest(TcpClient client)
{
// TODO: handle request
}

private void DisconnectClient(TcpClient client)
{
client.GetStream().Close();
client.Dispose();
}
}

It seems that most Async calls (especially in streams and the like) seem
to be for a job that repeats. If this is the case, and a loop that blocks
is more efficient, what's the point of having Async calls? I mean it's not
like you'll only want to read or write to a stream once, right?

It may not be that a blocking loop is more effecient. Actually, as I've
mentioned, it might be less effecient unless properly coded. The reason for
using a loop is that it illeviates ThreadPool usage where it might be
problematic. The ThreadPool is very useful to queue up a task that must be
processed in the background (i.e. BackgroundWorker should be used in .NET
2.0) to free up a GUI Thread so that the end user has a better experience
with your application, and it can handle multiple tasks that may start and
stop frequently very efficiently, however it's not useful for servicing an
unlimited number of requests from remote clients or for code that blocks
during the lifetime of the application.

OK. I do have another question though: What happens if, while reading
bytes off of a network stream more data gets sent? Abstracting away from
the above, say you are expecting a particular message of unknown length.
How do you know when to stop reading bytes? All the examples I've seen
seem to wait till the number of bytes read<=0. Couldn't we get more than
one message accedentally in that scenario? If at all it stops?

A few things here...


That's brilliant, and exactly as I imagined it to be. In short, you have
to be able to determine when what you're receiving ends, usually using
some kind of prefix or suffix. Cool!

You got it. I don't know how many other ways there are for doing this but
I've had a lot of success with the "suffix" approach.

HTH


.



Relevant Pages

  • Re: [PHP] $_SERVER["REMOTE_ADDR"] returning ::1
    ... Proxies can be implemented as shared clusters such that any request going through the cluster could appear to come from one of a number of IPs (i.e. the client is not tied to a single proxy appliance). ... This is unlikely however because I believe most ISPs will do everything they can to issue a connection with the same IP when the lease expires but it's not something you can rely on. ... Request it with an IPv6 domain/IP and REMOTE_ADDR will also be IPv6. ...
    (php.general)
  • Re: Learning how to use "Remote Login"
    ... for client connection requests. ... "listening", by your definition. ... who listens for client requests. ...
    (comp.sys.mac.system)
  • Re: Learning how to use "Remote Login"
    ... for client connection requests. ... "listening", by your definition. ... who listens for client requests. ...
    (comp.sys.mac.system)
  • Re: async i/o completion routines, threading question
    ... Our code was using GetRequestStream() to post the request synchronously, ... I have both client and server logging and it's 13 seconds between ... HttpListener stuff on the server side, but 13 seconds to open a connection ...
    (microsoft.public.dotnet.framework)
  • Re: breaking the model
    ... > The forms data then is in the Request object. ... HTTP Request; in this case, the form POST Request from the Page. ... client and server. ...
    (microsoft.public.dotnet.framework.aspnet)