www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - concurrency call to arms

reply John Belmonte <john neggie.net> writes:
This is actually not about war; rather the peace and prosperity 
of people writing concurrent programs.

(Andrei, I hope you are reading and will check out
https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement
considered-harmful/ and
https://vorpus.org/blog/timeouts-and-cancellation-for-humans/)

Recently I've been working with Trio, which is a Python async 
concurrency library implementing the concepts described in the 
articles above.  A synopsis (Python):

     with open_task_container() as container:
         container.start_task(a)
         container.start_task(b)
         await sleep(1)
         container.start_task(c)




The point is that tasks started in the container's scope will not 
live past the scope.  Scope exit will block until all tasks are 
complete (normally or by cancellation).  If task b has an 
exception, all other tasks in the container are cancelled.

What this means is that task lifetimes can be readily understood 
by looking at the structure of a program.  They are tied to 
scoped blocks, honor nesting, etc.

Similar for control of timeouts and cancellation:


completed in 10s
         reply = await request(a)
         do_something(reply)
         reply2 = await request(b)
         ...

These are novel control structures for managing concurrency.  
Combining this with cooperative multitasking and explicit, 
plainly-visible context switching (i.e. async/await-- sorry 
Olshansky) yields something truly at the forefront of concurrent 
programming.  I mean no callbacks, almost no locking, no 
explicitly maintained context and associated state machines, no 
task lifetime obscurity, no manual plumbing of cancellations, no 
errors dropped on the floor, no shutdown hiccups.  I'm able to 
write correct, robust, maintainable concurrent programs with 
almost no mental overhead beyond a non-concurrent program.

Some specimens (not written by me):

implementation in about 200 lines of code.  
https://github.com/python-hyper/h11/blob/33c5282340b61ddea0dc00a16b6582170d822d81/examples/trio-server.py

eyeballs" networking connection algorithm in about 150 lines of 
code.  
https://github.com/python-trio/trio/blob/7d2e2603b972dc0adeaa3ded35cd6590527b5e66/trio/_highlevel_open_tcp_stream.py

I'd like to see a D library supporting these control structures 
(with possibly some async/await syntax for the coroutine case).  
And of course for vibe.d and other I/O libraries to unify around 
this.

I'll go out on a limb and say if this could happen in addition to 
D addressing its GC dirty laundry, the language would actually be 
an unstoppable force.

Regards,
--John
Aug 16 2018
next sibling parent reply "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
On Thu, Aug 16, 2018 at 08:30:26PM +0000, John Belmonte via Digitalmars-d wrote:
[...]
 (Andrei, I hope you are reading and will check out
 https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/
 and https://vorpus.org/blog/timeouts-and-cancellation-for-humans/)
I read both articles, and am quite impressed by the revolutionary way of looking at concurrency. It provides a clean(er) abstraction that can be reasoned about much more easily than currently prevalent models of concurrency. Seems it would fit right in with D's message-based concurrency communication model. [...]
 These are novel control structures for managing concurrency.
 Combining this with cooperative multitasking and explicit,
 plainly-visible context switching (i.e. async/await-- sorry Olshansky)
 yields something truly at the forefront of concurrent programming.  I
 mean no callbacks, almost no locking, no explicitly maintained context
 and associated state machines, no task lifetime obscurity, no manual
 plumbing of cancellations, no errors dropped on the floor, no shutdown
 hiccups.  I'm able to write correct, robust, maintainable concurrent
 programs with almost no mental overhead beyond a non-concurrent
 program.
Indeed. It certainly seems like a promising step toward addressing the nasty minefield that is today's concurrent programming models. However, it would seem to require language support, no? It's going to be a tough sell to Walter & Andrei if it requires language support. (Though IMO it's worth it.) One potential problem point is C/C++ interoperability. Once you can call into the wild world of arbitrary C/C++ code that may spawn threads and do other low-level concurrent things, all bets are off and you can no longer provide the above guarantees. But we might be able to work around this with a mechanism similar to safe/ system/ trusted, to isolate potentially encapsulation-breaking code from code that's been vetted to not have unwanted concurrent side-effects. Then within the realm of "well-behaved" code, we can reap the benefits of the new concurrency model without being crippled by the potential of interacting with "badly-behaved" code. [...]
 I'd like to see a D library supporting these control structures (with
 possibly some async/await syntax for the coroutine case).  And of
 course for vibe.d and other I/O libraries to unify around this.
If this is possible to implement without requiring language support, that would be a major win, and would be much more likely to be acceptable to W & A.
 I'll go out on a limb and say if this could happen in addition to D
 addressing its GC dirty laundry, the language would actually be an
 unstoppable force.
[...] Not sure what you're referring to by "GC dirty laundry", but IMO, the GC gets a lot of undeserved hate from GC-phobic folk coming from C/C++. While there's certainly room for improvement, I think the GC ought to be regarded as one of those modern features that are essential to D's success, not the illegitimate step-child that everyone tries to avoid. It liberates the programmer from being constantly bogged down with the nitty-gritties of low-level memory management issues, and frees the mind to focus on actually solving the problem domain the program is intended to address. Just like this new concurrency model liberates the programmer from constantly having to worry about data races, deadlocks, and all the other nice things traditional concurrency models entail. ;-) T -- For every argument for something, there is always an equal and opposite argument against it. Debates don't give answers, only wounded or inflated egos.
Aug 16 2018
parent John Belmonte <john neggie.net> writes:
On Thursday, 16 August 2018 at 23:33:04 UTC, H. S. Teoh wrote:
 However, it would seem to require language support, no?  It's 
 going to be a tough sell to Walter & Andrei if it requires 
 language support. (Though IMO it's worth it.)
To implement scoped nursery and cancellation? I hope it could be done with libraries given D's flexibility. At the very least they could be prototyped with scope exits. async/await might need syntax. Part of async/await is just knowing what functions and call sites can context switch, so you can get that with decorators and clear library API. But the other part is compiler help-- e.g. any function with await must be declared async, enforced by the compiler. But I suspect a D library could do some compile time magic here too. One point is that the new control structures are valid regardless of how threads are implemented: OS threads, coroutines with implicit context switch, coroutines with explicit context switch, etc. What seems by far the most promising is the last one since it further simplifies reasoning about concurrent programs. And that's exactly what Trio + Python async/await provide.
 But we might be able to work around this with a mechanism 
 similar to  safe/ system/ trusted, to isolate potentially 
 encapsulation-breaking code from code that's been vetted to not 
 have unwanted concurrent side-effects.  Then within the realm 
 of "well-behaved" code, we can reap the benefits of the new 
 concurrency model without being crippled by the potential of 
 interacting with "badly-behaved" code.
It's an important point. As argued in the first article, as soon as parts of the program are spawning tasks ad-hoc, the benefits break down. Hence GOTO has been eliminated or neutered in most languages, using the author's analogy. Similarly D can use the approach of safe etc. so that we know what parts of the program will behave correctly on cancellation or exception.
 Not sure what you're referring to by "GC dirty laundry", but 
 IMO, the GC gets a lot of undeserved hate from GC-phobic folk 
 coming from C/C++.
I totally agree with the importance of a GC. I'm referring to GC stop-the world latency. E.g. Go language has a concurrent GC now at around 500 usec pause per GC and due to drop significantly more (https://blog.golang.org/ismmkeynote).
Aug 16 2018
prev sibling next sibling parent reply rikki cattermole <rikki cattermole.co.nz> writes:
After reading the article I can say, it isn't any better than async and 
await for dependencies. You still need an event loop.

The problem is that joining that happens at the end of that block needs 
to run the event loop for iterations until it completes. Which is 
wonderful if you're not doing real time like game development.

In essence you want a stack of state per thread, which uses the central 
event loop:

func():
	with x:
		spawn(foo)
		join(foo)
		endScope()
funcParent():
	with x:
		spawn(func)
		join(func)
		endScope()

If you don't have this, you will miss timers, window events and all 
sorts of things that could be very time sensitive which would be very 
very bad.

Because we have an event loop, we don't need a nursery! It comes free of 
charge. It also means we don't need that with statement... hang on that 
now becomes await and async! Just without the await (auto added in 
scope(exit), and compiler can merge them into a single function call ;) ).
Aug 16 2018
next sibling parent "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
On Fri, Aug 17, 2018 at 06:36:36PM +1200, rikki cattermole via Digitalmars-d
wrote:
 After reading the article I can say, it isn't any better than async
 and await for dependencies. You still need an event loop.
 
 The problem is that joining that happens at the end of that block
 needs to run the event loop for iterations until it completes. Which
 is wonderful if you're not doing real time like game development.
I don't see the problem. The event loop can spawn tasks into one main nursery, and each task can spawn subtasks into its own nurseries. The nursery in the task blocks until the all subtasks have completed, *but* that does not preclude other tasks in the event loop's main nursery from running simultaneously, e.g., to handle events and timers.
 In essence you want a stack of state per thread, which uses the central
 event loop:
 
 func():
 	with x:
 		spawn(foo)
 		join(foo)
 		endScope()
 funcParent():
 	with x:
 		spawn(func)
 		join(func)
 		endScope()
 
 If you don't have this, you will miss timers, window events and all
 sorts of things that could be very time sensitive which would be very
 very bad.
Why will you miss timers and window events? The event loop will spawn tasks into one nursery, while tasks spawn subtasks into their own nurseries. While the task nurseries are blocking to join subtasks, the event loop nursery continues running simultaneously. It doesn't join until the event loop exits. Please elaborate on why you think there's a problem here. I'm not seeing it.
 Because we have an event loop, we don't need a nursery! It comes free of
 charge. It also means we don't need that with statement... hang on that now
 becomes await and async! Just without the await (auto added in scope(exit),
 and compiler can merge them into a single function call ;) ).
I think you're missing the point here. The point is to create an abstraction of concurrent processes that's easier to reason about. Saying "we have an event loop, we don't need a nursery" is akin to saying "we have unrestricted goto, we don't need structured blocks like functions, loops and else-blocks". The point is not whether you can express the same things, but whether it's easier to reason about the resulting code. Two abstractions may be equivalent in expressive power, but one may be extremely hard to reason about (unrestricted gotos, await / async, etc.), while the other may be much easier to reason about (structured code blocks, nurseries). Abstractions that are hard to reason about tends to lead to buggy code, because the programmer has a hard time understanding the full implications of what he's writing. That's why we prefer abstractions that are easier to reason about. That's the point. T -- Perhaps the most widespread illusion is that if we were in power we would behave very differently from those who now hold it---when, in truth, in order to get power we would have to become very much like them. -- Unknown
Aug 17 2018
prev sibling next sibling parent John Belmonte <john neggie.net> writes:
On Friday, 17 August 2018 at 06:36:36 UTC, rikki cattermole wrote:
 Because we have an event loop, we don't need a nursery! It 
 comes free of charge. It also means we don't need that with 
 statement... hang on that now becomes await and async! Just 
 without the await (auto added in scope(exit), and compiler can 
 merge them into a single function call ;) ).
