LWN.net Logo

De Icaza: Callbacks as our Generations' Go To Statement

On his blog, Miguel de Icaza touts C# (and F#) async as a superior model for doing asynchronous programming to the mechanisms offered by other languages. He notes that using callbacks for asynchronous programming turns programmers into "glorified accountants" in much the same way goto statements did, as Edsger Dijkstra's famous "Go To Statement Considered Harmful" paper described. "And this is precisely where C# async (and F#) come in. Every time you put the word "await" in your program, the compiler interprets this as a point in your program where execution can be suspended while some background operation takes place. The instruction just in front of await becomes the place where execution resumes once the task has completed."
(Log in to post comments)

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 16, 2013 5:06 UTC (Fri) by atai (subscriber, #10977) [Link]

zillions of threads... you cannot tell what is going on any more, totally unlike C

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 16, 2013 7:40 UTC (Fri) by bofh69 (guest, #92387) [Link]

Async/await is not implemented by multiple threads, they are implemented above the Task-system. When the thread would block it continues with some other Task in the same context (I think there are only two or possibly three contexts - the GUI thread and the rest, possibly with a context of its own for the system thread pool).
At least when reading the code, it is much easier to see the flow and understand what the code does, unlike passing lambdas or separate callback methods.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 16, 2013 5:26 UTC (Fri) by simlo (subscriber, #10866) [Link]

It seems async mechanism is a bit like "low cost threads" or "volenterly preemption."

In some way I like the callback model better: The state of you program is explicit in the program. With thread programming you hide a lot of state information in local variables on your stack. With callbacks you put the state more explicit into the callback objects.

The way I prefer to program is to have one ( 1 !) thread per process and use select and callbacks when data arrive, when data can be written etc. To make this work I have to get rid of all blocking calls except for the select call. Such a program is much more predictable, because given the same input gives exactly the same result. With multiple threads the OS can reschedule at different points and therefore give you different results. This makes it a lot harder to debug.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 16, 2013 7:07 UTC (Fri) by vasvir (guest, #92389) [Link]

The architecture you describe looks to me server or service oriented. I believe the article is about interwined long time blocking calls with user actions.

For example


int temperature = getTemperature(...) //server call - goes through the net

if (temperature > 30) {
    Dialog.showText("Oh it's hot");
} else {
    Dialog.showText("Aah we are cool");
}

Now if you don't want your user interface (UI) to block you have to wrap the getTemperature() call in a thread, and the rest of the code in a callback. So far so good.

What happens when you need to do 2 async calls like getTemperatuture(point1) and getTemperatuture(point2)? In java you can create one wrapping thread that will execute them sequentially or 2 threads that will execute them in parallel. In such a case good luck joining the results back to your normal code flow.

In GWT (java compiler to javascript - executes in browser) there is no such choice. All async operations go through callbacks. So in the above example you would have chained the call to getTemperatuture(point2) inside the callback of getTemperatuture(point1). If you have more calls to perform you will need to redesign your server calls to avoid chaining. If you put user interface actions in the mix the UI code becomes intractable.

And by intractable what I really mean is that you cannot start from a simple design and continue adding features. This would lead either to extensive chaining or continuous redesigns. I don't know if that await is possible but I do know it is useful at least in UI code. The difference from server code is that server code runs in one context. UI code on the other hand usually has one thread from which you should issue all graphics/UI calls (which can block or not) and threads for long running tasks which are not allowed to issue any graphich/UI calls (Qt, Swing possibly GTK+ is like that).

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 16, 2013 7:53 UTC (Fri) by bofh69 (guest, #92387) [Link]

It is of course possible to delegate the actual calling of the getTemperature methods to some object and then let that object call them in sequence or in parallel and then call a callback when the results are in. I have not used Java much since Java2, but in both C++ and JavaScript there are such libraries available. It still adds some cluttering compared to async/await though and you still have to make sure the right code is run on the GUI thread.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 16, 2013 14:22 UTC (Fri) by FranTaylor (guest, #80190) [Link]

> I believe the article is about interwined long time blocking calls with user actions.

So Miguel's "secret" to "better programming" is to allow a very very confused programmer to get his code to compile?

"interwined long time blocking calls with user actions" is about the worst imaginable way to program. Any programmer attempting to do this will fill their code with hidden race conditions.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 18, 2013 14:15 UTC (Sun) by magnus (subscriber, #34778) [Link]

"interwined long time blocking calls with user actions" is about the worst imaginable way to program. Any programmer attempting to do this will fill their code with hidden race conditions.
One could argue that such a race condition becomes more apparent with a coroutine-ish coding style and that it was more hidden before. In a callback model we could have the exact same race where the program is written so it only works when the callback for a blocking action (getTemperature) is called before the callback from the user action that caused it (button click for example) is called a second time but this is never seen clearly since the functionality is spread out over multiple functions.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 19, 2013 7:22 UTC (Mon) by vasvir (guest, #92389) [Link]

I wouldn't disagree with that. However I believe there are two orthogonal issues here.

A) The thread management, starting, joining etc wrt callbacks, library help, compiler help (await) etc.

B) The GUI thread. It would be nice to assume the behavior De Icaza described could extend on the GUI handling but on a second thought I don't believe it is possible. At least not without extensive help from the toolkit.

I would like to comment on B. Consider the following timeline:

i) user action (button click)
ii) user's callback/slot is running in the GUI thread. You can do GUI stuff (enable/disable widgets, popup dialogs) but not block inside that callback for a long time because the GUI doesn't get updated.
iii) So you fork/thread/async your lengthy computation or your disk access or your network request.

The problem I was commenting on arises when:

a) you need to combine results from different threaded operations. It is entirely possible to do it. However you have to refactor the plumbing __or__ your backend every time. An object that will return 2 async values - no! make that three.

b) You want to break into the GUI code from the async code for user verification or for informing the user. i.e "This operation will take 10h. Are you sure you want to continue?"

The a) can be solved by A) library/compiler/language help. The b) needs different toolkit semantics i.e a multi-threaded toolkit which apparently is a very hard thing to do. See https://weblogs.java.net/blog/kgh/archive/2004/10/multith... Basically all widely used toolkits are single threaded toolkits (they use threads as workers).

