10 ms·
This kind of "transparent async" (like what golang also does) always has the danger of creating race-condition-style bugs, because some synchronous-looking piec
by Arnavion 6y ago
This kind of "transparent async" (like what golang also does) always has the danger of creating race-condition-style bugs, because some synchronous-looking piece of code doesn't run synchronously so some state at the beginning of the function has a different value at the end of the function. Have you had those kind of problems with your Java codebase?
- kjeetgill 6y ago> some synchronous-looking piece of code doesn't run synchronously so some state at the beginning of the function has a different value at the end of the function Does function coloring solve this? I don't think it's the case. Python, JS, etc. don't see this issue much because the interpreters tend to be single threaded or have GILs. C#, for instance, would see all the same issues a Java or Go program might with respect to variables changing over time.
- pas 6y agoOh, JS is full of such bugs, because it's trivial to write a loop that fires off async closures (Promises) that capture something by reference, so they then end up trampling all over each other.
- simiones 6y agoThis problem can also easily happen with async code, if you have any shared state with the async code, especially if you are running multiple async calls in parallel. It can even happen in single-threaded runtimes like Node, where people are even less likely to expect it.
- Arnavion 6y agoMy point is that "transparent async" code can lull the programmer into a false sense of security. Contrived example: void ChargeAccount(Account account, int cents) { int balance = account.balance; int newBalance = balance - cents; Logger.Write("changing account from {balance} to {newBalance}"); account.balance = newBalance; } This works fine because it's all synchronous code. Then someone classloads a Logger implementation that's "transparent async" because it logs to a network sink in addition to synchronous stderr, and now there's a chance two calls to ChargeAccount interleave and one of the charges is lost. If it's explicitly async with async-await keywords etc, then the race is easily noticeable and the programmer will know they have to use a mutex or atomics or whatever.
- simiones 6y agoI don't think I agree. If there is any other code that touches account.balance from any other thread of execution (OS thread, coroutine, etc), then this code is already not safe. If there isn't any other thread of execution, then this is safe regardless of whether the call to Logger.Write is sync or async. For example, the following program would be safe regardless of whether Logger.Write is sync or async: int main() { InitLogger(); ChargeAccount(acc, 10); // the program will only ever get here after // account.balance = newBalance has finished executing ChargeAccount(acc, 10); } While the following program is not safe even if Logger.Write is sync: int main() { InitLogger(); launchInNewThread(() => ChargeAccount(acc, 10)); launchInNewThread(() => ChargeAccount(acc, 10)); } Transparent async a la Java Loom or Go does not spawn extra threads - it just pauses the current thread and schedules a new thread whenever you're waiting for a sync call (which normally means waiting for the kernel, either in IO or some kind of lock). In fact, the first program if written with something like Loom would be perfectly equivalent to the following C#: async void ChargeAccount(Account account, int cents) { int balance = account.balance; int newBalance = balance - cents; await Logger.Write("changing account from {balance} to {newBalance}"); account.balance = newBalance; } public static void Main(string[] args) { InitLogger(); await ChargeAccount(account, 5); await ChargeAccount(account, 5); } No concurrency regardless of underlying thread pools. You seem to assume it is equivalent to something like this, which would indeed be unsafe, but is not the case: public static void Main(string[] args) { InitLogger(); var t1 = ChargeAccount(account, 5); var t2 = ChargeAccount(account, 5); await Tasks.AwaitAll(t1, t2); }
- fiddlerwoaroof 6y agoYeah, I think this is right: concurrency issues have to be solved by using data structures with concurrency semantics (software transactional memory or atomic refs/ints) and can’t really be solved by something like async/await. Go making you use channels to express asynchronous execution here is a feature, not a bug. async/await and friends (generators, callback-passing, etc.) all provide some clues that are sometimes useful for solving concurrency problems, but they aren’t able to make unsafe programs safe.