H. S. Teoh already made some fair points in response. I'll just add that having task lifetimes defined by nested blocks mixed right into the normal program flow is powerful, and trying to emulate this with function scopes is not sufficient (return doesn't work, exception handling is a mess, etc.). It's a subtle thing that you actually must try to appreciate. If it were obvious we would have had this decades ago. Regarding async / await, "auto" anything defeats the purpose: they are primarily markers to help programmers reason about the code. value = await some_container.async_get(key) modified_value = transform(some_std_lib_func(value)) /*... a bunch of non-await code that does more stuff with modified_value ...*/ some_container.set(key, modified_value) await foo() Essentially all code between await keywords can be considered atomic. This greatly reduces the need for locking, as well as the cognitive load of reasoning about race conditions and access to shared state. Contrast this with say go's coroutines, where context switches can happen on any function call (https://golang.org/doc/go1.2#preemption). Regards, --John
Aug 18 2018
prev sibling parent Russel Winder <russel winder.org.uk> writes:
On Fri, 2018-08-17 at 18:36 +1200, rikki cattermole via Digitalmars-d
wrote:
 After reading the article I can say, it isn't any better than async
 and=20
 await for dependencies. You still need an event loop.
=20
 [=E2=80=A6]
Or a work stealing threadpool. Event loops are only really needed in contexts that must be single threaded. --=20 Russel. =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D Dr Russel Winder t: +44 20 7585 2200 41 Buckmaster Road m: +44 7770 465 077 London SW11 1EN, UK w: www.russel.org.uk
Aug 22 2018
prev sibling next sibling parent reply Russel Winder <russel winder.org.uk> writes:
On Thu, 2018-08-16 at 20:30 +0000, John Belmonte via Digitalmars-d
wrote:
 This is actually not about war; rather the peace and prosperity=20
 of people writing concurrent programs.
=20
 (Andrei, I hope you are reading and will check out
=20
https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-con= sidered-harmful/
=20
On skimming this, I get the feeling the author doesn't really understand goroutines and channels. Actually I am not entirely sure the person understands concurrency and parallelism. =20
  and
 https://vorpus.org/blog/timeouts-and-cancellation-for-humans/)
=20
 Recently I've been working with Trio, which is a Python async=20
 concurrency library implementing the concepts described in the=20
 articles above.  A synopsis (Python):
Have you tried asyncio in the Python standard library? Is Trio better?
      with open_task_container() as container:
          container.start_task(a)
          container.start_task(b)
          await sleep(1)
          container.start_task(c)

=20

Assuming a, b, and c run in parallel and this is just a nice Pythonic way of ensuring join, this is fairly standard fork/join thread pool task management =E2=80=93 except Python is single threaded so the above is = time division multiplexing of tasks. std.parallelism can already handle this sort of stuff in D as far as I know.
 The point is that tasks started in the container's scope will not=20
 live past the scope.  Scope exit will block until all tasks are=20
 complete (normally or by cancellation).  If task b has an=20
 exception, all other tasks in the container are cancelled.
Use of scope like this is a good thing, and something GPars, Quasar, and others supports. Using a context manager in Python is clearly a very Pythonic way of doing it.
 What this means is that task lifetimes can be readily understood=20
 by looking at the structure of a program.  They are tied to=20
 scoped blocks, honor nesting, etc.
=20
 Similar for control of timeouts and cancellation:
=20

 completed in 10s
          reply =3D await request(a)
          do_something(reply)
          reply2 =3D await request(b)
          ...
=20
 These are novel control structures for managing concurrency. =20
 Combining this with cooperative multitasking and explicit,=20
 plainly-visible context switching (i.e. async/await-- sorry=20
 Olshansky) yields something truly at the forefront of concurrent=20
 programming.  I mean no callbacks, almost no locking, no=20
 explicitly maintained context and associated state machines, no=20
 task lifetime obscurity, no manual plumbing of cancellations, no=20
 errors dropped on the floor, no shutdown hiccups.  I'm able to=20
 write correct, robust, maintainable concurrent programs with=20
 almost no mental overhead beyond a non-concurrent program.
I'd disagree with them being novel control structures. The concepts have been around for a couple of decades. They have different expressions in different languages. Python's context manager just makes it all very neat. Clearly getting rid of the nitty-gritty management detail of concurrency and parallelism is a good thing. Processes and channels have been doing all this for decades, but have only recently become fashionable =E2=80=93 one up to Rob Pike and team. I've not followed clearly not for parallelism. Sadly async/await has become a fashion that means it is being forced into programming languages that really do not need it. Still there we see the power of fashion driven programming language development.=20
 Some specimens (not written by me):

 implementation in about 200 lines of code. =20
=20
https://github.com/python-hyper/h11/blob/33c5282340b61ddea0dc00a16b6582170d= 822d81/examples/trio-server.py

 eyeballs" networking connection algorithm in about 150 lines of=20
 code. =20
=20
https://github.com/python-trio/trio/blob/7d2e2603b972dc0adeaa3ded35cd659052= 7b5e66/trio/_highlevel_open_tcp_stream.py
=20
 I'd like to see a D library supporting these control structures=20
 (with possibly some async/await syntax for the coroutine case). =20
 And of course for vibe.d and other I/O libraries to unify around=20
 this.
Kotlin, Java, etc. are all jumping on the coroutines bandwagon, but why? There is no actual need for these given you can have blocking tasks in a threadpool with channels already.=20
 I'll go out on a limb and say if this could happen in addition to=20
 D addressing its GC dirty laundry, the language would actually be=20
 an unstoppable force.
Why? Are coroutines with language syntax support really needed? And whilst Go is obsessively improving it's GC so as to make it a non- issue to any performance arguments, it seems this is an insoluble problem in D. --=20 Russel. =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D Dr Russel Winder t: +44 20 7585 2200 41 Buckmaster Road m: +44 7770 465 077 London SW11 1EN, UK w: www.russel.org.uk
Aug 22 2018
parent reply John Belmonte <john neggie.net> writes:
On Wednesday, 22 August 2018 at 16:49:01 UTC, Russel Winder wrote:
 Have you tried asyncio in the Python standard library? Is Trio 
 better?
The library that Guido admits is a disaster? https://twitter.com/gvanrossum/status/938445451908472832 Trio and libraries like it have evolved out of frustration with asyncio.
      with open_task_container() as container:
          container.start_task(a)
          container.start_task(b)
          await sleep(1)
          container.start_task(c)

 

Assuming a, b, and c run in parallel and this is just a nice Pythonic way of ensuring join, this is fairly standard fork/join thread pool task management
It's far more than "ensuring join". It ensures that tasks created by children are similarly constrained. It allows the normal try-catch of exceptions coming from any level of child tasks. It ensures that when a task has an error, all sibling tasks are cancelled and the resulting multi-exception is propagated to the parent.
 – except Python is single threaded so the above is time
 division multiplexing of tasks.
No, tasks can optionally be real threads (obviously constrained by GIL). Soon they can be tasks from the "multiprocessing" library as well (separate process, either local or remote). These are details of Trio's implementation and Python. As mentioned, the control structures apply to any concurrency model (implicit or explicit context switching, OS or light threads, etc.)

 for concurrency but clearly not for parallelism. Sadly 
 async/await has become a fashion that means it is being forced 
 into programming languages that really do not need it.
async/await is a means to explicitly mark points of context switching in API's and code. It applies to threads, coroutines, remote processing, or anything else which may allow something else to modify system state while you're waiting. Local to a single thread, async/await with coroutines happens to be wonderful because it eliminates a ton of explicit locking, fretting about race conditions, etc. required of the programmer. It's a building block-- e.g. you can then combine such threads in careful ways with message passing within and among CPU cores to get the best of all worlds. While Go can certainly implement the nursery and cancellation control structures, there is no turning back on Go's implicit context switching (i.e. any old function call can cause a context switch). The human is back to fretting about locks and race conditions, and unable to prove that even the smallest of programs is correct.
Aug 27 2018
parent reply Russel Winder <russel winder.org.uk> writes:
On Tue, 2018-08-28 at 03:36 +0000, John Belmonte via Digitalmars-d
wrote:
 On Wednesday, 22 August 2018 at 16:49:01 UTC, Russel Winder wrote:
 Have you tried asyncio in the Python standard library? Is Trio=20
 better?
=20 The library that Guido admits is a disaster? =20 https://twitter.com/gvanrossum/status/938445451908472832 =20 Trio and libraries like it have evolved out of frustration with=20 asyncio.
When I originally wrote the comment I was thinking of asyncore, which is even worse than asyncio. [=E2=80=A6]
=20
 It's far more than "ensuring join".  It ensures that tasks=20
 created by children are similarly constrained.  It allows the=20
 normal try-catch of exceptions coming from any level of child=20
 tasks.  It ensures that when a task has an error, all sibling=20
 tasks are cancelled and the resulting multi-exception is=20
 propagated to the parent.
Any fork/join framework will provide such guarantees.
 =E2=80=93 except Python is single threaded so the above is time
 division multiplexing of tasks.
=20 No, tasks can optionally be real threads (obviously constrained=20 by GIL). Soon they can be tasks from the "multiprocessing"=20 library as well (separate process, either local or remote). =20 These are details of Trio's implementation and Python. As=20 mentioned, the control structures apply to any concurrency model=20 (implicit or explicit context switching, OS or light threads,=20 etc.)
But that is the point, this is Python specific, and yet the motivating example is a misunderstanding of how Go is used. This inconsistency seriously undermines the general argument. I have no problem with using with statement and context managers to enforce fork/join abstractions in Python. But to say this is a new general abstraction is false claim. [=E2=80=A6]
 async/await is a means to explicitly mark points of context=20
 switching in API's and code.  It applies to threads, coroutines,=20
 remote processing, or anything else which may allow something=20
 else to modify system state while you're waiting.
Certainly it is a language structure to support yield to the appropriate scheduler, the question is whether a language structure is required or it can be handled with library features. I suspect language feature makes things easier of implementation.
 Local to a single thread, async/await with coroutines happens to=20
 be wonderful because it eliminates a ton of explicit locking,=20
 fretting about race conditions, etc. required of the programmer. =20
 It's a building block-- e.g. you can then combine such threads in=20
 careful ways with message passing within and among CPU cores to=20
 get the best of all worlds.
I've never really worried about single threaded concurrency, nor used threaded context, you can do all that is needed using processes and channels. Having said this executors in single or multi threaded contexts work fine at the library level without language constructs.
 While Go can certainly implement the nursery and cancellation=20
 control structures, there is no turning back on Go's implicit=20
 context switching (i.e. any old function call can cause a context=20
 switch).  The human is back to fretting about locks and race=20
 conditions, and unable to prove that even the smallest of=20
 programs is correct.
Not if the user sticks to using processes and channels as is the idiom on Go. Go has all the locks stuff, but if you use channels as the means of communication between processes, the programmer never needs explicit locks since everything is handled bu blocking on channels. --=20 Russel. =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D Dr Russel Winder t: +44 20 7585 2200 41 Buckmaster Road m: +44 7770 465 077 London SW11 1EN, UK w: www.russel.org.uk
Aug 28 2018
parent John Belmonte <john neggie.net> writes:
On Tuesday, 28 August 2018 at 20:05:32 UTC, Russel Winder wrote:
 But that is the point, this is Python specific, and yet the 
 motivating example is a misunderstanding of how Go is used. 
 This inconsistency seriously undermines the general argument.
I don't believe I misunderstand how Go is used. Trying to solve every concurrency issue with processes and channels-- were you need some data silo to manage chunks of shared state-- is not exactly friendly to casual or novice programmers, which is an important segment of the population.
 async/await is a means to explicitly mark points of context 
 switching in API's and code.  It applies to threads, 
 coroutines, remote processing, or anything else which may 
 allow something else to modify system state while you're 
 waiting.
Certainly it is a language structure to support yield to the appropriate scheduler, the question is whether a language structure is required or it can be handled with library features. I suspect language feature makes things easier of implementation.
I'm in total agreement about using a library solution if possible, and stated that in the original post. Important news relevant to this thread: Kotlin has announced support of structured concurrency in its standard coroutine library, citing the "Go statement considered harmful" article as inspiration. https://medium.com/ elizarov/structured-concurrency-722d765aa952 Kotlin appears to be a good example of supporting many concurrency mechanisms via a library. From the manual:
 Many asynchronous mechanisms available in other languages can be
 implemented as libraries using Kotlin coroutines. This includes 
 async/await

 generators/yield

Furthermore it generalizes across different ways of managing shared state. A concurrency scope can span threads, so shared state must be accessed through locks or channels; or a scope can be pinned to a single thread, allowing the programming simplicity of Trio's single-threaded model.
Sep 14 2018
prev sibling next sibling parent Russel Winder <russel winder.org.uk> writes:
On Thu, 2018-08-16 at 16:33 -0700, H. S. Teoh via Digitalmars-d wrote:
=20
[=E2=80=A6]
 I read both articles, and am quite impressed by the revolutionary way
 of
 looking at concurrency.  It provides a clean(er) abstraction that can
 be
 reasoned about much more easily than currently prevalent models of
 concurrency.  Seems it would fit right in with D's message-based
 concurrency communication model.
I found the assumptions about what goroutines were to be wrong. Yes there is an interesting structure built using Python context managers to manage tasks executed by time division multiplexing, but is that really needed since the current systems work just fine if you have threadpools and multiple executing threads =E2=80=93 as Java, Go, etc. have= but Python does not. [=E2=80=A6]
=20
 Indeed.  It certainly seems like a promising step toward addressing
 the
 nasty minefield that is today's concurrent programming models.
I'd say processes and channels works just fine. What is this really providing outside the Python sphere? (Also Javascript?)
=20
[=E2=80=A6] --=20 Russel. =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D Dr Russel Winder t: +44 20 7585 2200 41 Buckmaster Road m: +44 7770 465 077 London SW11 1EN, UK w: www.russel.org.uk
Aug 22 2018
prev sibling next sibling parent "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
On Wed, Aug 22, 2018 at 05:56:09PM +0100, Russel Winder via Digitalmars-d wrote:
 On Thu, 2018-08-16 at 16:33 -0700, H. S. Teoh via Digitalmars-d wrote:
 […]
 I read both articles, and am quite impressed by the revolutionary
 way of looking at concurrency.  It provides a clean(er) abstraction
 that can be reasoned about much more easily than currently prevalent
 models of concurrency.  Seems it would fit right in with D's
 message-based concurrency communication model.
I found the assumptions about what goroutines were to be wrong. Yes there is an interesting structure built using Python context managers to manage tasks executed by time division multiplexing, but is that really needed since the current systems work just fine if you have threadpools and multiple executing threads – as Java, Go, etc. have but Python does not.
I approached the article from a language-independent viewpoint. While I know a little bit of Python, I wasn't really very interested in the Python-specific aspects of the article, nor in the specific implementation the author had written. What caught my interest was the concept behind it -- the abstraction for concurrent/parallel computation that is easy to reason about, compared to other models. The main innovative idea, IMO, is the restriction of parallel/concurrent processing to the lifetime of an explicit object, in this case, a "nursery". (TBH a better term could have been chosen, but that doesn't change the underlying concept.) More specifically, the lifetime of this object can in turn be tied to a lexical scope, which gives you an explicit, powerful way to manage the lifetime of child processes (threads, coroutines, whatever), as opposed to the open-endedness of, say, spawning a thread that may run arbitrarily long relative to the parent thread. This restriction does not limit the expressive power of the abstraction -- it "gracefully degrades" to current open-ended models if, for example, you allocate a nursery on the heap and spawn child processes / threads / etc. into it. However, by restricting the open-endedness of child (process, thread, ...) lifetime, it gives you the ability to reason about control flow in a much more intuitive way. It restores the linearity of control flow in a given block of code (with the well-defined exception if a nursery was explicitly passed in), making it it much easier to reason about. Unless you're explicitly passing nurseries around, you no longer have to worry about whether some function you call in the block might spawn new processes that continue running after the block exits. You no longer need to explicitly manage shared resources and worry about whether resource X could be released at the end of the block. And so on. Even in the more complex case where nurseries are being passed around, you can still reason about the code with relative ease by examining the lifetime of the nursery objects. You no longer have to worry about the case where background processes continue running past the lifetime of the main program (function, block, etc.), or manually keeping track of child processes so that you can sync with them. Once you have this new way of thinking about concurrent processing, other possibilities open up, like returning values from child processes, propagating exceptions, cancellation, etc.. (Cancellation may require further consideration in non-Python implementations, but still, the model provides the basis for a cleaner approach to this than open-ended models allow.) […]
 Indeed.  It certainly seems like a promising step toward addressing
 the nasty minefield that is today's concurrent programming models.
I'd say processes and channels works just fine. What is this really providing outside the Python sphere? (Also Javascript?)
[...] Functionally, not very much. Readability and understandibility-wise, a lot. And that is the point. I personally couldn't care less what it contributes to Python, since I don't use Python very much outside of SCons, and within SCons concurrent processing is already taken care of for you and isn't an issue the user needs to worry about. So in that sense, Trio isn't really relevant to me. But what I do care about is the possibility of a model of concurrency that is much more easily understood and reasoned about, regardless of whether the underlying implementation uses explicit context-switching, fibres, threads, or full-blown processes. Basically, what we're talking about is the difference between a control flow graph that's an arbitrarily-branching tree (open-ended concurrency model with unrestricted child lifetimes: one entry point, arbitrary number of exits), vs. a single-entry single-exit graph where every branch eventually rejoins the parent (nursery model). Having an arbitrarily branching control flow means many concepts don't work, like return values, propagating exceptions back to the parent, managing child lifetimes, etc.. Having well-defined joining points for all children means that it's possible to have well-defined return values, exception propagation, manage child lifetimes, etc.. I don't claim this solves all the difficulties of comprehension in concurrent programming, but it does reduce the mental load by quite a bit. And that to me is a plus, because reduced mental load means the programmer is more likely to get it right, and can spend more effort actually focusing on the problem domain instead of wrestling with the nitty-gritty of concurrency. More productivity, less bugs. Like using a GC instead of manual memory management. Or writing in D instead of assembly language. :-D T -- Almost all proofs have bugs, but almost all theorems are true. -- Paul Pedersen
Aug 22 2018
prev sibling next sibling parent Russel Winder <russel winder.org.uk> writes:
On Wed, 2018-08-22 at 12:00 -0700, H. S. Teoh via Digitalmars-d wrote:
[=E2=80=A6]
=20
 I approached the article from a language-independent viewpoint. While
 I
 know a little bit of Python, I wasn't really very interested in the
 Python-specific aspects of the article, nor in the specific
 implementation the author had written.  What caught my interest was
 the
 concept behind it -- the abstraction for concurrent/parallel
 computation
 that is easy to reason about, compared to other models.
But I see nothing new in the concept (unless I am missing something): scatter/gather parallelism has been a staple part of parallelism for more than 35 years, and that is what this model is about. =20
 The main innovative idea, IMO, is the restriction of
 parallel/concurrent
 processing to the lifetime of an explicit object, in this case, a
 "nursery". (TBH a better term could have been chosen, but that
 doesn't
 change the underlying concept.)  More specifically, the lifetime of
 this
 object can in turn be tied to a lexical scope, which gives you an
 explicit, powerful way to manage the lifetime of child processes
 (threads, coroutines, whatever), as opposed to the open-endedness of,
 say, spawning a thread that may run arbitrarily long relative to the
 parent thread.
Another name for scatter/gather for the last 35+ years is farmer/worker, which is just another way of describing this nursery. Unless I am missing something, this is just new terminology for the same abstraction.
 This restriction does not limit the expressive power of the
 abstraction
 -- it "gracefully degrades" to current open-ended models if, for
 example, you allocate a nursery on the heap and spawn child processes
 /
 threads / etc. into it.
=20
 However, by restricting the open-endedness of child (process, thread,
 ...) lifetime, it gives you the ability to reason about control flow
 in
 a much more intuitive way.  It restores the linearity of control flow
 in
 a given block of code (with the well-defined exception if a nursery
 was
 explicitly passed in), making it it much easier to reason
 about.  Unless
 you're explicitly passing nurseries around, you no longer have to
 worry
 about whether some function you call in the block might spawn new
 processes that continue running after the block exits. You no longer
 need to explicitly manage shared resources and worry about whether
 resource X could be released at the end of the block. And so on.
=20
 Even in the more complex case where nurseries are being passed
 around,
 you can still reason about the code with relative ease by examining
 the
 lifetime of the nursery objects.  You no longer have to worry about
 the
 case where background processes continue running past the lifetime of
 the main program (function, block, etc.), or manually keeping track
 of
 child processes so that you can sync with them.
=20
 Once you have this new way of thinking about concurrent processing,
 other possibilities open up, like returning values from child
 processes,
 propagating exceptions, cancellation, etc..  (Cancellation may
 require
 further consideration in non-Python implementations, but still, the
 model provides the basis for a cleaner approach to this than open-
 ended
 models allow.)
I am not sure I see a difference between nursery and threadpool or executor. Everything that is being said about this nursery can be said of threadpools and executors so it seems to be just a relabelling of a system already available. I am not trying to attack the idea of nice management of concurrency and parallelism, I have been railing against crap parallelism for 35+ years. What worries me is that there is a claim of something new here when there appears not to be something new, it is a relabelling of a known concept.
=20
 [=E2=80=A6]
 Indeed.  It certainly seems like a promising step toward
 addressing
 the nasty minefield that is today's concurrent programming
 models.
=20 I'd say processes and channels works just fine. What is this really providing outside the Python sphere? (Also Javascript?)
=20 [...] =20 Functionally, not very much. =20 Readability and understandibility-wise, a lot.
Really? I am not convinced. There is a danger of mixing management of processes/tasks/threads with managing data. Programmers should never have to manage processes/tasks/threads the framework or language should handle that, programmers should be worried only about their data and the flow and readiness of it.
 And that is the point. I personally couldn't care less what it
 contributes to Python, since I don't use Python very much outside of
 SCons, and within SCons concurrent processing is already taken care
 of
 for you and isn't an issue the user needs to worry about. So in that
 sense, Trio isn't really relevant to me.  But what I do care about is
 the possibility of a model of concurrency that is much more easily
 understood and reasoned about, regardless of whether the underlying
 implementation uses explicit context-switching, fibres, threads, or
 full-blown processes.
We are on the same page on that wish, that is certain.
 Basically, what we're talking about is the difference between a
 control
 flow graph that's an arbitrarily-branching tree (open-ended
 concurrency
 model with unrestricted child lifetimes: one entry point, arbitrary
 number of exits), vs. a single-entry single-exit graph where every
 branch eventually rejoins the parent (nursery model). Having an
 arbitrarily branching control flow means many concepts don't work,
 like
 return values, propagating exceptions back to the parent, managing
 child
 lifetimes, etc..  Having well-defined joining points for all children
 means that it's possible to have well-defined return values,
 exception
 propagation, manage child lifetimes, etc..