What I am arguing here is that of course it is possible to write programs that do both a) and b). The problem is that the plumbing becomes fragile and needs constant attention when new features are being added.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 19, 2013 8:10 UTC (Mon) by smurf (subscriber, #17840) [Link]

if you want to break into the GUI from your async code, that's easy. You create a global queue and use that in the GUI as just another source for events. The async code can queue whatever it wants for the GUI to act on.

If the async thread needs a result back, send a variable plus a locked semaphore across. Let the GUI clear the semaphore when the user has answered the request, wait on it in the async thread. Wrap the request/response in an object for cleaner code, debugging, exception handling, and whatnot.

That's not at all fragile. On the contrary, it's good practice to separate code and UI anyway.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 19, 2013 8:26 UTC (Mon) by vasvir (guest, #92389) [Link]

I completely agree with that, separation GUI and process or IO.

However you have to create the protocol for the queue you described. What are the events, what are the responses etc. I am describing this as plumbing. My point is that you can't write simple pseudocode and expect it to work (like you can on python for example). You cannot prototype fast enough because the plumbing layer needs to evolve with the actual features of your program. I am maintaining that the plumping is a significant part of the program which scales linearly with the program (constant percent).

Seriously I see no other way besides toolkit support. So for example this queue idea should be a Qt/Gtk/Swing primitive that I could use without having

a) to implement it in the first place
b) special case it - depending on which context (thread) I am executing
c) jump through hoops to use it

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 19, 2013 8:15 UTC (Mon) by magnus (subscriber, #34778) [Link]

I wasn't thinking about toolkit with a separate GUI thread like in Java, I was mainly thinking about single-threaded toolkits using multiplexing based on poll() and callbacks (hidden under some "signal" sugar).

With a lightweight thread/coroutine system you would put such a GUI in a lightweight thread. Any time an LWT blocks the runtime switches stacks and another LWT can execute, and the runtime collects the different file descriptors and does poll() on them when all LWTs are blocking.

I imagine you could then design the toolkit such that you don't have callbacks at all but you have different LWTs dealing with different parts of the GUI that synchronize with the GUI LWT. You could have one LWT just handling the single button press and the long computation for example.

There are some practical problems with this. One is calling library functions (or native code from high-level langs) that blocks behind your back and ruins the whole system. To be "bullet proof" there needs be some kernel support that allows the runtime to catch blocking syscalls like read,sleep etc anywhere even if done from assembler code. Another problem on Linux is that you can't use poll() on local files, it will return that it's readable even if the call will cause the process to go to io_wait state.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 19, 2013 8:54 UTC (Mon) by vasvir (guest, #92389) [Link]

I wasn't thinking about toolkit with a separate GUI thread like in Java, I was mainly thinking about single-threaded toolkits using multiplexing based on poll() and callbacks (hidden under some "signal" sugar).

All major ones are like that

  • Qt: http://qt-project.org/doc/qt-5.0/qtcore/thread-basics.html#gui-thread-and-worker-thread
  • Gtk: http://blogs.operationaldynamics.com/andrew/software/gnome-desktop/gtk-thread-awareness
  • swing: http://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html
  • swt: http://book.javanb.com/swt-the-standard-widget-toolkit/ch05lev1sec7.html

About your LWT proposal:

There are some attempts in a proper multithreaded toolkits. Here are some:

  • http://www.cs.tufts.edu/~nr/cs257/archive/john-reppy/exene.ps
  • http://www.inf.uos.de/elmar/projects/java-gtk/

I can't comment on them but I do like the smurf's queue idea better. It should be even possible for existing toolkits to be modified to support it. With a global queue a thread can block waiting from GUI thread input but

  • The GUI thread never blocks - is always running
  • The toolkit remains single - threaded effectively avoiding all the locking problems described at https://weblogs.java.net/blog/kgh/archive/2004/10/multithreaded_t.html

I would extending the queue idea by saying that all callbacks/slots from GUI should run in different threads like you are saying. All GUI updates operations should go through the global queue.

But please don't tell me this is not major plumbing...

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 19, 2013 21:16 UTC (Mon) by magnus (subscriber, #34778) [Link]

I think you are missing a difference here between the Java toolkits (Swing & AWT) vs QT and GTK, in that Qt and GTK allows you to add your own file descriptors and timeouts to the main (GUI) event loop. They will then be included in the same poll() call as the GUI file descriptor. Therefore you can do things like slow network access in the same single thread as the GUI without blocking the GUI updates, and your network callbacks can manipulate the GUI since you know the GUI is in an idle state whenever another callback is called.

With the Java toolkits you don't have access to the event loop so you are forced to turn to multithreading to implement this.

The point of the lightweight thread model idea I mentioned would be to use just a single thread as described above but move the work of registering/unregistering file descriptors and callbacks and the event loop etc into the language runtime so you can code the network access as if it was a separate thread with blocking calls. Since it's still not a true thread and there is no parallel execution, the problems of true multi-threaded GUI:s that you refer to don't apply.

My pipe dream was to implement this functionality all the way down into the C/binary level allowing you to run existing single-threaded code in lightweight threads. This is of course just a fantasy and is never going to happen...

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 19, 2013 21:40 UTC (Mon) by Cyberax (✭ supporter ✭, #52523) [Link]

Java has to work on Windows and on Windows it doesn't use poll() to get events, it uses the GetMessage()/DispatchMessage() event loop.

Also, any blocking operations in the GUI thread are a big no-no.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 19, 2013 22:53 UTC (Mon) by smurf (subscriber, #17840) [Link]

The problem with "blocking" is that it's ill-defined. Unix semantics, for instance, states that if select/poll returns, then a non-blocking read() on the file descriptor in question will, well, not block. I.e., retutn "immediately".

Yeah, right. You're invited to test that idea on any NFS- or SMB-stored file. :-/

The bottom line is: Most of the time, a cooperatively-scheduled event loop is perfectly fine. But there are applications which definitely won't work without OS threads.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 20, 2013 1:34 UTC (Tue) by foom (subscriber, #14868) [Link]

select/poll actually don't work at all on plain files. Unfortunately, the API design is that when a fd doesn't support poll, that is indicated by claiming that it's ready for POLLIN | POLLOUT | POLLRDNORM | POLLWRNORM.

epoll has a different API, and actually returns a failure if the fd doesn't support polling.

It's not clear which one is actually better. They both have their pitfalls.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 20, 2013 5:15 UTC (Tue) by magnus (subscriber, #34778) [Link]

Yes, these issues would need to be fixed in Unix to make cooperative scheduling work flawlessly. Which is never going to happen of course. So we will need to paper it over using helper threads and other means.

So I agree with your bottom line, I'm just saying that it would be neat if cooperative scheduling in user space could be made to work.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 20, 2013 6:28 UTC (Tue) by smurf (subscriber, #17840) [Link]

Actually, the kernel does have async IO.

However, you get completion via a signal, which you have to poll() on with signalfd (and then lookup the context and callback for whatever you intended to do with the data), which is significantly more work than to delegate the whole rigmarole to a thread in the first place.

Worse, most libraries don't allow you to feed data in piecemeal, so AIO won't help anyway.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 20, 2013 8:08 UTC (Tue) by HelloWorld (subscriber, #56129) [Link]

> Yeah, right. You're invited to test that idea on any NFS- or SMB-stored file. :-/
Doesn't it work to make the descriptor non-blocking, i. e. fcntl(fd, F_SETFL, O_NONBLOCK)?

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 20, 2013 8:59 UTC (Tue) by smurf (subscriber, #17840) [Link]

No. Regular files (and block devices) do not work that way.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 20, 2013 15:00 UTC (Tue) by HelloWorld (subscriber, #56129) [Link]

So why does the O_NONBLOCK flag exist at all? It is, after all, a flag originally intended for use with open(2), which is primarily used for opening regular files.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 20, 2013 15:28 UTC (Tue) by etienne (subscriber, #25256) [Link]

> So why does the O_NONBLOCK flag exist at all?

Because a regular file is already mapped in memory, a read/write operation can only delay by a page fault and that is not considered blocking - any "jump" assembly instruction can produce a page fault...
The problem is that treating a page fault from an NFS/FUSE file may be long.

On the more general subject, I wonder if this whole need for new keyword / languages is not due to a bad multi-thread design, due to the "big loop" of window 3.1, due to the inefficient fork() of windows, due to the lack of "overcommited" memory and copy-on-write... but I may be wrong.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 20, 2013 21:52 UTC (Tue) by simlo (subscriber, #10866) [Link]

I use the same code for TCP sockets and file descriptors for non-blocking read/write using select(). It works the same.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 20, 2013 22:16 UTC (Tue) by andresfreund (subscriber, #69562) [Link]

Only in the sense that select() et al. will always return plain fd's for normal files as ready for read/write. Independent of the fact that read()/write() will block while waiting for disk io.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 23, 2013 8:34 UTC (Fri) by simlo (subscriber, #10866) [Link]

You are right: I checked the kernel code and found that the fops for ext3 at least doesn't have a poll method set. Then select returns immediately if the read bit is set.

This really should be stated in the select() man page. I can't find anything else in the man pages either.

Is this a standeard behaviour? How does other Unices behave? Is this something which could be fixed in Linux without breaking a lot of programs? For network filesystems the behaviour ought to be like for sockets, such a stalling network filesystem can't make your program stall.

Fortunately for use, we only use file sockets in our test setup. In the production code all file access goes through a TCP socket to another process. A stalling filesystem can therefore not block our time critical code.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 23, 2013 12:39 UTC (Fri) by smurf (subscriber, #17840) [Link]

Yes it is standard behavior, yes every other Unix out there behaves in the same way, no you cannot have different select() behavior on block files without changing kernel semantics in an incompatible way,

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 23, 2013 14:41 UTC (Fri) by etienne (subscriber, #25256) [Link]

> different select() behavior on block files

Long time I did not try, but it would still be nice is select() did not return "writeable" for a file open read-only, "readable" for a write-only file, "writeable" when quota exceeded or filesystem full, and "readable" when the file position is at the end of the file and no other process has increased the size of that file...

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 24, 2013 7:35 UTC (Sat) by smurf (subscriber, #17840) [Link]

It would break semantics. select() returning doesn't mean "you can read or write". It means "reading or writing will not block" (the idea being that block devices and regular files don't, which is stupid but cannot be changed, among other reasons because it conflicts with the idea that you will never get a short read or write from these devices and files).

Anyway, writing to a read-only file certainly will not block. It will instantly return an error.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 20, 2013 20:37 UTC (Tue) by smurf (subscriber, #17840) [Link]

open() is also used for character devices. Or FIFOs. Or (Unix-domain) sockets.

Not all flags make sense on all file types. Or devices, for that matter. So what?

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 20, 2013 12:22 UTC (Tue) by sorpigal (subscriber, #36106) [Link]

With a global queue a thread can block waiting from GUI thread input but The GUI thread never blocks - is always running The toolkit remains single - threaded effectively avoiding all the locking problems described at https://weblogs.java.net/blog/kgh/archive/2004/10/multithreaded_t.html

Isn't this essentially how BeOS has always worked?

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 20, 2013 19:26 UTC (Tue) by magnus (subscriber, #34778) [Link]

I would extending the queue idea by saying that all callbacks/slots from GUI should run in different threads like you are saying. All GUI updates operations should go through the global queue.
Hmm I did some more thinking on this. :)

While you don't want processing unrelated to the GUI to block the GUI thread, at the same time you DO want the GUI to be blocked while you modify the UI state because that simplifies the processing and avoids a lot of races. The toolkit can guarantee for example if you close a window inside a callback that you can not get more events to that window that were enqueued between the callback was called and the delete call.

So whatever method you use, you have some parts of the application UI logic that should be synchronous to the GUI (GUI is blocked and code must be non-blocking) and some that are asynchronous (GUI is not blocked and code may block). Neither lightweight threads or using event queues for everything will solve that problem by itself...

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 20, 2013 21:47 UTC (Tue) by vasvir (guest, #92389) [Link]

Actually I read (in FF mode) the PhD of the Elmar Ludwig (one of the links I posted). I don't know about BeOS so I can't comment. I tried also to read the exene paper but it was over my head.

It looks what we are discussing here is what he (Ludwig) is proposing. One global queue in order to keep the toolkit event based single threaded and some channels/futures so the worker threads can take input (wait) from the GUI.

So whatever method you use, you have some parts of the application UI logic that should be synchronous to the GUI (GUI is blocked and code must be non-blocking) and some that are asynchronous (GUI is not blocked and code may block). Neither lightweight threads or using event queues for everything will solve that problem by itself...

I don't understand why you think that. Looks doable to me. I think I am semi convinced by the design. Of course it is completely possible that we are talking about different things...

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 21, 2013 5:55 UTC (Wed) by magnus (subscriber, #34778) [Link]

I don't understand why you think that. Looks doable to me. I think I am semi convinced by the design. Of course it is completely possible that we are talking about different things...
It's doable all right, question is how much you gain from just blindly moving all callback processing into threads. I still think you need to distinguish explicitly between "instant" callback processing and long-running activity and build the kind of application-specific "plumbing" you're talking about between the two.

If you move all callback processing to a single separate thread, you haven't actually solved anything wrt blocking. If that thread blocks then the GUI will become unresponsive. It's unresponsive in a slightly nicer way than in the single-threaded blocking case since the toolkit's internal event handling is running so you can still click on buttons etc but nothing intelligent happens when you do. So for blocking operations you'll need to launch additional helper threads just as you could have done in the single-threaded case.

The alternative is to use multiple threads that listen to different sets of events but then you will probably run into complicated races and dependency problems when they start touching the same widgets. This could possibly be useful at a one thread per window level but below that is probably not very useful.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 21, 2013 10:19 UTC (Wed) by vasvir (guest, #92389) [Link]

Here is simplified pseudocode. All disclaimers you have ever heard apply:
main program:
...
button->registerEventHandler(event_type, handler, boolean on_new_thread);
...

The handler: if 
handler(...) // if (on_new_thread then runs on separate thread) {
  // handler doesn't know if is on gui_thread or not. We don't care

  // processing...

  // now it's time to update the GUI.
  // Oups. Normally we can't do that or we should be very careful
  // but now we are on our idealized world so let's go and do it.
  
  menu->disable(some args);
}

// Now on Menu widget. We have to check if we are on the gui thread or not
Menu::disable(args) {
  if (!isGUIThread()) {
       // special magic - We have to reschedule the operation on the GUI Thread
       postEvent(GUIOperationEvent, this /*(menu)*/, Operation.Disable, args);
       // the post event will force the menu->disable to be handled from the GUI thread
       return;
  }

  // normal GUI code. We are on the gui thread.
  ...implementation...
}
This is a naive implementation. The problems are:
  • It uses the event subsystem for message passing and for rescheduling tasks on the GUI thread. I don't know if this is a good idea or a specialized message queue should be used for this. On the other hand the event queue is global and it is there on every toolkit :-).
  • disable() doesn't return anything to the issuing thread so our world is ideal. If disable() needs to return something it has to do it via a channel or a future or something complicated to the waiting worker thread. This should collapse to a normal call in the case of on_new_thread == false.

The major problem with this approach is that you need to have two ways to call the same methods with the same arguments for all GUI operations that are not thread safe. This needs some instrumentation. I don't know if it is possible to create a generic mechanism and not having to synchronize between sync and async implementation. Maybe with template wizardry you can do it.

On the bright size the client code can call GUI operations from any thread (or from the gui thread) and wait them to finish without complicated logic or plumbing and that's the gain.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 21, 2013 19:16 UTC (Wed) by magnus (subscriber, #34778) [Link]

How do you handle if the button is clicked a second time while the "// processing" is going on?

Also the GUI could change state because of other callbacks being called during the processing, so maybe the window has already been closed when menu->disable is called, or it doesn't make sense to disable it any more because the conditions have changed. You get lots of possible cases to consider.

You also haven't covered that for a more complex case you might want to read out state from the GUI, contents of text boxes and such. Do you need to make a roundtrip to the GUI thread to send a request and get a response back for each such thing? What could happen if you're holding a lock while you're making such a roundtrip?

The point I'm trying to make is that if you view the GUI as a finite state machine, making all callbacks asynchronous greatly increases the number of states visible from the outside that it can be in... So you want the "state switching" logic (disabling/enabling widgets etc) to be in the GUI thread so it all happens "at once".

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 22, 2013 9:56 UTC (Thu) by vasvir (guest, #92389) [Link]

You point very valid problems.

For the button reclick I would say you are 100% right. You can't have the event listener in another thread entirely. So it has to run in the gui_thread but it can spawn threads from there. Now since it is running in the gui_thread you can disable the button before you spawn the thread or leave it alone be if you want multiple actions running the same time.

I agree with the rest. Having the toolkit single threaded - event based as it is and pass messages/events from the threads is the essence of the proposal. And yes of course it is difficult to communicate back staff to the waiting threads. This is probably why it hasn't been done yet.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 16, 2013 17:01 UTC (Fri) by mohaine (guest, #6101) [Link]

    In GWT (java compiler to javascript - executes in browser) there is no such
    choice. All async operations go through callbacks. So in the above example 
    you would have chained the call to getTemperatuture(point2) inside the callback 
    of getTemperatuture(point1). If you have more calls to perform you will need to 
    redesign your server calls to avoid chaining. If you put user interface actions 
    in the mix the UI code becomes intractable


This is just plain wrong. Yes you have to do callbacks in GWT since there is only one one main thread, but they don't have to be (and shouldn't be) chained. It is trivial to run multiple requests at the same time and have the last one to complete do the final work. I'll agree that chaining is a horrible solution, which is why you don't do it unless the second request depends on the results of the first request.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 16, 2013 6:39 UTC (Fri) by salimma (subscriber, #34460) [Link]

Not all other languages, surely. This seems, IMHO, similar to how Go channels (and Clojure's new core.async library) work.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 16, 2013 8:17 UTC (Fri) by neonsignal (guest, #87868) [Link]

Indeed, there's a whole list of languages supporting continuations in wikipedia
https://en.wikipedia.org/wiki/Continuation#Programming_la...

I would have thought that the channels in Go would more properly be classified as coroutine support.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 16, 2013 11:23 UTC (Fri) by chrisV (subscriber, #43417) [Link]

I don't use Go, but channels don't seem to be similar to coroutines (on the other hand, C# async does behave like a coroutine). channels seem to be much the same as futures in other languages - in other words, the thread of execution reading from a channel will block until a result becomes available.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 16, 2013 12:34 UTC (Fri) by Cyberax (✭ supporter ✭, #52523) [Link]

Channels are a way to get results from coroutines.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 16, 2013 12:46 UTC (Fri) by chrisV (subscriber, #43417) [Link]

> Channels are a way to get results from coroutines.

I thought they were a way to get results from goroutines?

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 16, 2013 14:17 UTC (Fri) by emk (subscriber, #1128) [Link]

I agree that 'async' is much nicer than manual callbacks, but a well-designed programming language can do even better than that. Erlang, Go, Elixir, etc., all allow you to write normal code that blocks on I/O and allows you to wait for incoming messages.

I think it's time to move to languages that transparently support thousands of lightweight threads with inexpensive message passing and good latency, rather than messing around with callbacks or lots of extra keywords.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 16, 2013 7:35 UTC (Fri) by bofh69 (guest, #92387) [Link]

The async keyword is a great hack for the C#/F#/VB.Net languages given how they have evolved. But it is quite a bit of a hack in my opinion and it does lead to quite a bit of DRY violations when library writers write one synchronous and one asynchronous method doing the same thing, just like it was common before the keyword was added.

Could this not have been handled more by the compiler/runtime so it automatically creates a synchronous method when called from a synchronous context or vice versa?

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 16, 2013 7:39 UTC (Fri) by smurf (subscriber, #17840) [Link]

Having recently converted a largish Python program from Twisted Deferred handlers (callbacks) to Gevent (coroutines), I have to agree: callbacks are evil.

Callbacks are a twisty mess. My main problem with them is that the call graph gets lost; you get an exception somewhere and you have no idea whatsoever which code queued the callback that queued the callback that died on you.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 16, 2013 13:44 UTC (Fri) by kjp (subscriber, #39639) [Link]

Someday I'm looking forward to trying the "yield from" syntax in python 3.4 for coroutines. Seems much, much simpler.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 16, 2013 22:17 UTC (Fri) by ssmith32 (subscriber, #72404) [Link]

Ok, I still need to read the friendly article - and I will, Miguel's articles are usually quite good, especially when I don't know something or don't initially agree with his point...

But I *had* to echo your sentiment - I worked on a large Twisted program, and, yes, it was also a large twisted program. Especially when it comes to exceptions. The best part was there were parts where exceptions got swallowed, or dumped to stderr in a daemon that was only set up to log stdout.. so, yeah, you basically had no idea what was going on...

It could be possible to program neatly in Twisted, but the coroutine stuff I've seen is always much much cleaner (no experience in it though).

I have to say, though - in more cases than most people think, threads really aren't as evil as people say. Especially with some of the nicer higher-level libs that provide the thread pool and work queue components built-in. I think threads are confusing, but get beat on a little too much...

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 16, 2013 22:48 UTC (Fri) by ssmith32 (subscriber, #72404) [Link]

Ok, read it,, and hrmm.. seems like, at least in his example, threads would be simplest.

He had a method that took and picture and posted it.
Each part of the method that did any I/O, he had "await" in it, so the program could continue.

If he just wrapped the method in a thread, he'd have it execute asynchronously AND the method would be even simpler, AND you would know when it was executing, like so:

Thread thr = new Thread( TakePhotoAndPost );
thr.start()
....
....
thr.join()

What confuses me about coroutines and callbacks both, is that you have even less of an idea when things execute - you have no idea when it's taking a picture and posting it... and these things usually matter. I mean you need to know when it takes a picture, at least, so you take the right one...

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 21, 2013 11:13 UTC (Wed) by alankila (subscriber, #47141) [Link]

The general problem here is that thr.join() is not generally permissible. You aren't allowed to wait for results of another thread in the first thread, which is usually dedicated for UI event processing. You can only join if you know it has quit already, because then you know that the join() will return without a delay.

I've had to solve these kind of problems and I typically startup a thread pool with number of threads equal to CPU cores of system, and then start pushing work items into them. If multiple work items are dependent on each other before a further work item can be scheduled, then the work items have a counter which decrements by 1 every time the item is done and when that counter reaches 0, then another work item is pushed into the thread queue. There's also typically a way to schedule work items to be run in the UI event loop, so the work can continue in context of that thread later on.

The problem here is that it's all kinda confusing-looking. Hence the motivation for something like async/await. It does look pretty nifty but I haven't really studied it in detail so far.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 20, 2013 22:19 UTC (Tue) by simlo (subscriber, #10866) [Link]

From this comment as well as the main article the main problem of callbacks seems to be error handling using exceptions.

An error is an event. I.e. in the event based, callback driven programming exceptions have to handled in specific callbacks. Say for instance a connect to a TCP port fails with "connection refused." If connect() is blocking throw an exception. But with non-blocking connect() you will get the result of the connect() operation later maybe in the same callback handling a successfull connect(). So you are forced have to have error handling right there and decide what to do. You can't wrap the whole long sequence into one big try catch and handle it all in one big error handler.

To me it sounds like event-callback driven coding actually forces the coder to handle all failures intelligently. Maybe a showMessage("Connection failed: <reason>") or simply try the connect() again, or whatever is a proper failure mode.

This only strengthens by faith in single threaded, event-callback driven programs not using exceptions. And yes, as mentioned in comments above proper GUI toolkits also supports that, so it is not only for server-programming.
At me this works very well for embedded programs written in C++. I compare with Java with a lot of threads. The C++ programs are much more stable and have far fewer flaws with error handling in for instance handling of TCP connections.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 16, 2013 9:00 UTC (Fri) by sabroad (guest, #92392) [Link]

"Every time you put the word "await" in your program, the compiler interprets this as a point in your program where execution can be suspended while some background operation takes place."
Example:
> var mFile = await picker.TakePhotoAsync (new Xamarin.Media.StoreCameraMediaOptions ());
> var tagsCtrl = new GetTagsUIViewController (mFile.GetStream ());

Ummm- isn't the await just making an Async call synchronous? How is that any advancement on plain C where, "the compiler interprets a function call as a point in your program where execution can be suspended while some operation takes place."

It seems a bit smoke and mirrors to me, Async and await.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 16, 2013 11:15 UTC (Fri) by kugel (subscriber, #70540) [Link]

No. When you hit the await keyword in a method (B) the method returns to its caller(A) (returning a Task<> object). The rest of the function, while it looks sequentially and synchronous, is wrapped into a callback(C) by the compiler that is run the when the async code finishes (=> continuation). This includes the actual return statement of the function. C runs asynchronous but in the same context/thread the await keyword was used, e.g. B's context.

When C returns the actual value is noted in the Task object B returned for later use (e.g. to update a text field in the UI).

Also note that async/await propagates up in the call hierarchy, i.e. A also called B using the await keyword and so on.

This is how I understand it and it sounds pretty nifty to me.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 16, 2013 11:40 UTC (Fri) by chrisV (subscriber, #43417) [Link]

It makes the asynchronous call appear synchronous in terms of how you program, but underneath the runtime treats it as a coroutine, or switchable coroutines on a thread pool. No thread of execution actually blocks, even though it appears to do so.

About as close as you can get to it in C is with setjmp()/longjmp(). You would need language support for delimited continuations (such as provided by guile's scheme implementation) to do a more wholesome emulation of it.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 16, 2013 12:47 UTC (Fri) by bofh69 (guest, #92387) [Link]

There is Adam Dunkels' protothreads code for C that uses macros to translate serial code into a state machine that returns when awaiting some result, much like this async/await does. Someone else has made a C++ port that gives some extra advantages (like reentry via multiple objects). Its quite clever, but puts some restrictions on the written code.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 21, 2013 14:07 UTC (Wed) by pbonzini (subscriber, #60935) [Link]

You can do it relatively easily in C with makecontext() + swapcontext(), more precisely. sigaltstack() + kill() can be a replacement for makecontext(), and sigjmp() + longjmp() can be a replacement for swapcontext().

QEMU does exactly this to implement coroutines.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 21, 2013 21:15 UTC (Wed) by chrisV (subscriber, #43417) [Link]

> You can do it relatively easily in C with makecontext() + swapcontext()

Those are not in the C standard.

They are not even in the POSIX standard anymore. They were removed in POSIX.1-2008.

Which takes you back to setjmp/longjmp.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 21, 2013 23:13 UTC (Wed) by smurf (subscriber, #17840) [Link]

> Those are not in the C standard.

So are a whole lot of other useful functions. You're not going to be able to write a sane program with just POSIX in your environment anyway.

So what's your problem?

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 22, 2013 19:36 UTC (Thu) by chrisV (subscriber, #43417) [Link]

I think your problem is that you have completely missed the point. The poster said those functions for implementing coroutines are available in C. They are not; nor are they now available in POSIX. So if you cannot "write a sane program with just POSIX in your environment anyway" what do you use to implement coroutines?

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 22, 2013 20:03 UTC (Thu) by pbonzini (subscriber, #60935) [Link]

I wrote that: sigaltstack() + kill() can be a replacement for makecontext(), and sigjmp() + longjmp() can be a replacement for swapcontext().

However, it's not as easy as makecontext() + swapcontext().

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 22, 2013 20:14 UTC (Thu) by chrisV (subscriber, #43417) [Link]

Do you have an implementation you can point to?

What is sigjmp by the way()? (I know of sigaltstack(): it is in POSIX though not in C, although I cannot immediately see how it will help with coroutines. sigjmp() I have never heard of.)

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 23, 2013 1:34 UTC (Fri) by nybble41 (subscriber, #55106) [Link]

> What is sigjmp by the way()?

I don't think there is a sigjmp(). sigsetjmp() and siglongjmp() are POSIX functions which work like setjmp() and longjmp(), except that they save and restore the signal masks along with the rest of the environment.

> I know of sigaltstack() ... I cannot immediately see how it will help with coroutines

I believe the idea is to give the signal handler an independent stack so that the two coroutines don't interfere with each other. Otherwise they would overwrite each other's stack space. You can't use longjmp() or siglongjmp() to return to a context which is deeper in the stack. Under more normal circumstances, that could only mean one which has already returned; here, in the absence of sigaltstack(), it would mean the signal hander. Using separate stacks avoids that problem, allowing you to safely switch back and forth.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 23, 2013 5:29 UTC (Fri) by pbonzini (subscriber, #60935) [Link]

sigjmp() is simply setjmp(). :)

sigaltstack() is used with the coroutine's stack as the argument. Then a kill() or pthread_kill() is used to trick the kernel into performing the work of makecontext(). Once you do that, you can setjmp()+longjmp() back into the creator coroutine, and from that point all transfers of control are done simply with setjmp()+longjmp(). I think this originates in the GNU Pth library.

You can find code in QEMU's coroutine-* source files. It includes makecontext/swapcontext, sigaltstack, win32 fibers, and a debugging version that uses threads. Note that the thread version has different signal behavior if a coroutine is used to "jump" from a thread to another, which is why I called it only good for debugging. With the thread version, the coroutine will keep the sigmask of the thread that created it. For all other versions, the sigmask is that of the thread that runs the coroutine.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 16, 2013 12:38 UTC (Fri) by cmorgan (guest, #71980) [Link]

Cool, I just read this last night.

'await' looks like a great way to let the compiler perform more of the heavy lifting. I like the improvement in syntax and readability.

On another note, it looks like MS has extended its commitment to .net for at least another handful of years or more. I was worried it might go the way of Silverlight and being a huge fan of Mono it would make it more difficult to argue for the use of .net/c# on Linux if MS, the creators of the language/vm dropped it.

Sounds like boost::async

Posted Aug 16, 2013 13:01 UTC (Fri) by cry_regarder (subscriber, #50545) [Link]

Sounds like boost::async

Posted Aug 16, 2013 13:53 UTC (Fri) by chrisV (subscriber, #43417) [Link]

boost::async() is now std::async() in C++11, and it returns a (blocking) future. You can compel it to start a new thread but it does not provide its result asynchronously (it can't, because C++11 has no standard event loop nor support for coroutines).

You can sort-of emulate C# async in effect, by having a thread which composes tasks using std::async(), blocks on the futures it obtains, and then when finished posts its result to the program's event loop (in glib, say, using g_idle_add()). With this scheme you cannot eliminate at least one blocking thread (and you may end up with more), but at least the blocking thread will be scheduled out by the OS's scheduler while it is blocking.

A more refined development of this for C/C++ is to run a thread pool set with a maximum number of threads approximating to the number of cores available, where tasks assigned to the pool increment the maximum thread count when about to block so as to keep cores active (decrementing again when coming out of the block) and deliver the composed result to the program's event loop. It avoids "callback hell" (you only have one callback), but this still puts the onus on the programmer to manage the thread count correctly. C# async hides all that.

Sounds like boost::async

Posted Aug 16, 2013 14:08 UTC (Fri) by Cyberax (✭ supporter ✭, #52523) [Link]

Boost now gained stackfull coroutine support, so emulating async/wait is fairly trivial.

Sounds like boost::async

Posted Aug 16, 2013 14:44 UTC (Fri) by chrisV (subscriber, #43417) [Link]

> Boost now gained stackfull coroutine support, so emulating async/wait is
> fairly trivial.

Excellent. Although I suspect "fairly trivial" may not be so trivial when doing it in a multi-threaded context: it might require in effect writing an entire VM. If you could put forward a boost proposal with outline implementation I suspect it would be well received.

Sounds like boost::async

Posted Aug 16, 2013 16:09 UTC (Fri) by Cyberax (✭ supporter ✭, #52523) [Link]

You need some kind of central dispatch, sure. But nothing approaching a level of a VM.

But I cheated a little bit because I know about await/async implementation based on Boost.Coroutine: https://github.com/panaseleus/await_emu (originally from here: http://www.rsdn.ru/forum/cpp/5219587.flat#5219587 )

Sounds like boost::async

Posted Aug 16, 2013 16:26 UTC (Fri) by elanthis (guest, #6227) [Link]

And in a few years, won't be necessary. This async/await feature is being worked on for C++ built on top of future.

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n...

The idea received very favorable feedback at the committee meeting I was at. My only complaints I raised were that the implementation provides no support for allocators or user-controlled allocation of task data, which is a problem for people needing very tight memory control or specialized debugging interfaces (e.g games, others).

Sounds like boost::async

Posted Aug 16, 2013 16:53 UTC (Fri) by Cyberax (✭ supporter ✭, #52523) [Link]

I think it's a bad idea. Stackful coroutines are more powerful and they are already implemented for C++.

The only major thing missing is composite stack - ability to grow stack on demand, like in Go.

Sounds like boost::async

Posted Aug 22, 2013 16:36 UTC (Thu) by tvld (guest, #59052) [Link]

> The idea received very favorable feedback at the committee meeting I was at.

That was definitely _not_ my impression at the ISO C++ meetings I attended. What I observed was most people not commenting, some liked it, and some disliked it.

While await (i.e., "resumable" functions) makes concurrency _look_ less complex, it's still concurrent execution even if it may look like a sequential program.

If you look at the proposal closely, there are several open issues or unanswered questions (e.g., which properties do the execution agents have precisely? how much of a light-weight thread are they?). Second, there's no detailed discussion of why this should come in exactly this form, including having to annotate functions; there are high-level remarks about C# etc. having it, and that some of the design's properties are necessary because of certain unnamed implementation concerns, but there's no discussion at all about the implementation trade-offs.

I'm mentioning the implementation aspect because besides making concurrency look less scary, this is clearly driven by performance concerns: There can be blocking which they want to avoid (thus, add concurrency), yet they don't want to pay the price of full OS threads (e.g., ordinary stacks, thread-local storage). Thus, await gives you lighter-weight execution agents (again, they don't specify the details, but that's how I understood the answers of the proposal's authors).

So, overall I think it's necessary to think about to which extent standardized light-weight execution agents would be sufficient. (Note that there might be implemented similar to green threads, but unlike traditional green threads or M:N threading those execution agents don't have to pretend that the green thread is a true OS thread, so they have to do significantly less). If they are sufficient, then one probably doesn't need to burden programmers with littering their code with function annotations and await keywords, nor require them to specify all the points where cooperative scheduling can take place.

Sounds like boost::async

Posted Aug 19, 2013 15:29 UTC (Mon) by jwakely (subscriber, #60262) [Link]

> boost::async() is now std::async() in C++11

std::async() came first, it was designed by the C++ committee then later added to Boost.

Ocaml...

Posted Aug 16, 2013 14:32 UTC (Fri) by rsidd (subscriber, #2582) [Link]

F# being derived from Ocaml, anyone know if this exists also in Ocaml? I came across the async library for ocaml -- is it the same thing, or similar enough?

Ocaml...

Posted Aug 16, 2013 14:38 UTC (Fri) by rsidd (subscriber, #2582) [Link]

Trying to answer my own question, I see that F# actually lists Linux officially as a supported platform! (Depends on mono.) Maybe I should give it a try.

i.e. coroutines

Posted Aug 16, 2013 17:31 UTC (Fri) by stevenj (guest, #421) [Link]

Yes, coroutines are very useful for event processing, asynchronous I/O, and similar tasks.

Like Miguel, I'm sure we are all grateful to Microsoft for inventing coroutines.

i.e. coroutines

Posted Aug 17, 2013 20:54 UTC (Sat) by marcH (subscriber, #57642) [Link]

Let's also be grateful to whoever came with the term "callback" since he or she (re-)invented functional programming.

i.e. coroutines

Posted Aug 17, 2013 21:52 UTC (Sat) by marcH (subscriber, #57642) [Link]

As an interesting coincidence: Bret Victor - the future of programming
https://www.youtube.com/watch?v=8pTEmbeENF4

i.e. coroutines

Posted Aug 17, 2013 22:19 UTC (Sat) by Cyberax (✭ supporter ✭, #52523) [Link]

I'll bite. It's not a 'future of programming', it's more like the view of the future of programming from 1970.

i.e. coroutines

Posted Aug 21, 2013 14:09 UTC (Wed) by pbonzini (subscriber, #60935) [Link]

Reinvented continuation-passing style, actually.

Callbacks are not that bad

Posted Aug 19, 2013 12:25 UTC (Mon) by man_ls (guest, #15091) [Link]

I have been coding extensively in JavaScript lately, been to callback hell and back, and wanted to report what it is like (and how I got out).

Callbacks and GOTOs are not even in a similar degree of evil: any GOTO in a program throws you back (and is therefore a bad thing), while just one level of callbacks is not hard to understand. When I see a stack of callbacks I am rather reminded of how in the 90s I used to see a bunch of nested for loops, which made code utterly mystifying. Only after a careful work of rewriting was the inner structure of the code exposed, usually leaving just one level of for loops and creating a few functions in the way.

In our present times, many nested callbacks are evil, but one or two are fine. I would draw the cognitive line at two levels of nested callbacks.

How can the remaining levels be flattened? Easy: instead of lambdas, use named functions. One function receives one callback, does two levels, and continues with the callback. This:

doFirstThing(function(result, callback) {
  doSecondThing(result, function(result2) {
    doThirdThing(result2, function(result3) {
      callback(result3);
    });
  });
});
Becomes this:
doInitialThings(function(result, callback) {
  doThirdThing(callback);
});

function doInitialThings(input, callback) {
  doFirstThing(input, function(result) {
    doSecondThing(result, callback);
  });
}
There. The node convention of passing callback(error, result) makes things a bit more verbose, but it is tolerable.

In extreme cases some outer structure is needed to hold all the transient state, instead of relying on closures. A JavaScript object is fine for this purpose: functions become member functions of the object, and this object can be passed around. I prefer to create Java-like objects anyway, but there is some choice.

There are also many aids like the node.js async library (which I highly recommend), but it is trivial to write your own code to do the same. People are touting futures as the solution to all problems but I have yet to see the advantage.

Callbacks are very superior to the async keyword in many respects: they can be stored, stacked, serialized, parallelized or whatever else is needed. The lack of threads is very refreshing after having worked with Java: your state is yours and nobody is messing with it behind your back.

In short: use callbacks wisely and you will not be disappointed.

Callbacks are not that bad

Posted Aug 22, 2013 6:26 UTC (Thu) by wahern (subscriber, #37304) [Link]

I don't understand why people are touting futures at all. Futures are just a synchronization mechanism. Futures are much too low-level.

The answer to the problem is coroutines, which allows one to flip the producer/consumer relationship between two blocks of code, and to do so in both directions. Coroutines allow you to replace a callback with a return value, while still allowing both blocks of code to push parameters using a function invocation and retrieve results with a return value, which is the most natural form.

In languages like Lua where coroutines are implemented as separate virtual stacks, allowing yielding across intermediate function invocations, coroutines can be used to hack together a kind of cooperative threading. And so when people hear coroutines they immediately jump to the idea of threading. But that entirely misses the fundamental benefit of coroutines, which is the ability to invert consumer/producer in an ad hoc manner at the language level, without resorting to message passing.

Coroutines are the final answer, and it's why my favorite language is Lua. Not only does it have flawless coroutine support, it doesn't use any tricks with the C stack, which otherwise can cause headaches when writing libraries or integrating into large applications, especially with POSIX threads.

There's no easy answer for C or C++ code. Juggling C stacks is ugly and problematic. The simplest and most straight-forward solution in C is to use GCC's computed gotos, which makes it easy to implement generators. It's _extremely_ useful, and that's why it was one of the earliest extensions. I used computed gotos all over the place in my non-blocking libraries, because I despise callbacks. Not just to restart an I/O operation, but to implement streaming-type behavior for routines which transform data in smallish chunks.

Callbacks are not that bad

Posted Aug 22, 2013 13:41 UTC (Thu) by Cyberax (✭ supporter ✭, #52523) [Link]

Juggling stacks in C/C++ is not that complicated. You do need platform-specific functions to do it (fibers on Windows, makecontext on Linux, etc) but once you do it then you immediately get stackfull coroutines.

Callbacks are not that bad

Posted Aug 22, 2013 18:07 UTC (Thu) by wahern (subscriber, #37304) [Link]

I never said it was complicated. I said it doesn't play well with POSIX threading in some environments. This used to be the case on Linux with the old LinuxThreads implementation, for example. And I believe it's still the case on NetBSD. So, you can't use them, _and_ POSIX threads, _and_ be portable.

Also, unless your stacks are small it's a waste of memory. In Lua a coroutine is 100 bytes, and I can use as many of them as I want to simplify an interface. In C, your stack needs to be largish--e.g. 8K or more--depending on what libraries you use and what assumptions they make about stack size. On Linux PATH_MAX is 4K, and lots of software uses PATH_MAX to size automatic char arrays.

Say I have 8K stacks at 8K each, that's 64MB of mostly wasted space. Also, in the future Linux may move to segmented stacks. Segmented stacks may not play nice with makecontext interfaces.

Yes, we've all played around with makecontext and friends. It's an interesting interface. But it's a hack, pure and simple, and not something you should be building any largish project around unless you don't care about hitting a brick wall down the road.

Callbacks are not that bad

Posted Aug 22, 2013 18:21 UTC (Thu) by wahern (subscriber, #37304) [Link]

I mean, I never said it was complicated, and by "ugly and problematic" I was referring to issues with POSIX threads and similar issues where switching stacks can cause problems.

Also, setcontext is not all that lightweight. It has to query the signal mask, and it has to save all the registers, including floating point registers. Actual POSIX threads are so highly optimized these days that the context switching costs between a ucontext_t thread and a real POSIX thread are about the same. So if all you're doing with makecontext, etc is rolling a threading implementation, you're not actually buying much. Just classic premature optimization.

It may make sense if you're using ucontexts as classic coroutines to invert consumer/producer patterns, but C doesn't have closures or multiple return values, so that becomes very tedious. At the end of the day state machines using computed gotos can be simply easier to work with and much more straight-forward. (Yes, you can also use tricks with the switch statement, but straight-up computed gotos--without using the indexing trick GCC recommends to speed up linking--is so much more convenient and pleasant.)

Callbacks are not that bad

Posted Aug 22, 2013 18:29 UTC (Thu) by Cyberax (✭ supporter ✭, #52523) [Link]

Well, I worked with a complicated and large (about 3M lines) telecom app back in 2002 (I think) that was completely coroutine-based. It worked quite well.

As for stacks, if we assume that we have a server with a coroutine-for-connection model then 8k connections is already a load big enough that it makes 64Mb of overhead insignificant.

Segmented stacks would actually help a lot and reconciling them with makecontext() shouldn't be that complicated.

De Icaza: Callbacks as our Generations' Go To Statement

Posted Aug 20, 2013 1:52 UTC (Tue) by Trelane (guest, #56877) [Link]

perhaps I'm completely misunderstanding, but this sounds like Android'sTask stuff.

Copyright © 2013, Eklektix, Inc.
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds