Slide 1

Slide 1 text

An Application Perspective MANAGING ASYNCHRONY

Slide 2

Slide 2 text

TYPES OF ASYNCHRONY

Slide 3

Slide 3 text

DETERMINISTIC ORDER.

Slide 4

Slide 4 text

(I/O)

Slide 5

Slide 5 text

NON-DETERMINISTIC ORDER.

Slide 6

Slide 6 text

(USER EVENTS)

Slide 7

Slide 7 text

THESE ARE DIFFERENT. Because both are "events", we reach for the same tools when building abstractions.

Slide 8

Slide 8 text

APPLICATION CODE SHOULD USUALLY NOT BE ASYNC. In some cases, apps will define their own abstractions for the asynchrony. In a few cases (chat servers), it may be appropriate. In most cases, we can abstract away callbacks from application logic through abstraction.

Slide 9

Slide 9 text

CONTEXT.

Slide 10

Slide 10 text

fs.readFile("/etc/passwd", function(err, data) { if (err) { throw err; } console.log(data); }); ASYNC CODE. Scheduler Initial Program Primitive Status Primitive Value Error Condition Application Callback

Slide 11

Slide 11 text

var value; callbacks = new Map; callbacks.set(function() { return true; }, program); while(callbacks.length) { callbacks.forEach(function(pollStatus, callback) { if (value = pollStatus()) { callback(value); } }); sleep(0.1); } SCHEDULER. Scheduler Initial Program Primitive Status Primitive Value Error Condition Application Callback

Slide 12

Slide 12 text

SCHEDULER. true, program callbacks

Slide 13

Slide 13 text

SCHEDULER. callbacks poll, callback poll, callback poll, callback poll, callback poll, callback

Slide 14

Slide 14 text

SCHEDULER.

Slide 15

Slide 15 text

fs.readFile("/etc/passwd", function(err, data) { if (err) { throw err; } console.log(data); }); ASYNC CODE. Scheduler Initial Program Primitive Status Primitive Value Error Condition Application Callback true, program

Slide 16

Slide 16 text

fs.readFile("/etc/passwd", function(err, data) { if (err) { throw err; } console.log(data); }); ASYNC CODE. Scheduler Initial Program Primitive Status Primitive Value Error Condition Application Callback fileReady, callback

Slide 17

Slide 17 text

fs.readFile("/etc/passwd", function(err, data) { if (err) { throw err; } console.log(data); }); ASYNC CODE. Scheduler Initial Program Primitive Status Primitive Value Error Condition Application Callback

Slide 18

Slide 18 text

$ man select Select() examines the I/O descriptor sets whose addresses are passed in readfds, writefds, and errorfds to see if some of their descriptors are ready for reading, ... PRIMITIVE STATUS. Scheduler Initial Program Primitive Status Primitive Value Error Condition Application Callback

Slide 19

Slide 19 text

fcntl(fd, F_SETFL, flags | O_NONBLOCK); read(fd, buffer, 100); PRIMITIVE VALUE. Scheduler Initial Program Primitive Status Primitive Value Error Condition Application Callback

Slide 20

Slide 20 text

fcntl(fd, F_SETFL, flags | O_NONBLOCK); error = read(fd, buffer, 100); if (error === -1) { callback(errorFrom(errno)); } else { callback(null, buffer); } RESULT. Scheduler Initial Program Primitive Status Primitive Value Error Condition Application Callback

Slide 21

Slide 21 text

SCHEDULER. start program receive callbacks: [ io, callback ] program finishes select on the list of all passed IOs IO is ready for a callback invoke callback

Slide 22

Slide 22 text

WHEN ORDER IS DETERMINISTIC, WE WANT A SEQUENTIAL ABSTRACTION.

Slide 23

Slide 23 text

THREADS.

Slide 24

Slide 24 text

