[MUD-Dev] Multi-threaded mud server.

Ola Fosheim Grøstad <olag@ifi.uio.no> Ola Fosheim Grøstad <olag@ifi.uio.no>
Tue May 18 21:19:55 CEST 1999


Caliban Tiresias Darklock wrote:
> So if you have two players online, each with one character, each on a
> different thread of a multiprocessor machine, and they have a conversation
> -- don't you have to context switch twice with every statement? The current
> player says something, which needs to go to the other player, so you
> context-switch to the other thread to talk to player two, send him the
> information, and then tell the original thread that you completed the
> action successfully, which is yet *another* context switch. So your context
> switching immediately goes through the roof, doesn't it?

First of all, traditionally academics don't care about constant efficiency
issues (10 times faster is not enough, it has to be at least ^2 faster :).
And of course, until recently multiprocessing was an academic discipline,
kindof. Those guys only care about elegance and simulation/modelling beauty,
not speed improvements.

I am not an expert, but maybe a simple example could be good for discussion?
I think it could be more like this (Assuming you use monitors for
protection, that means semaphores in object methods. Also assuming that you
do not use distributed multiprocessing, which threading implies. (sketchy)):


class Somebody {
protected:
    semaphorestuff mutex;
    semaphorestuff wakemeup;

    bool private_wakeup(){
	bool was_waked_up = false;

	mutex.down();
	if (wakemeup.is_down()){
	    wakemeup.up();	// makes brainpower runnable
	    was_waked_up = true;
	}
	mutex.up();
	return was_waked_up;
    }

public: 

    bool wakeup(){
	forbid();
	bool b = protected_wakeup();
	permit();
	return b;
    }


    hearing(sender,text){
	forbid();	// only one process may access the object
	
	if (private_wakeup())
	    add_client_buffer(sender.name() + " wakes you up");

	add_client_buffer(sender.name() + " tells you " + text);

	permit();	// ok, now somebody else may access it
    }


    process Somebody::brainpower() {
        forever{
	    something = sleep_until_I_sense_something();

	    if (something == feelingsleepy) {
		wakemeup.down(); // makes brainpower sleeping
		forbid();
		r = thisroom;
		permit();
		r->tell_everybody(name() + " wakes up.");
	    }

 	    if (something == command_tell_somebody){
		something.who.hearing(this,something.message);
	    }
	}
    }
};

I probably should add more mutex stuff :P, but as you see you don't have to
context switch. Each process (not unix-process, but a thread) will simply
fill up other object's buffers directly and forget about them. If you don't
then you can be pretty sure that your system will deadlock in a way which
will give you some headeaches. However, if you do producer-consumer stuff
using a unibuffer/port (rendezvous style) THEN there will certainly be a lot
of context switching.

However, I think that with a little effort you can actually get a lot of the
readability advantages of threading with coroutines (or other nonpreemptive
approaches) without the semaphores. Unfortunately C and C++ doesn't support
that. However, on some C platforms it requires almost no assembly code to
add the feature. I'd love to use threads or coroutines in the loginprocess
(sketchy example of coroutines);

class Login : Coroutine { 
    connection c;
    start(){
	do{
	    do{
	    	while (!c.put_line_nonblocking("Enter login name:"))
		    main.resume();

	    	while(!(name = c.get_line_nonblocking())) main.resume();

		main.resume();

	    } while(!legal_name(name));

	    if(name_exists(name)){

	    	while (!c.put_line_nonblocking("Enter password:"))
		    main.resume();

	        while(!(pwd = c.get_line_nonblocking()))
		    main.resume();

	      	}
	    } else create_character(name,&pwd);

	} while(!a = find_account(name,pwd));		

	a->attach_connection(c);
	main.readyaccount = a;
    }

    create_character(n,p){
	get some stuff...
	main.resume();
	get some more stuff...
	main.resume();
	etc...
    }
};


class Main : Coroutine {
    connection_list connlist;
    account_list acclist;
    object_list runninglist;

    Account *readyaccount;

    start(){
	forever{
	    while(c = new_connection()){
	        connlist.add(l = new Login(c));
	        l->start();
	    }

	    readyaccount = 0;
	    forall l in connlist with nonempty buffer { 
		l.resume;
		if(readyaccount) {
		    connlist.remove(l);
		    delete l;
		    acclist.add(readyaccount);
		    readyaccount->start();
		    readyaccount = 0;
		}
	    }

	    forall a in acclist who are ready { a->resume(); }

	    forall o in runninglist { o->resume(); }
	}
    }

} main;


> For example, you
> might have a "login" thread which handles everyone connected but not logged
> in, a thread which handles combat, a thread which handles character
> creation, etc.

Yeah, but then you loose the advantage of parallelism, which for mud
programming is readability IMO. (+ Decoupling. Modularity.)

class Login : Threaded { 
    connection c;
    start(){
	do{
	    do{
	    	c.put_line_blocking("Enter login name:");
	    	name = c.get_line_blocking();
	    } while(!legal_name(name));

	    if(name_exists(name)){
		c.put_line_blocking("Enter password:"));
		pwd = c.get_line_blocking();
	    } else create_character(name,&pwd);

	} while(!a = find_account(name,pwd));		

	a->attach_connection(c);
	a.start();
    }
};

To do the login process without this you will have to implement a
statemachine yourself. Which is usually somewhat ugly as you probably have
discovered :).  If you are going to waste your brain on syncing, make sure
you get the advantages! What I have done myself is rather clumsy and
involves switch statements.  Probably the same speed or faster than
coroutines, and faster than threading, but really... How much time is spent
on login anyway?  What is worrying is portability and deadlock issues. Of
course, if you go for a pure event-handling system, then you get something
that looks like coroutines, sortof.

> Ideally, you would set this up so most of your common operations are in the
> same thread, while progressively less common actions get separated out into
> threads which can be put to sleep entirely when they're not needed. A login
> thread, for example, only needs to be active when someone is sitting at the
> main login screen to log in; if there are long periods during which no one
> logs in, the thread can be put to sleep during those times.

Yes, but that happens automagically because you listen to the socket with
blocking. Context switching at that rate isn't too bad. Your computer are
already doing more heavy context switches, more frequently. A thread switch
only involves saving and loading the registerfile? A couple of hundred
cycles on a pentium if done right. A good scheduler will let you control the
responsiveness of the process, to prevent it from waking up all the time.

There are some other situations that can get bad when you use extensive
threading as a paradigm. Threaded login seems to be a good idea, especially
if you have some hairy server-client handshaking. IMO.

--
Ola Fosheim Groestad,Norway      http://www.stud.ifi.uio.no/~olag/


_______________________________________________
MUD-Dev maillist  -  MUD-Dev at kanga.nu
http://www.kanga.nu/lists/listinfo/mud-dev




More information about the mud-dev-archive mailing list