semantics of concurrent program depends on -O level, -f[no-]omit-yields

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
17 messages Options
Reply | Threaded
Open this post in threaded view
|

semantics of concurrent program depends on -O level, -f[no-]omit-yields

Johannes Waldmann-2
Dear Cafe,

I am surprised by the behaviour of the program below
(the interesting property is whether it will output "foo").

Behaviours (plural) actually: it seems to depend
on optimisation level, on omit-yields,
and on very small changes in the source code:

It does print (mostly), when compiled with -O0.
It does not, when compiled with -O2.
With -O2 -fno-omit-yields, it will print.
With -O0 -fno-omit-yields, and when I remove the two newTVar
in the beginning, it will mostly not print.

How come?

These differences already occur with the
last two lines replaced by "forever $ return ()",
so the STM stuff may be inessential here.
But that's the context where I came across the problem.

- J.W.


import Control.Concurrent.STM
import Control.Concurrent ( forkIO )
import Control.Monad ( forever )
import System.IO

main = do

  atomically $ newTVar "bar"
  atomically $ newTVar False

  forkIO $ putStrLn "foo"

  x <- atomically $ newTVar False
  forever $ atomically $ writeTVar x True

_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.
Reply | Threaded
Open this post in threaded view
|

Re: semantics of concurrent program depends on -O level, -f[no-]omit-yields

Brandon Allbery
This is undoubtedly nothing more than timing issues. Remember that the main thread exiting will kill the entire process, automatically killing all other threads as side effect. So the question is how much the thread manages to get done before that happens.

If you disable output buffering, you may find that "f" or "fo" sometimes gets written before process exit.

On Thu, Nov 29, 2018 at 1:43 PM Johannes Waldmann <[hidden email]> wrote:
Dear Cafe,

I am surprised by the behaviour of the program below
(the interesting property is whether it will output "foo").

Behaviours (plural) actually: it seems to depend
on optimisation level, on omit-yields,
and on very small changes in the source code:

It does print (mostly), when compiled with -O0.
It does not, when compiled with -O2.
With -O2 -fno-omit-yields, it will print.
With -O0 -fno-omit-yields, and when I remove the two newTVar
in the beginning, it will mostly not print.

How come?

These differences already occur with the
last two lines replaced by "forever $ return ()",
so the STM stuff may be inessential here.
But that's the context where I came across the problem.

- J.W.


import Control.Concurrent.STM
import Control.Concurrent ( forkIO )
import Control.Monad ( forever )
import System.IO

main = do

  atomically $ newTVar "bar"
  atomically $ newTVar False

  forkIO $ putStrLn "foo"

  x <- atomically $ newTVar False
  forever $ atomically $ writeTVar x True

_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.


--
brandon s allbery kf8nh

_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.
Reply | Threaded
Open this post in threaded view
|

Re: semantics of concurrent program depends on -O level, -f[no-]omit-yields

Johannes Waldmann-2
On 11/29/18 7:48 PM, Brandon Allbery wrote:

> main thread exiting will kill the entire process,

The main thread does "forever $ something".

The process does not exit (I am not getting
the console prompt). I observe that the process
either prints and then hangs, or it hangs immediately.

- J.
_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.
Reply | Threaded
Open this post in threaded view
|

Re: semantics of concurrent program depends on -O level, -f[no-]omit-yields

Bryan Richter-2


On Thu, Nov 29, 2018, 20:51 Johannes Waldmann <[hidden email] wrote:
On 11/29/18 7:48 PM, Brandon Allbery wrote:

> main thread exiting will kill the entire process,

The main thread does "forever $ something".

The process does not exit (I am not getting
the console prompt). I observe that the process
either prints and then hangs, or it hangs immediately.

Printing to the console still isn't a great test to see if something has "run", because of buffering that seems to behave unintuitively in these situations. Maybe try flushing stdout within the forked thread, to ensure the runtime is doing what you think it's doing?