fs.readFile("/etc/passwd", function(err, data) { console.log(data); }); fs.readFile("/etc/sudoers", function(err, data) { console.log(data); }); ASYNC.

Slide 25

Slide 25 text

CALLBACK. function body visible variables

Slide 26

Slide 26 text

new Thread(function() { var data = fs.readFile("/etc/passwd"); console.log(data); }); new Thread(function() { var data = fs.readFile("/etc/sudoers"); console.log(data); }); sleep(); THREADS.

Slide 27

Slide 27 text

new Thread(function() { var data = fs.readFile("/etc/passwd"); console.log(data); }); new Thread(function() { var data = fs.readFile("/etc/sudoers"); console.log(data); }); sleep(); THREADS. Scheduler Initial Program Primitive Status Primitive Value Error Condition Application Callback

Slide 28

Slide 28 text

new Thread(function() { var data = fs.readFile("/etc/passwd"); console.log(data); }); new Thread(function() { var data = fs.readFile("/etc/sudoers"); console.log(data); }); sleep(); THREADS. Scheduler Initial Program Primitive Status Primitive Value Error Condition Application Callback The scheduler does essentially the same thing when using threads.

Slide 29

Slide 29 text

new Thread(function() { var data = fs.readFile("/etc/passwd"); console.log(data); }); new Thread(function() { var data = fs.readFile("/etc/sudoers"); console.log(data); }); sleep(); THREADS. Scheduler Initial Program Primitive Status Primitive Value Error Condition Application Callback Same with initial program.

Slide 30

Slide 30 text

STACK. entry stack frame stack frame stack frame current stack frame stack frame local variable values + current position The main difference with threads is that the callback structure is more complicated. callback resume thread

Slide 31

Slide 31 text

SELECT + READ. Scheduler Initial Program Primitive Status Primitive Value Error Condition Application Callback

Slide 32

Slide 32 text

new Thread(function() { var data = fs.readFile("/etc/passwd"); console.log(data); }); new Thread(function() { var data = fs.readFile("/etc/sudoers"); console.log(data); }); sleep(); THREADS. Scheduler Initial Program Primitive Status Primitive Value Error Condition Application Callback When data is ready, resume the associated thread.

Slide 33

Slide 33 text

SCHEDULER. start program and sub-threads receive callbacks: [ io, thread ] main program pauses select on the list of all passed IOs IO is ready invoke callback (resume associated thread)

Slide 34

Slide 34 text

■ Possible simultaneous code ■ Can eliminate if desired via a GIL ■ Unexpected interleaved code and context switching overhead ■ Can eliminate if desired by disabling pre- emptive scheduling ■ More memory required for callback structure DIFFERENCES. The thread abstraction, which is useful to manage asynchronous events with deterministic order, has varying implementation-defined characteristics. It will always require more memory.

Slide 35

Slide 35 text

fs.readFile("/etc/passwd", function(err, data) { console.log(data); }); fs.readFile("/etc/sudoers", function(err, data) { console.log(data); }); ASYNC. Async code can and usually does still have global state and interleaved execution.

Slide 36

Slide 36 text

CONFUSION.

Slide 37

Slide 37 text

"FIBERS ARE LIKE THREADS WITHOUT THE PROBLEMS"

Slide 38

Slide 38 text

new Fiber(function() { var data = fs.readFile("/etc/passwd"); console.log(data); }); new Fiber(function() { var data = fs.readFile("/etc/sudoers"); console.log(data); }); sleep(); FIBERS. Scheduler Initial Program Primitive Status Primitive Value Error Condition Application Callback Same with initial program.

Slide 39

Slide 39 text

fs.readFile = function(filename) { var fiber = Fiber.current; fs.readAsync(filename, function(err, data) { fiber.resume(data); }); return Fiber.yield(); }; IMPLEMENTATION. Scheduler Initial Program Primitive Status Primitive Value Error Condition Application Callback Fibers implement the status and value parts of the scheduler in the language, but fundamentally have the same data structures as threads.

Slide 40

Slide 40 text

CALLBACK. entry stack frame stack frame stack frame current stack frame stack frame local variable values + current position callback resume thread The fact that fibers are "lighter" is an implementation detail. Having to implement manual yielding is a pain.

Slide 41

Slide 41 text

Threads are useful when working with asynchronous events that arrive in a deterministic order. “ TO RECAP.

Slide 42

Slide 42 text

WHEN TO USE CALLBACKS.

Slide 43

Slide 43 text

var Stats = Ember.View.extend({ templateName: 'stats', didInsertElement: function() { this.$().flot(); } }); Stats.create().append(); // vs. var Stats = Ember.View.extend({ templateName: 'stats' }); var view = yield Stats.create().append(); view.$().flot(); APPROACHES.

Slide 44

Slide 44 text

■ Encapsulation: The caller needs to know about the call to flot() ■ Resiliance to Errors: All callers needs to remember to call flot() ■ Composability: The framework can no longer simply ask for a view and render it as needed PROBLEMS.

Slide 45

Slide 45 text

LIFECYCLE HOOKS SHOULD BE USED TO AID ENCAPSULATION.

Slide 46

Slide 46 text

JAVASCRIPT.

Slide 47

Slide 47 text

INVARIANT: TWO ADJACENT STATEMENTS MUST RUN TOGETHER. This is a core guarantee of the JavaScript programming model and cannot be changed without breaking existing code.

Slide 48

Slide 48 text

new Thread(function() { var data = fs.readFile("/etc/passwd"); console.log(data); }); PROBLEM. In this case, readFile implicitly halts execution and allows other code to run. This violates guarantees made by JS.

Slide 49

Slide 49 text

BUT SEQUENTIAL ABSTRACTIONS ARE STILL USEFUL!

Slide 50

Slide 50 text

"GENERATORS"

Slide 51

Slide 51 text

var task = function*() { var json = yield jQuery.getJSON(url); var element = $(template(json)).appendTo('body'); yield requestAnimationFrame(); element.fadeIn(); }; var scheduler = new Scheduler; scheduler.schedule(task); YIELDING.

Slide 52

Slide 52 text

var task = function*() { var json = yield jQuery.getJSON(url); var element = $(template(json)).appendTo('body'); yield requestAnimationFrame(); element.fadeIn(); }; var scheduler = new Scheduler; scheduler.schedule(task); YIELDING. Scheduler Initial Program Primitive Status Primitive Value Error Condition Application Callback Here, the task is yielding an object that knows how to be resumed.

Slide 53

Slide 53 text

STATUS AND VALUE. Thus far, we have limited status and value to built-in primitives that the VM knows how to figure out. In order for generators to be useful, we will need to expose those concepts to userland.

Slide 54

Slide 54 text

PROMISES.

Slide 55

Slide 55 text

file.read = function(filename) { var promise = new Promise(); fs.waitRead(filename, function(err, file) { if (err) { promise.error(err); } else { promise.resolve(file.read()); } }); return promise; } PROMISES. Scheduler Initial Program Primitive Status Primitive Value Error Condition Application Callback Here, we are still allowing the VM to let us know when the file is ready, but we control the callback manually.

Slide 56

Slide 56 text

var prompt = function() { var promise = new Promise(); $("input#confirm") .show() .one('keypress', function(e) { if (e.which === 13) { promise.resolve(this.value); } }); return promise(); }; spawn(function*() { var entry = yield prompt(); console.log(entry); }); BETTER PROMPT. Scheduler Initial Program Primitive Status Primitive Value Error Condition Application Callback In this case, we are in control of both the status information and the value of the status.

Slide 57

Slide 57 text

PROMISES ARE A PRIMITIVE. Promises provide a shared implementation of status/value that a scheduler can use. In JavaScript, generators + promises give us a way to use a sequential abstraction for asynchronous events with deterministic order. In a UI app, a small part of the total interface may have deterministic order, and we can use this abstraction on demand. In general, I would prefer to use promises together with a higher level sequential code abstraction than use promises directly in application code.

Slide 58

Slide 58 text

var spawn = function(generator, value) { try { promise = generator.send(value); promise.success(function(value) { spawn(generator, value); }) promise.fail(function(err) { generator.throw(err); }) } catch(e) { if (!isStopIteration(e)) { generator.throw(err); } } }; PSEUDOCODE. Scheduler Initial Program Primitive Status Primitive Value Error Condition Application Callback

Slide 59

Slide 59 text

GENERATORS ARE EXPLICIT AND SHALLOW. Because generators are explicit and shallow, they don't have a parent stack to store, so their memory usage is more like callbacks. Generators can be explicitly chained though.

Slide 60

Slide 60 text

OTHER LANGUAGES?

Slide 61

Slide 61 text

class File def read promise = Promise.new async_read do |string| promise.resolve(string) end Thread.yield promise end end BETTER PRIMITIVES. In languages that already have threads, promises can be used to provide more power in the existing scheduler. This can be implemented in terms of sync primitives.

Slide 62

Slide 62 text

EXTERNAL EVENTS. These are events that are external to the application or application framework. They are typically handled as asynchronous events. If they have deterministic order, the above techniques may work.

Slide 63

Slide 63 text

EXTERNAL EVENTS. However, you may not want to wait to do anything until the first Ajax request is returned, so you typically will not try to use sequential abstractions even for network I/O.

Slide 64

Slide 64 text

■ External events ■ I/O responses (IndexedDB, Ajax) ■ setTimeout and setInterval ■ DOM Mutation Observers ■ User Interaction ■ click ■ swipe ■ unload TWO TYPES.

Slide 65

Slide 65 text

INTERNAL EVENTS. These are events generated by the app or app framework for another part of the app or app framework.

Slide 66

Slide 66 text

var EventedObject = function(properties) { for (var property in properties) { this[property] = properties[property]; } }; Object.prototype.set = function(key, value) { EventEmitter.fire(this, key + ':will-change'); this[key] = value; EventEmitter.fire(this, key + ':did-change'); }; EVENTED MODELS.

Slide 67

Slide 67 text

