Re: New stackless proposal

Christian Tismer <[email protected]>
Newsgroups gmane.comp.lang.prothon.user
Message-ID <[email protected]>
Mark Hahn wrote:

> http://prothon.org/wiki?pagename=StacklessLanguageFeatures

Hmm, quite a bulk 'o stuff.

Ok, my first question is: What kind of granulatiry do you
try to achieve, and do you support pre-emptive multitasking?

...

> In order to start the OS thread running, you first have
> to call the OSThread object to arm it. This saves away
> the calling arguments so it can be started later. Then
> you run .start() to start it running. Usually you can
> do this in one statement.

Lots of details here. What I need is the overall picture.

> O>> thread1 = thread1(10)  # call thread1 to "arm" it(*1,5)
> O>> thread1
> <OSThread:41fc3a2:armed>
> O>> thread1.start()        # these two could have been thread1(10).start()
> thread1 is running
> O>> thread1
> <OSThread:41fc3a2:running>
> O>>
> thread1 is stopping

What does "running" mean?

> The Thread object is a light-weight thread that is managed
> by the Prothon intepreter instead of the OS. It cannot
> pre-empt C code or blocked IO like the OS threads can,
> but it does not consume the large amount of RAM that an
> OS thread does and the switching time is much faster. You
> can use maybe a thousand Thread objects compared to maybe
> a hundred OSThread objects in a typical system.

No, if you do it right, the factor is at least 100.
You can do 10.000 to 100.000 threads instead of a hundred.
A thread is typically less than 100 bytes.

> You create a Thread object using the same constructor/wrapper
> technique but it also adds a ThreadMgr attribute. You can
> assign the Thread to any ThreadMgr (which we will learn more
> about later), but it defaults to the current one. Later, you
> arm the Thread by calling it. Finally, you start it by
> calling it's .start() method on it, at which time you
> get another chance to assign a ThreadMgr.

The ThreadMgr is something like the thread's controller?
Why this difficult in the first place? Give it a default
controller that does what the user expects, and provide
him with an interface to change the default?

>    newThread = Thread(funcObject, threadMgr = curThreadMgr())
>    Thread.start(threadMgr = None)      # (*6)
> 
> O>> def thread1(seconds):
> ...    print 'thread1 is running'
> ...    Sleep(seconds * 1.0)
> ...    print 'thread1 is stopping'

You are even more into threads than I am. Please, have a look
into greenlets, to get an idea what the minimum is. Threads
are not a basic building block. They are an artificial
construct. The industry made us believe that this is what
we need. But I doubt it is. The basic thing is something
that runs when you want it to run, and that stops when
you want it to stop. Sleeping for some time comes far, far
later.
I made the same mistake. There is no such something liek a task,
in the first place. Things begin with a piece of code, and your
decision when to let it run. Start small, much smaller, and
let people play.
Don't make big proposals before you have played with it.
You will find yourself overspecifying way too early.

> How the objects work
> All code running at all times in Prothon has a
> current OSThread, ThreadMgr, and Thread object
> active. The built-in functions curOSThread(),
> curThreadMgr(), and curThread() can be called
> at any time to retrieve these objects.

Ok.

> The current OSThread and Thread are self-explanatory.
> They are the threads currently being executed. The
> current ThreadMgr is the object that controls the
> switching and queuing of Thread objects. A ThreadMgr
> object is what is called a channel in Christian's
> stackless and what I called a gate or coupler in
> my earlier proposals.

Something I can understand.

> The OSThread, ThreadMgr, and Thread objects make up
> a proper containment hierarchy. Each OSThread can
> have multiple ThreadMgr objects and each ThreadMgr
> object can have multiple Thread objects. Each object
> in one level of the heirarchy can only be contained
> in one object of the next level up.

I understand the idea of the containment. I do not
understand how this must be a heirarchy at all. My
tasklets don't have that; they are anarchistic. :-)

> Each ThreadMgr will queue up the different Thread
> objects waiting to run in that ThreadMgr. For each
> Thread it will keep track of what it is waiting on.
> It will use any tuning parameters specified in the
> ThreadMgr attributes.

I don't get the tuning issue. Channels are simple
constructs which queue up the tasklets which are waiting.
There are higher level constructs, like tasklets waiting
on multiple channels (and these are *needed*, finally),
but not on this primitive level.

> The different ThreadMgr objects in an OSThread will be
> scheduled for execution by the interpreter based on
> tuning parameters also kept in the ThreadMgr attributes.

I understand the equivalence with channels and their
flags for scheduling decisions.
Question: Who will do the scheduling?

> The different OSThread threads will be scheduled by the OS of course.

Ah.

> The yield function
> 
> Having a current ThreadMgr in each Thread means that
> you can use just the simple yield function instead
> of the traditional channel.transfer() operations.

Are you addressing the Python yield statement?
This can even be expressed with a non-stackless
python, and is therefore not worthy being addressed
here. A yield is just for simple generators.
You should not need channels (ThreadMgrs) for this.

> The yield function will need to be well-known by the
> interpreter as the current threads and ThreadMgr will
> need to be well-known. Even though yield is built-in
> it will really be a method of the current ThreadMgr
> by default.

It would be helpful if you could express your intent,
instead of declaring how things will work. In this case,
I don't see the benefit of this generalization.

...

> The yield function by itself means to just temporarily
> give up control to any other thread. If no other
> threads have much to do then control may come back
> right away. This is good to use in a loop when you
> are doing nothing useful.

Am I right that you don't want to model the Python yield?
This one is like an ordinary function call, which just
happens not to clear its state. I think it is a good
idea to treat simple yield like a function call, that
returns to its invoker, whatsoever.

> yield(value)
> 
> This should look familiar as a generator yield or one-half
> of one-way co-routines. It means to give up control to any
> other thread waiting on this ThreadMgr with another
> matching newValue = yield(recv = True) statement. The value
> this gives will be taken by the matching statement in the
> other thread.

Why should yield() be any different from yield(value) ?

> newValue = yield(recv = True) # (*3,9)
> 
> This matches the one above for one-way co-routines or
> generators. It will block until another thread on
> this ThreadMgr executes a yield(value) and gives a
> value for this to take.

This is a yield into the other direction?
I think this is really no good idea.
Yield is a generator statement, skewed towards
"return a value and wait to get resumed".
Using the same keyword for the other part looks
counter-intuitive.
Please compare the old ICON idioms. They had suspend
(aka yield) and resume.

Anyway, this is al just syntactic sugar to express this
skewed, asymmetric idea of generators. I like coroutines
much better.

> newValue = yield(value, recv = True)
> 
> This is a two-way matching yield. This will block until
> another thread on this ThreadMgr executes the same
> kind of two-way statement. They will then exchange values.

Overloading, of course. What you want to express is a transfer()
from Modula II. I think it is fair to use this name.

> If any of the yield calls above have an argument
> with = otherThread present, then only the Thread
> object specified in otherThread will be used to match
> this thread instead of letting the ThreadMgr choose
> any thread. As an example, the simple yield(with = otherThread)
> will stop the current thread and resume otherThread. If
> otherThread is not running then you will get an exception
> when using it in the yield call.

I'm not sure if this is what you want. The explicit spelling
of a target thread is exactly what the Limbo people tried to
explictly avoid. The reason to have a channel at all is about
not having to name the other thread!
So I'd prefer to leave the rendevouz points as anonymous
as possible. This can be controlled by direct argument passing
easily. I see no point to make it overly explicit to the "slave".

> ThreadMgr methods and functions
> 
> As I said before the yield built-in function is really a
> method of the current ThreadMgr. You can also create new
> ThreadMgr objects to call yield on or to make the current
> one. The concepting of making a ThreadMgr the current one
> is really the moving of the current Thread from the current
> ThreadMgr to a new ThreadMgr. There is no difference.

This is nothing that my brain can understand without
further introduction.

...

> The current Prothon Mutex object will be replaced by a lock
> keyword. It would be of the form: lock obj: lock-block. Like
> Java, obj can be any object and becomes a mutex in addition
> to it's normal meaning. This allows you to use the object
> being protected, such as a list or dictionary, as obj,
> which is very intuitive.

I see good points in doing this, but again, you are missing to
explain the reasons.
Actually, I don't care quite much, since I'm not interested in
the syntax, but in how efficiently the locks can be implemented.

> No two Thread type threads can enter lock-block at the same
> time based on the lock on obj. The ThreadMgr will block one
> of them. Think of lock-block as being a try-block with an
> unlock for the lock in an invisible finally-block (this is
> how it would be implemented).

I'm wondering if my current scheme is too simple, or if your's
is overly complicated? Is this due to the fact that you allow
completely concurrent processing, maybe?

> Note that this lock keyword allows Thread to have a kill()
> method. Thread.kill() will kill a thread by throwing a
 > ThreadKill exception in the thread, but only after waiting
> until the thread has exited any lock-blocks it might be in.
> This means it might take arbitrarily long or forever to kill
> a thread. Therefore Thread.kill() will have an optional
> time-out argument with a matching exception.

And what should happen after the Thread.kill() has surpassed the
timeout? Will you give up, then?
I have a much simpler strategy:
Whenever I have to kill a thread, I'm sending TaskletKill
exceptions until infinitum. This is no worse than current
Python behaviro: If a thread does not respond to certain
exceptions, there is nothing that can be done.

> Existing OS thread methods (*15)
> 
> The existing Prothon Thread methods sleep(), running?(),
> and join() will be implemented on both new thread types.
> Logic will be implemented to make the different thread
> types compatible with each other when using these methods
> on Thread. For example when calling Thread.sleep(),
> if all Threads are idle then an OSThread.sleep() will be called.

Sounds doable, but as said, I'd prefer to see a simpler
model, which is not trying to mimick the real thread
paradigm, but less.

> Implementing the gen keyword
> 
> The gen keyword works the same as before (*13) except
> that yield is a function instead of a keyword.

???

> It is now implemented using co-routines though.

Sound a little bad. Everything implemented using coroutines
should need to be implemented using coroutines. Generators have
been proven to be implementable without coroutines. So, unless
you can prove this is more efficient, I say it is less efficient
than a native, stack-based generator implementation.

> The gen keyword creates a Thread object. The for loop
> statement execution arms it and calls iter_() on it
> creating a special ThreadIter object (which extends Thread).
> ThreadIter is a co-routine that translates next() calls
> into yield() calls.

Can you explain why this is so? Is this just an implementation
detail, or is it a design decision?

Special case:
If you have a generator, and an iterator that runs through this
generatorm, like in

def lineGenerator():
     while condition:
         parse_some_lines
         yield some_lines

def lineConsumer(gen):
     for line in gen:
         handle line

Who will be the caller, and who will be the callee?
In other words: Who will have to reconstruct its stack
state all the time, and who will be just served by a return?

For my own current optimizations, I have the slight impression
that it makes things cheaper to have the iterator stay on
the stack, and the generator is to be re-run all the time.
Although I thing that real efficiency can only be done with
two real stacks and a real switch.

I hope I created a bit of confusion -- chris

-- 
Christian Tismer             :^)   <mailto:[email protected]>
Mission Impossible 5oftware  :     Have a break! Take a ride on Python's
Johannes-Niemeyer-Weg 9a     :    *Starship* http://starship.python.net/
14109 Berlin                 :     PGP key -> http://wwwkeys.pgp.net/
work +49 30 89 09 53 34  home +49 30 802 86 56  mobile +49 173 24 18 776
PGP 0x57F3BF04       9064 F4E1 D754 C2FF 1619  305B C09C 5A3B 57F3 BF04
      whom do you want to sponsor today?   http://www.stackless.com/
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.