_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.
Reply | Threaded
Open this post in threaded view
|

Re: semantics of concurrent program depends on -O level, -f[no-]omit-yields

Brandon Allbery
I also note that the "forever" is in fact writing to a TVar. I'd be curious as to whether it's retrying for some reason, possibly related to the "lost" TVars confusing the STM machinery. I seem to recall it has some infelicities currently; and I have no idea how (or if) STM retries interact with thread yielding.

On Thu, Nov 29, 2018 at 2:25 PM Bryan Richter <[hidden email]> wrote:


On Thu, Nov 29, 2018, 20:51 Johannes Waldmann <[hidden email] wrote:
On 11/29/18 7:48 PM, Brandon Allbery wrote:

> main thread exiting will kill the entire process,

The main thread does "forever $ something".

The process does not exit (I am not getting
the console prompt). I observe that the process
either prints and then hangs, or it hangs immediately.

Printing to the console still isn't a great test to see if something has "run", because of buffering that seems to behave unintuitively in these situations. Maybe try flushing stdout within the forked thread, to ensure the runtime is doing what you think it's doing?
_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.


--
brandon s allbery kf8nh

_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.
Reply | Threaded
Open this post in threaded view
|

Re: semantics of concurrent program depends on -O level, -f[no-]omit-yields

Johannes Waldmann-2
> ... try flushing stdout within the forked thread,

I did. The behaviour is still as described:
depends on -O0/2, [no]omit-yield,
and small changes in the source.

While I agree with the general point -
why would I need to hFlush after putStrLn?
hGetBuffering stdout  tells me it's  LineBuffering,
and putStrLn does write a line?

- J.
_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.
Reply | Threaded
Open this post in threaded view
|

Re: semantics of concurrent program depends on -O level, -f[no-]omit-yields

Brandon Allbery
The idea is that putStrLn iterates putChar over the String, then putChar '\n'; so thread scheduling would be more obvious with individual characters being output instead of a single flush triggered by the final putChar.

On Thu, Nov 29, 2018 at 2:37 PM Johannes Waldmann <[hidden email]> wrote:
> ... try flushing stdout within the forked thread,

I did. The behaviour is still as described:
depends on -O0/2, [no]omit-yield,
and small changes in the source.

While I agree with the general point -
why would I need to hFlush after putStrLn?
hGetBuffering stdout  tells me it's  LineBuffering,
and putStrLn does write a line?

- J.
_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.


--
brandon s allbery kf8nh

_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.
Reply | Threaded
Open this post in threaded view
|

Re: semantics of concurrent program depends on -O level, -f[no-]omit-yields

Johannes Waldmann-2
> so thread scheduling would be more obvious with individual
> characters being output instead of a single flush triggered by the final
> putChar.

Yes but in my example program, there is no contention for stdout,
as only one thread is using it.

I am inclined to enter this into the GHC issue tracker
as it seems there's no obvious explanation,
and "lost TVars confusing the STM machinery" was mentioned.
Do you mean that this a known thing? Searching the tracker
for "lost TVar" does not turn up anything.

- J.W.
_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.
Reply | Threaded
Open this post in threaded view
|

Re: semantics of concurrent program depends on -O level, -f[no-]omit-yields

Brandon Allbery
What does this have to do with contention for stdout? Thread switching is unrelated; seeing individual output operations just gives more hints about when the thread switches happen. And with -fno-omit-yields it presumably can happen when putChar is evaluated, not because of I/O but because of function entry.

On Thu, Nov 29, 2018 at 2:49 PM Johannes Waldmann <[hidden email]> wrote:
> so thread scheduling would be more obvious with individual
> characters being output instead of a single flush triggered by the final
> putChar.

Yes but in my example program, there is no contention for stdout,
as only one thread is using it.

