28

I've been reading a lot about threading lately as I am looking to develop a high-performance, scalable TCP server capable of handling up to 10,000-20,000 clients, each client of which is consistently communicating bidirectionally to the server with a command-based system. The server will receive a command, and execute either a single (or many) tasks as per the command. My question is how to appropriately make use of the .NET threading constructs for a variety of situations, executing tasks that could take between one minute to several hours, depending on the work being performed.

What's confusing me the most is the fact that everywhere I read, I see something like "use a manually created Thread (or custom thread pool) to handle 'long-running' tasks, and use TPL for short-lived tasks, or tasks that require parallel processing." What exactly is a long-running task? Is that 5 seconds, 60 seconds, an hour?

With what time frame should I be using each of these three methods of creating threads:

  • Manually created Threads
  • The .NET ThreadPool class
  • TPL

Another issue I've contemplated is as follows--say my server does in fact have 20,000 clients connected, each of which sends 1 command (which could translate to one or many tasks) per second. Even with powerful hardware, isn't there a chance that I could be pushing too high of a workload into whatever thread pool / work item queue I have, thereby eventually generating an OutOfMemoryException after the queue slowly fills to the maximum?

Any insight would be greatly appreciated.

cjones26
  • 3,459
  • 1
  • 34
  • 51
  • And on average how long will each command take to be procesed? – rene Mar 13 '11 at 09:44
  • That's the thing--there's really not telling how long the command will take to complete. I feel that I've received enough information to plan accordingly. Of course 20,000 clients is a long shot (5,000 is more like it), but I want to be ready to scale in the future if necessary. Thank you everyone for your responses. – cjones26 Mar 13 '11 at 09:57
  • 2
    @slashp Long running means "more than a couple hundred milliseconds", ie. something that disrupts your other work (and introduces significant latency to processing). If you aim for 5 ms latency for example, even 1ms can be considered "long running" :D Also, I'd like to point out that 20k TCP clients is getting close to the practical limit - each TCP connection needs a separate port, and you only have 65535 of those *maximum* - and they live for about 4 minutes after being closed. You might have to think about scaling out (more servers) rather than scaling up (more connections per server). – Luaan Mar 27 '14 at 08:42

4 Answers4

18

Actually, for that scenario all of those are secondary; the first thing you should look at is asyc-IO, aka .BeginRead(...) etc; this allows you to minimise the number of threads by waiting on IO completion ports - much more efficient.

Once you have a complete message, at that scale I would throw the message into a custom thread-pool/synchronized-queue. I would have a controlled number of regular threads (not pool threads or IOCP) servicing that queue to process each item.

As it happens I'm doing something similar (lower scale) at the moment; to prevent memory exploding, I have capped the work queue; if it gets full (i.e. the workers can't keep up) then you might block IOCP for a small while, perhaps with a timeout eventually that tells the client "too busy" at the IOCP layer.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • +1 for "queue + servicing threads" - very likely to be the best approach – Adam Ralph Mar 13 '11 at 09:47
  • 1
    Sorry, I forgot to mention I understand IOCP will be used for the networking portion, I just wanted to give some background on what I want to accomplish. I also understand I need to be careful about heap fragmentation when using IOCP which can cause OutOfMemoryException as well. I really just need clarification on when to use which of the three constructs, as well as the definition of a "long-running" task. – cjones26 Mar 13 '11 at 09:48
  • @slashp I edited to add some thoughts on the memory issue; since you are likely to be running constantly, you may as well own the threads yourself - avoids messing with ThreadPool, plus you can name them – Marc Gravell Mar 13 '11 at 09:50
10

What's confusing me the most is the fact that everywhere I read, I see something like "use a manually created Thread (or custom thread pool) to handle 'long-running' tasks, and use TPL for short-lived tasks, or tasks that require parallel processing."

Strange advice, or maybe you mis-quoted a little. A thread is also capable of parallel processing, and with the TPL you can create a Task with the LongRunning option. What remains is that you should not launch long tasks on the ThreadPool.

What exactly is a long-running task? Is that 5 seconds, 60 seconds, an hour?

The TPL runs on top of the ThreadPool and the TP will create new Threads at a max of 2 per second. So long-running is >= 500 ms


Even with powerful hardware, isn't there a chance that I could be pushing too high of a workload into whatever thread pool / work item queue I have,

Yes, no Threading tool can extend your actual capacity...

With 20k clients you will probably need a Server Farm, an option to include in your design early...

So you should probably give WCF a good look before getting to deep into sockets.

H H
  • 263,252
  • 30
  • 330
  • 514
  • Now I thought the thread scheduler wouldn't always respect the "LongRunning" option? Thanks for the info on "long-running." – cjones26 Mar 13 '11 at 09:50
  • @slas LongRunning is a hint for the scheduler so I suppose it's free to ignore it, the whole "no LongRunning" thing is about (not) upsetting the scheduler / load-balancing. – H H Mar 13 '11 at 09:54
  • 1
    Even though LongRunning is defined as a hint the actual implementation in the default TaskScheduler will always create a new thread - see http://coderkarl.wordpress.com/2012/12/13/long-running-tasks-and-threads/ – Lummo Mar 27 '14 at 10:44
  • @HenkHolterman How do you know that ThreadPool creates new Thread at a max 2 per second? Where it can be configured? – Alexander Vasilyev May 06 '15 at 15:12
8

Marcs suggestion is the way I would do it. But if you tasks take longer than one second and the clients sends a request per second the queue would steadily increase.

In that case I would use one server as facade which gets all requests from the clients and send responses back to them in an asynchronous manner.

The server would put all requests in a message queue which is read by several other servers. Those servers handle the requests and put the response in another message queue which is read by the first server.

Another solution would be to use a load balancing server.

jgauffin
  • 99,844
  • 45
  • 235
  • 372
4

You appear to be building a server which will service thousands of concurrent requests, each long-running in terms of minutes to hours.

Typically, make thread workloads short enough to complete at most within a few seconds. Anything longer, you'll start hogging server resources and seriously affect your server's scalability. Having tens of thousands of threads block on long-running operations, or doing those long-running operations simultaneously, will definitely kill your scalability.

Not sure how much CPU time you're consuming in each long-running. This will affect your design, e.g.:

If each long-running is primarily blocking on I/O, you may use one thread to wait on overlapped I/O or I/O completion port, then wake up new threads to process completed I/O (up to a throttled limit). You'll need to have a limit number of threads to service the waiting connections.

If each long-running operation waits for other operations to complete, consider Windows Workflow Foundation.

If each long-running operation consumes CPU, you don't want too many of them to be running at any one time or it will thrash your server. In this case, use MSMQ and/or TPL to queue tasks and make sure only a few are running at the same time.

In all of these, it seems that you are keeping the client connection open. The worst thing to do is to keep one thread blocking for each connection. You'll need to implement thread-pooling strategies in order to use only a limited number of threads to service all outstanding connections.

Stephen Chung
  • 14,497
  • 1
  • 35
  • 48