Questions regarding sockets and threading

Started by
4 comments, last by hplus0603 16 years, 9 months ago
I've scanned through the forums, read the FAQ and looked at some guides but I can't seem to find any sort of closure as far as this topic goes, thus, I am posting here regarding some of my concerns. First, let me explain my game design. Its a small tile-based 2-4 player game which I plan on having multiple instances of the game running as to support multiple concurrent games on a server. I plan to use TCP to do the client-server connection. I had initially planned to have one game instance per thread and then doing the sends and recvs within each game instance. But then I began to read some more and the idea of multithreading in dealing with clients and servers seems to be somewhat of a "hot topic", especially on these forums. In general, what kind of situations is multithreading a viable opportunity, and when is it not, should I even be concerned with multithreading at this point? If not, how do I handle which players are associated with which game instances? Any advice or improvement suggestions on the game processing idea would be greatly appreciated.
Quote:Q10) Should I spawn a thread per connection in my game code? A10) The short answer is "likely no." The longer answer is that, if you're using UDP, you should only need a single socket, from which you can read a packet, process a packet, repeat. If you're using TCP, you will need a socket per player, but the players are likely sharing and affecting a single world state inside the program, which needs to be protected by locking; thus, the extra threads would just end up blocking on locks anyway, so using select() and reading out what's there, then processing it, is recommended. Some users find "co-operative threads" or fibers to be a nice middle ground, because it gives some of the advantages of real threads (a stack per context) without the locking overhead (because yield is explicit).
If someone could also give a bit more insight or elaborate on the bolded portion, it would be much appreciated.
Advertisement
Co-operative threads, also known as Fibers, allow you to suspend execution in the middle of a function, and let some other function resume. They look a lot like regular threads, except you know exactly when the execution will suspend and resume, so any stretch of code between two suspending calls is implicitly a critical section (won't be pre-empted).

Thus, if you create an I/O fiber that does the following:

forever {  wait for input (using fiber)  process input}

Then you know that your fiber will be suspended, and another fiber scheduled, only during the wait; the "process input" will be done without any interference of other fibers on the same thread. The main benefit is that you can turn state machine-like code (with outstanding requests) into serial, easy-to-read code, that looks a lot like a thread, without paying the locking overhead.

However, the OS doesn't natively switch fibers on library or system calls; you'll have to make arrangements for doing that yourself, typically using overlapped/asynchronous I/O, and writing a fiber scheduler in your main function.


To answer your original question: If you run one game instance per thread, so the threads are data independent, it's fine to run them like that in a single process. The main danger is that crashing one game, will crash them all, so you may want to split the different game instances into different processes. However, if you listen on a single port for all the game instances, you can't really use a separate process per (unless you use fork() under UNIX after accepting), so threading is quite acceptable in that case.

enum Bool { True, False, FileNotFound };
In your case I'd run one thread per game.

Single threaded model is not problematic for up to 16-32 players. And since you have no shared resources, you don't need to worry about locking.

Quote:However, the OS doesn't natively switch fibers on library or system calls; you'll have to make arrangements for doing that yourself, typically using overlapped/asynchronous I/O, and writing a fiber scheduler in your main function.


Might want to look into Boost::ASIO. One locking option are strands, which lack of documentation, but perform the function of fibers transparently.

In addition, its IO Service can be used for more than just IO, with support for timers it allows for some pretty solid asynchronous design. I've found that implementing bandwidth throttling in a purely asynchronous manner extremely simplified the code, making it no more than a selection of which callback to use, with no need for locking.

And, if designed properly, allows for completely non-problematic scaling of single-threaded network code to be executed in multiple threads, with handlers using fiber-like synchronization to ensure callbacks are executed by a single thread at a time.

I won't vouch for efficiency, but so far I've had no scalability issues - the library uses the most suitable platform primitives to handle asynchronous aspects.

Unfortunately, the library is somewhat lacking in documentation beyond tutorials, so ability to stomach boost's syntax is required to deal with them.
Thank you both for your replies, they are very informative, however I still have an outstanding question regarding the appropriateness of multithreading.

From my original post:

Quote:what kind of situations is multithreading a viable opportunity, and when is it not


Additionally, how well do single threaded systems scale? Does multithreading increase the scalability of systems? What kind of "rules" are there for scalability?
Quote:Additionally, how well do single threaded systems scale? Does multithreading increase the scalability of systems? What kind of "rules" are there for scalability?


MT can increase scalability. But it doesn't out of box.

I use the following rules: <32 players, anything goes. <100 users - tight network loop, with some caution with regard to memory allocation and calls made. 100-250, several threads, either one per X clients, or by functional separation. 500+, well, lots of things start to matter there.

But these rules don't really exist, since they depend solely on the nature of traffic. A web server can have no problems handling hundreds of users with an architecture that wouldn't handle 10 in a game. Web applications might use bulky models, yet deliver solid performance.

Scalability of multi-threaded designs is limited by synchronization. If threads are completely independant (they don't share any resources), then you get almost infinite scalability - consider 500 web servers running in a data center. If you add 500 more, you have twice the performance.

But if these 500 servers read files from a single machine, then 499 servers will be running idle, while only one is serving data. Good and scalable multi-threaded design isn't easy, and a single mis-placed lock can negate any advantage. Lock as little as possible, duplicate resources where needed, share where possible, use lockless primitives if available and viable.
It's usually better to scale through multi-processing than multi-threading. That brings your data dependencies out in the open, and allows you to cluster onto any number of affordable processors, over a network.

The problem with multi-threading is that all the cores will be talking to the same memory (for SMP), so the memory bus becomes the bottleneck rather than the CPU. Especially with 32-core or 64-core CPUs (like Sun Niagara2), SMP is really running out of steam.

A few cores available might help for gross division, such as collision detection vs AI/pathfinding vs rendering (on a client), but trying to fine-grain multi-thread a multi-player server will likely run into more locking overhead than you gain in scalability. Multiprocessing, using whatever API you feel comfortable with (MPI, RPC, CORBA, etc) is likely better -- although, of course, some APIs perform better than others for these applications. I'd stay away from RPC and CORBA, for example :-)
enum Bool { True, False, FileNotFound };

This topic is closed to new replies.

Advertisement