I am inclined to enter this into the GHC issue tracker
as it seems there's no obvious explanation,
and "lost TVars confusing the STM machinery" was mentioned.
Do you mean that this a known thing? Searching the tracker
for "lost TVar" does not turn up anything.

- J.W.


--
brandon s allbery kf8nh

_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.
Reply | Threaded
Open this post in threaded view
|

Re: semantics of concurrent program depends on -O level, -f[no-]omit-yields

Sven Panne-2
In reply to this post by Johannes Waldmann-2
Am Do., 29. Nov. 2018 um 19:43 Uhr schrieb Johannes Waldmann <[hidden email]>:
I am surprised by the behaviour of the program below
(the interesting property is whether it will output "foo").

Behaviours (plural) actually: it seems to depend
on optimisation level, on omit-yields,
and on very small changes in the source code: [...]

IMHO there is nothing very surprising here: You have 2 threads with no synchronization between them whatsoever, so you get what you deserve: Undefined behavior. :-) This is the behavior you get in basically all programming languages/execution environments I know of, *unless* they make a very strong guarantee about their scheduling behavior (whichis very rare, for good reasons). Do we have such a guarantee somewhere in the GHC/base documentation? I don't think so, but if we had, I would be interested to see a reference to that.

Cheers,
   S.

_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.
Reply | Threaded
Open this post in threaded view
|

Re: semantics of concurrent program depends on -O level, -f[no-]omit-yields

Ian Denhardt
In reply to this post by Johannes Waldmann-2
Quoting Johannes Waldmann (2018-11-29 13:42:42)

> These differences already occur with the
> last two lines replaced by "forever $ return ()",
> so the STM stuff may be inessential here.
> But that's the context where I came across the problem.

There's another thread right now with a subject line of "Timing out a
pure evaluation of an expression I did not write myself," that seems
like it might be related: I would expect forever $ return () to not
allocate, which would mean it would never hit any yields, and thus never
be rescheduled, and hogging the CPU.

I've been able to reproduce your results, and if I change the last line
to:

    forever $ do
        yield
        atomically $ writeTVar x True

..it always prints -- so the culprit is definitely a failure to yield.

-Ian
_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.
Reply | Threaded
Open this post in threaded view
|

Re: semantics of concurrent program depends on -O level, -f[no-]omit-yields

Johannes Waldmann-2
In reply to this post by Sven Panne-2
> IMHO there is nothing very surprising here: You have 2 threads with no
> synchronization between them whatsoever, so you get what you deserve:
> Undefined behavior. :-)

Well, yes.

It feels as if the scheduler is mighty unfair here
(delaying the printing indefinitely)
but apparently it is allowed to do so - mainly since there is
no specification that would require otherwise.

But then (seconding your question) what guarantees *do* we have?
For a single-threaded program, it would certainly not be OK
to execute "main = print ()" as "block immediately"?
But when we  forkIO  this, then it can happen?

Possibly related: discussion about (state of formal
specification of) GHC RTS memory model at
https://mail.haskell.org/pipermail/ghc-devs/2018-November/016583.html

- J.W.
_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.
Reply | Threaded
Open this post in threaded view
|

Re: semantics of concurrent program depends on -O level, -f[no-]omit-yields

Johannes Waldmann-2
In reply to this post by Ian Denhardt

>     forever $ do
>         yield
>         atomically $ writeTVar x True
>
> ..it always prints -- so the culprit is definitely a failure to yield.

A-ha.

So my implicit assumption was
that a run of the transaction manager (because "atomically")
is also a yield - but this example shows that it isn't.

If this is indeed the case, then this deserves to be mentioned
in the documentation of Control.Concurrent.STM ?

- J.W.
_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.
Reply | Threaded
Open this post in threaded view
|

Re: semantics of concurrent program depends on -O level, -f[no-]omit-yields

Sven Panne-2
In reply to this post by Ian Denhardt
Am Do., 29. Nov. 2018 um 21:54 Uhr schrieb Ian Denhardt <[hidden email]>:
[...] I've been able to reproduce your results, and if I change the last line
to:

    forever $ do
        yield
        atomically $ writeTVar x True

..it always prints -- so the culprit is definitely a failure to yield.

But even that is not enough from a specification POV: After the yield, the same thread might be schedule immediately again, and again, ... Or do we have some specification of the scheduler? I don't think so, but perhaps I'm wrong in this respect. If we have one, it has to state explicitly that the scheduling is fair in the sense that every runnable thread actually runs after a finite amount of time, otherwise you are in undefined land again...

The question where scheduling can actually happen is a totally different issue, and I don't know of a specification here, either. In GHC, this seems to be tied to allocations, but this is a bit brittle and unintuitive. To guarantee that you hit a scheduling point after a finite amount of time is easy in principle, e.g. do this on every backwards branch and on every function entry. But this has an associated cost, so we have a tradeoff here.

In general, I wouldn't worry too much about the semantics of unsynchronized threads, if you rely on this somehow, you will sooner or later enter a world of pain. Add e.g. thread priorities to the mix, and you will suffer even more, experiencing wonderful things like priority inversion etc.  :-P

_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.
Reply | Threaded
Open this post in threaded view
|

Re: semantics of concurrent program depends on -O level, -f[no-]omit-yields

Johannes Waldmann-2
Hi,

> the same thread might be schedule immediately again, and again, ... Or
> do we have some specification of the scheduler?

My working assumption is that the scheduler tries to be fair.
So all strange behaviour could be explained with the scheduler
not running at all, because threads weren't yielding.

> The question where scheduling can actually happen is a totally different
> issue, and I don't know of a specification here, either. In GHC, this
> seems to be tied to allocations, but this is a bit brittle and
> unintuitive.
Yes, especially if the compiler might (re)move allocations
due to some code transformations.

Given that, it now feels strange that the following *does* work:

main = do
  forkIO $ do threadDelay 1000000 ; putStrLn "foo"
  forever $ putStr ""

I am seeing the "foo" output. I expect the last line
to be non-allocating. But it does still yield? Why?

- J.W.
_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.
Reply | Threaded
Open this post in threaded view
|

Re: semantics of concurrent program depends on -O level, -f[no-]omit-yields

Ian Denhardt
Quoting Johannes Waldmann (2018-11-30 05:50:03)

> Given that, it now feels strange that the following *does* work:
>
> main = do
>   forkIO $ do threadDelay 1000000 ; putStrLn "foo"
>   forever $ putStr ""
>
> I am seeing the "foo" output. I expect the last line
> to be non-allocating. But it does still yield? Why?

putStr has to acquire a lock on stdout, so that's probably enough to
allow the scheduler to run.
_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.
Reply | Threaded
Open this post in threaded view
|

Re: semantics of concurrent program depends on -O level, -f[no-]omit-yields

Brandon Allbery
Hm, has that been optimized to output all at once? The implementation I recall is more or less mapM_ putChar, deferring the lock to putChar which never gets invoked because the list is empty.

Okay, just checked; it reserves the handle up front, and then the above implementation (albeit directly instead of via mapM_) is used only in the NoBuffering case, using an internal function that doesn't reserve. Which will complicate understanding what's going on, although my suggestion earlier about unbuffering output still applies.

On Fri, Nov 30, 2018 at 1:35 PM Ian Denhardt <[hidden email]> wrote:
Quoting Johannes Waldmann (2018-11-30 05:50:03)

> Given that, it now feels strange that the following *does* work:
>
> main = do
>   forkIO $ do threadDelay 1000000 ; putStrLn "foo"
>   forever $ putStr ""
>
> I am seeing the "foo" output. I expect the last line
> to be non-allocating. But it does still yield? Why?

putStr has to acquire a lock on stdout, so that's probably enough to
allow the scheduler to run.
_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.


--
brandon s allbery kf8nh

_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.