var person = new EventedObject({ firstName: "Yehuda", lastName: "Katz" }); $("#person").html("

" + person.firstName + '' + person.lastName + "

"); EventEmitter.on(person, 'firstName:did-change', function() { $("#person span:first").html(person.firstName); }); EventEmitter.on(person, 'lastName:did-change', function() { $("#person span:last").html(person.lastName); }); DECOUPLING.

Slide 68

Slide 68 text

$.getJSON("/person/me", function(json) { person.set('firstName', json.firstName); person.set('lastName', json.lastName); }); NETWORK.

Slide 69

Slide 69 text

DECOUPLING. network model rendering

Slide 70

Slide 70 text

var person = new EventedObject({ firstName: "Yehuda", lastName: "Katz", fullName: function() { return [this.firstName, this.lastName].join(' '); } }); $("#person").html("

" + person.fullName() + "

"); EventEmitter.on(person, 'firstName:did-change', function() { $("#person p").html(person.fullName()); }); EventEmitter.on(person, 'lastName:did-change', function() { $("#person p").html(person.fullName()); }); PROBLEMS.

Slide 71

Slide 71 text

$.getJSON("/person/me", function(json) { person.set('firstName', json.firstName); person.set('lastName', json.lastName); }); IF WE DO THIS:

Slide 72

Slide 72 text

DOUBLE RENDER!

Slide 73

Slide 73 text

TRANSACTIONS.

Slide 74

Slide 74 text

var person = new EventedObject({ firstName: "Yehuda", lastName: "Katz", fullName: function() { return [this.firstName, this.lastName].join(' '); } }); EventEmitter.on(person, 'firstName:did-change', function() { UniqueEmitter.fire(person, 'fullName:did-change'); }); EventEmitter.on(person, 'lastName:did-change', function() { UniqueEmitter.fire(person, 'fullName:did-change'); }); UniqueEmitter.on(person, 'fullName:did-change', function() { $("#person p").html(person.fullName()); }); COALESCING.

Slide 75

Slide 75 text

$.getJSON("/person/me", function(json) { UniqueEmitter.begin(); person.set('firstName', json.firstName); person.set('lastName', json.lastName); UniqueEmitter.commit(); }); NETWORK.

Slide 76

Slide 76 text

GETS COMPLICATED. This type of solution is not a very good user-facing abstraction, but it is a good primitive.

Slide 77

Slide 77 text

DATA FLOW. What we want is an abstraction that describes the data flow via data bindings.

Slide 78

Slide 78 text

var person = Ember.Object.extend({ firstName: "Yehuda", lastName: "Katz", fullName: function() { return [this.get('firstName'), this.get('lastName')].join(' '); }.property('firstName', 'lastName') }); $("#person").html("

" + person.get('fullName') + "

"); person.addObserver('fullName', function() { $("#person p").html(person.get('fullName')); }); DECLARATIVE. The .property is the way we describe that changes to firstName and lastName affect a single output.

Slide 79

Slide 79 text

$.getJSON("/person/me", function(json) { person.set('firstName', json.firstName); person.set('lastName', json.lastName); }); NETWORK. Behind the scenes, Ember defers the propagation of the changes until the turn of the browser's event loop. Because we know the dependencies of fullName, we can also coalesce the changes to firstName and lastName and only trigger the fullName observer once.

Slide 80

Slide 80 text

BOUNDARIES. A good data binding system can provide an abstraction for data flow for objects that implement observability. For external objects and events, you start with asynchronous observers (either out from the binding system or in from the outside world).

Slide 81

Slide 81 text

BETTER ABSTRACTIONS. For common cases, we can do better.

Slide 82

Slide 82 text

var person = Ember.Object.create({ firstName: "Yehuda", lastName: "Katz", fullName: function() { return [this.get('firstName'), this.get('lastName')].join(' '); }.property('firstName', 'lastName') }); var personView = Ember.Object.create({ person: person, fullNameBinding: 'person.fullName', template: compile("

{{fullName}}

") }); person.append(); $.getJSON("/person/me", function(json) { person.set('firstName', json.firstName); person.set('lastName', json.lastName); }); EXTENDED REACH. For common boundary cases, like DOM, we can wrap the external objects in an API that understands data-binding. This means that we won't need to write async code to deal with that boundary.

Slide 83

Slide 83 text

SAME SEMANTICS. If you extend an async abstraction to an external system, make sure that the abstraction has the same semantics when dealing with the outside world. In Ember's case, the same coalescing guarantees apply to DOM bindings.

Slide 84

Slide 84 text

CONCLUSION.

Slide 85

Slide 85 text

LIMITED ASYNC CODE IN APPLICATIONS. In general, applications should not have large amounts of async code. In many cases, an application will want to expose abstractions for its own async concerns that allow the bulk of the application code to proceed without worrying about it. One common pattern is using an async reactor for open connections but threads for the actual work performed for a particular open connection.

Slide 86

Slide 86 text

DIFFERENT KINDS OF ASYNC CODE. Asynchronous code that arrives in a strict deterministic order can make use of different abstractions that async code that arrives in non-deterministic order. Internal events can make use of different abstractions than external events.

Slide 87

Slide 87 text

LAYERS. Promises are a nice abstraction on top of async code, but they're not the end of the story. Building task.js on top of promises gives us three levels of abstraction for appropriate use as needed.

Slide 88

Slide 88 text

THANK YOU.

Slide 89

Slide 89 text

QUESTIONS?