I'm just catching up on this, so I'm going to make some notes about things that took a minute to figure out. Correct me where I'm wrong.
For the most part I'll be talking in C# lingo, because this stuff comes from C# and is much more natural there. There are C++/CX versions of all this, but they're rather more ugly. Occasionally I'll dip into what it looks like in CX, which is where we start :
1. "hat" (eg. String^)
Hat is a pointer to a ref-counted object. The ^ means inc and dec ref in scope. In cbloom code String^ is StringPtr.
The main note : "hat" is a thread-safe ref count, *however* it implies no other thread safety. That is,
the ref-counting and object destruction is thread safe / atomic , but derefs are not :
Thingy^ t = Get(); // thread safe ref increment here
t->var1 = t->var2; // non-thread safe var accesses!
There is no built-in mutex or anything like that for hat-objects.
2. "async" func keyword
Async is a new keyword that indicates a function might be a coroutine. It does not make the function
into an asynchronous call. What it really is is a "structify" or "functor" keyword (plus a "switch")
. Like a C++ lambda, the main thing the language does for you is package
up all the local variables and function arguments and put them all in a struct. That is (playingly
rather loosely with the translation for brevity) :
async void MyFunc( int x )
{
string y;
stuff();
}
[ is transformed to : ]
struct MyFunc_functor
{
int x;
string y;
void Do() { stuff(); }
};
void MyFunc( int x )
{
// allocator functor object :
MyFunc_functor * f = new MyFunc_functor();
// copy in args :
f->x = x;
// run it :
f->Do();
}
So obviously this functor that captures the function's state is the key to making this into an async
coroutine.
It is *not* stack saving. However for simple usages it is the same. Obviously crucial to this is using a language like C# which has GC so all the references can be traced, and everything is on the heap (perhaps lazily). That is, in C++ you could have pointers and references that refer to things on the stack, so just packaging up the args like this doesn't work.
Note that in the above you didn't see any task creation or asynchronous func launching, because it's not. The "async" keyword does not make a function async, all it does is "functorify" it so that it *could* become async. (this is in contrast to C++11 where "async" is an imperative to "run this asynchronously").
3. No more threads.
WinRT is pushing very hard to remove manual control of threads from the developer. Instead you have an OS thread pool that can run your tasks.
Now, I actually am a fan of this model in a limitted way. It's the model I've been advocating for games for a while. To be clear, what I think is good for games is : run 1 thread per core. All game code consists of tasks for the thread pool. There are no special purpose threads, any thread can run any type of task. All the threads are equal priority (there's only 1 per core so this is irrelevant as long as you don't add extra threads).
So, when a coroutine becomes async, it just enqueues to a thread pool.
There is this funny stuff about execution "context", because they couldn't make it actually clean (so that any task can run any thread in the pool); a "context" is a set of one or more threads with certain properties; the main one is the special UI context, which only gets one thread, which therefore can deadlock. This looks like a big mess to me, but as long as you aren't actually doing C# UI stuff you can ignore it.
See ConfigureAwait etc. There seems to be lots of control you might want that's intentionally missing. Things like how many real threads are in your thread pool; also things like "run this task on this particular thread" is forbidden (or even just "stay on the same thread"; you can only stay on the same context, which may be several threads).
4. "await" is a coroutine yield.
You can only use "await" inside an "async" func because it relies on the structification.
It's very much like the old C-coroutines using switch trick. await is given an Awaitable (an interface to an async op). At that point your struct is enqueued on the thread pool to run again when the Awaitable is ready.
"await" is a yield, so you may return to your caller immediately at the point that you await.
Note that because of this, "async/await" functions cannot have return values (* except for Task which we'll see next).
Note that "await" is the point at which an "async" function actually becomes async. That is, when you call an async function, it is *not* initially launched to the thread pool, instead it initially runs synchronously on the calling thread. (this is part of a general effort in the WinRT design to make the async functions not actually async whenever possible, minimizing thread switches and heap allocations). It only actually becomes an APC when you await something.
(aside : there is a hacky "await Task.Yield()" mechanism which kicks off your synchronous invocation of a coroutine to the thread pool without anything explicit to await)
I really don't like the name "await" because it's not a "wait" , it's a "yield". The current thread does not stop running, but the current function might be enqueued to continue later. If it is enqueued, then the current thread returns out of the function and continues in the calling context.
One major flaw I see is that you can only await one async; there's no yield_all or yield_any. Because of this
you see people writing atrocious code like :
await x;
await y;
await z;
stuff(x,y,z);
Now they do provide a Task.WhenAll and Task.WhenAny , which create proxy tasks that complete when the desired
condition is met, so it is possible to do it right (but much easier not to).
Of course "await" might not actually yield the coroutine; if the thing you are awaiting is already done, your coroutine may continue immediately. If you await a task that's not done (and also not already running), it might be run immediately on your thread. They intentionally don't want you to rely on any certain flow control, they leave it up to the "scheduler".
5. "Task" is a future.
The Task< > template is a future (or "promise" if you like) that provides a handle to get the result of a coroutine when it eventually completes. Because of the previously noted problem that "await" returns to the caller immediately, before your final return, you need a way to give the caller a handle to that result.
IAsyncOperation< > is the lower level C++/COM version of Task< > ; it's the same thing without the helper methods of Task.
IAsyncOperation.Status can be polled for completion. IAsyncOperation.GetResults can only be called after completed. IAsyncOperation.Completed is a callback function you can set to be run on completion. (*)
So far as I can tell there is no simple way to just Wait on an IAsyncOperation. (you can "await"). Obviously they are trying hard to prevent you from blocking threads in the pool. The method I've seen is to wrap it in a Task and then use Task.Wait()
(* = the .Completed member is a good example of a big annoyance : they play very fast-and-loose with documenting the thread safety semantics of the whole API. Now, I presume that for .Completed to make any sense it must be a thread-safe accessor, and it must be atomic with Status. Otherwise there would be a race where my completion handler would not get called. Presumably your completion handler is called once and only once. None of this is documented, and the same goes across the whole API. They just expect it all to magically work without you knowing how or why.)
(it seems that .NET used to have a Future< > as well, but that's gone since Task< > is just a future and having both is pointless (?))
So, in general if I read it as :
"async" = "coroutine" (hacky C switch + functor encapsulation)
"await" = yield
"Task" = future
then it's pretty intuitive.
What's missing?
Well there are some places that are syntactically very ugly, but possible. (eg. working with IAsyncOperation/IAsyncInfo in general is super ugly; also the lack of simple "await x,y,z" is a mistake IMO).
There seems to be no way to easily automatically promote a synchronous function to async. That is,
if you have something like :
int func1(int x) { return x+1; }
and you want to run it on a future of an int (Task< int >) , what you really want is just a simple syntax like :
future
which makes a coroutine that waits for its args to be ready and then runs the synchronous function.
(maybe it's possible to write a helper template that does this?)
<
int> x = some async func that returns an int
future<
int> y = start func1( x );
Now it's tempting to do something like :
future
and you see that all the time in example code,
but of course that is not the same thing at all and has many drawbacks (it waits immediately even though "y"
might not be needed for a while, it doesn't allow you to create async dependency chains, it requires you are
already running as a coroutine, etc).
<
int> x = some async func that returns an int
int y = func1( await x );
The bigger issue is that it's not a real stackful coroutine system, which means it's not "composable",
something I've written about before :
cbloom rants 06-21-12 - Two Alternative Oodles
cbloom rants 10-26-12 - Oodle Rewrite Thoughts
Specifically, a coroutine cannot call another function that does the await. This makes sense if you think of the "await" as being the hacky C-switch-#define thing, not a real language construct. The "async" on the func is the "switch {" and the "await" is a "case ". You cannot write utility functions that are usable in coroutines and may await.
To call functions that might await, they must be run as their own separate coroutine. When they await,
they block their own coroutine, not your calling function. That is :
int helper( bool b , AsyncStream s )
{
if ( b )
{
return 0;
}
else
{
int x = await s.Get
The idea here is that "myfunc1" is a coroutine, it calls a function ("helper") which does a yield; that
yields out of the parent coroutine (myfunc1).
That does not work and is not allowed. It is what I would like to see in a good coroutine-centric language.
Instead you have to do something like :
<
int>();
return x + 10;
}
}
async Task<
int> myfunc1()
{
AsyncStream s = open it;
int x = helper( true, s );
return x;
}
async Task
Here "helper" is its own coroutine, and we have to block on it. Now it is worth noting that because WinRT
is aggressive about delaying heap-allocation of coroutines and is aggresive about running coroutines
immediately, the actual flow of the two cases is not very different.
<
int> helper( bool b , AsyncStream s )
{
if ( b )
{
return 0;
}
else
{
int x = await s.Get<
int>();
return x + 10;
}
}
async Task<
int> myfunc1()
{
AsyncStream s = open it;
int x = await helper( true, s );
return x;
}
To be extra clear : lack of composability means you can't just have something like "cofread" which acts like synchronous fread , but instead of blocking the thread when it doesn't have enough data, it yields the coroutine.
You also can't write your own "cosemaphore" or "comutex" that yield instead of waiting the thread. (does WinRT provide cosemaphore and comutex? To have a fully functional coroutine-centric language you need all that kind of stuff. What does the normal C# Mutex do when used in a coroutine? Block the whole thread?)
There are a few places in the syntax that I find very dangerous due to their (false) apparent simplicity.
1. Args to coroutines are often references. When the coroutine is packaged into a struct and delayed
execution, what you get is a non-thread-safe pointer to some shared object. It's incredibly easy to
write code like :
async void func1( SomeStruct^ s )
{
s->DoStuff();
MoreStuff( s );
}
where in fact every touch of 's' is potentially a race and bug.
2. There is no syntax required to start a coroutine. This means you have no idea if functions are
async or not at the call site!
void func2()
{
DeleteFile("b");
CopyFile("a","b");
}
Does this code work? No idea! They might be coroutines, in which case DeleteFile might return before it's
done, and then I would be calling CopyFile before the delete. (if it is a coroutine, the fix is to call
"await", assuming it returned a Task).
Obviously the problem arises from side effects. In this case the file system is the medium for communicating side effects. To use coroutine/future code cleanly, you need to try to make all functions take all their inputs as arguments, and to return all their effects are return values. Even if the return is not necessary, you must return some kind of proxy to the change as a way of expressing the dependency.
"async void" functions are probably bad practice in general; you should at least return a Task with no data (future< void >) so that the caller has something to wait on if they want to. async functions with side effects are very dangerous but also very common. The fantasy that we'll all write pure functions that only read their args (by value) and put all output in their return values is absurd.
It's pretty bold of them to make this the official way to write new code for Windows. As an experimental C# language feature, I think it's pretty decent. But good lord man. Race city, here we come. The days of software having repeatable outcomes are over!
As a software design point, the whole idea that "async improves responsiveness" is rather disturbing. We're gonna get a lot more trickle-in GUIs, which is fucking awful. Yes, async is great for making tasks that the user *expects* to be slow to run in the background. What it should not be used for is hiding the slowness of tasks that should in fact be instant. Like when you open a new window, it should immediately appear fully populated with all its buttons and graphics - if there are widgets in the window that take a long time to appear, they should be fixed or deleted, not made to appear asynchronously.
The way web pages give you an initial view and then gradually trickle in updates? That is fucking awful and should be used as a last resort. It does not belong in applications where you have control over your content. But that is exactly what is being heavily pushed by MS for all WinRT apps.
Having buttons move around after they first appeared, or having buttons appear after the window first opened - that is *terrible* software.
(Windows 8 is of course itself an example; part of their trick for speeding up startup is to put more things delayed until after startup. You now have to boot up, and then sit there and twiddle your thumbs for a few minutes while it actually finishes starting up. (there are some tricks to reduce this, such as using Task Scheduler to force things to run immediately at the user login event))
Some links :
Jerry Nixon @work Windows 8 The right way to Read & Write Files in WinRT
Task.Wait and �Inlining� - .NET Parallel Programming - Site Home - MSDN Blogs
CreateThread for Windows 8 Metro - Shawn Hargreaves Blog - Site Home - MSDN Blogs
Diving deep with WinRT and await - Windows 8 app developer blog - Site Home - MSDN Blogs
Exposing .NET tasks as WinRT asynchronous operations - Windows 8 app developer blog - Site Home - MSDN Blogs
Windows 8 File access sample in C#, VB.NET, C++, JavaScript for Visual Studio 2012
Futures and promises - Wikipedia, the free encyclopedia
Effective Go - The Go Programming Language
Deceptive simplicity of async and await
async (C# Reference)
Asynchronous Programming with Async and Await (C# and Visual Basic)
Creating Asynchronous Operations in C++ for Windows Store Apps
Asynchronous Programming - Easier Asynchronous Programming with the New Visual Studio Async CTP
Asynchronous Programming - Async Performance Understanding the Costs of Async and Await
Asynchronous Programming - Pause and Play with Await
Asynchronous programming in C++ (Windows Store apps) (Windows)
AsyncAwait Could Be Better - CodeProject
File Manipulation in Windows 8 Store Apps
SharpGIS Reading and Writing text files in Windows 8 Metro
2 comments:
In your example with helper just remove both awaits and you will have what you want.
@OmariO - err I think you're missing the point and picking on a technicality. You should pretend that the example is not as trivial as it is.
Post a Comment