But should the programmer care about the details of the tasks/fibres/processes? Shouldn't a task/fibre/process terminate when it has finished providing the data. Lifetime should be tied to the data not to some arbitrary notion of code. This is the core of why the analysis of Go is wrong here. goroutines without channels are meaningless. The papers undermines goroutines without understanding how they are used is Go. Dataflow, actor, CSP, fork/join, etc. implementing various scatter gather strategies already cover the abstraction of "nurseries", and thus cover the problems you highlight. The abstractions already exist, and "nurseries" do not seem to add anything new.
 I don't claim this solves all the difficulties of comprehension in
 concurrent programming, but it does reduce the mental load by quite a
 bit. And that to me is a plus, because reduced mental load means the
 programmer is more likely to get it right, and can spend more effort
 actually focusing on the problem domain instead of wrestling with the
 nitty-gritty of concurrency.  More productivity, less bugs.  Like
 using
 a GC instead of manual memory management.  Or writing in D instead of
 assembly language. :-D
Concurrent programming already has ways of providing a good UX. At least there are lots of frameworks for this for most programming languages in various guises. --=20 Russel. =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D Dr Russel Winder t: +44 20 7585 2200 41 Buckmaster Road m: +44 7770 465 077 London SW11 1EN, UK w: www.russel.org.uk
Aug 23 2018
prev sibling parent John Belmonte <john neggie.net> writes:
On Thursday, 16 August 2018 at 20:30:26 UTC, John Belmonte wrote:
 These are novel control structures for managing concurrency.  
 Combining this with cooperative multitasking and explicit, 
 plainly-visible context switching (i.e. async/await-- sorry 
 Olshansky) yields something truly at the forefront of 
 concurrent programming.  I mean no callbacks, almost no 
 locking, no explicitly maintained context and associated state 
 machines, no task lifetime obscurity, no manual plumbing of 
 cancellations, no errors dropped on the floor, no shutdown 
 hiccups.  I'm able to write correct, robust, maintainable 
 concurrent programs with almost no mental overhead beyond a 
 non-concurrent program.
I've written an article which attempts to expand on the ingredients making Trio + async/await effective, in the hope this paradigm can be carried elsewhere. https://medium.com/ belm0/concurrency-made-easy-d3fdb0382c58
Sep 26 2018