Re: New stackless proposal

"Mark Hahn" <[email protected]>
Newsgroups gmane.comp.lang.prothon.user
Message-ID <006401c467ce$0f02eb70$0d01a8c0@MarkVaio>
Christian Tismer wrote:

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

Yes I would like to do pre-emption.  Granularity depends on how complex the
threadMgr logic is.  If we do efficient C code for switching and only run
threadMgr methods rarely for guidance then granularity could be fine.  If
the threadMgr method runs at switching time or something else bad like that
then granularity would be coarse.

>> 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.

It was hard enough to describe the details for now.  Especially since the
proposal is still changing.  If it settles down I might be able to do better
docs.

You should have read the overall proposal before replying.  :-)

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

It means that the start() method has been called and the function has not
terminated.

> 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?

Ok.  Will do.

>>    newThread = Thread(funcObject, threadMgr = curThreadMgr())
>>    Thread.start(threadMgr = None)      # (*6)
>>
>>>> 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.

A thread is just a function that runs.  What could be simpler than that?   I
think you are maybe assuming things about my threads that aren't there.

When you pointed me to greenlets before all I saw was
implementation-specific stuff for the Python stack.  I don't know how to
translate that.

>> 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. :-)

I just did that to make it easy to manage.  The objects have to be stored
somewhere.  I'm open to alternatives.  An object doesn't have to be in the
hierachy obviously, it can be moved somewhere else, it just won't run unless
it is in the hierarchy.

>> 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.

OK, I didn't know what tuning params were needed.  I just thought there
might be some.  It's ok with me to not have any.

>> 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?

I guess we'll have a fixed single scheduler object that can be overridden.
You tell me.

> 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.

I currently have the python-type yield you are talking about implemented in
Prothon.  We can decide whether to keep this or switch to the co-routine
based yield after we see how well each works.  Both will be running at the
same time.

>> 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) ?

So your channel.transfer() works without a value also?  If so then I'll make
yield() work just like yield(value).  I thought a value was always needed.

>> 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.

If we use co-routines for generators then using the yield keyword for both
makes sense.  This idea seemed like a great unifying idea.  If we don't
unify generators with co-routines then we can use the same idea for the
yield function in the proposal but use a different keyword.

>> 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.

Yes, transfer would be good if we don't go with yield.

>> 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.

Where would I pass this argument to?

> 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.

It is a confusing paragraph you can ignore.  It just means you can change
the current ThreadMgr which moves the current Thread to the ThreadMgr.

> 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.

So you send the kill exception right away?  That is unsafe.  You cannot send
a kill exception when the thread is in the middle of protected code or some
resource could be left locked forever.  That is why I have the lock-block
and wait to send the kill until the code is outside of the block. This is
how ADA works.

 I don't care what happens on a time-out.  Once a time-out happens we could
send the time-out exception to the kill sender and safely undo the kill
attempt, if we wish, or we could still send the kill if it ever final gets
out of the lock-block.  We do need to keep running the thread as long as it
is in the lock-block so it can unlock the resource it is waiting on.

>> 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.

We have to have these methods for the OS threads.  I see no reason to not
have them for the other threads also.  It is no effort to implement and it
doesn't harm their functionality.  Correct me if I'm wrong.

Also I really don't understand this simpler model.  Can you please try to
explain what can be simpler?

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

I mean it works the same to the user, the programmer, not that it is
implemented the same.  See the tutorial if you don't know how it works.

>> 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?

As I said, I already have the Python implementation in the current Prothon
(except I use gen instead of def).  I will leave it there.  We can compare
the two and see which is better.

> 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?

My proposal has the implementation that explains the answer to your
questions.  The first next() call creates and calls the generator thread and
then calls yield to get the first value.  Niether thread will have to
reconstruct any stack state.  Both threads will have their own stacks
existing the whole time they run.  The generator stack will run until it
terminates and then it's stack will be destroyed.  The calling stack will be
the normal stack that exists before and after the gen is used.

> 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.

My proposal DOES have two stacks and a real switch.  Each thread has its own
stack and the threadMgr is a real switch.  What can I do to explain my
implementation better?

Or by real stack do you mean an OS stack?  I thought we were implementing
stackless here.

By my count I have three things to change based on your suggestions above
and several maybes.  You are asking for something simpler than threads but I
don't understand what that something would be.  I think maybe this is just a
matter of semantics.  You are suggesting we use a Python-type generator
which Prothon already has and I'm suggesting we wait and see which works
better.

I will implement the proposal on a trial basis with the method "transfer()"
instead of "yield()" so it doesn't conflict with my "yield" keyword already
implemented.  This will actually match what you want for generator.

So we are actually in pretty good agreement on all this.
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.