阿里云主机折上折
  • 微信号
Current Site:Index > Generators and coroutines

Generators and coroutines

Author:Chuan Chen 阅读数:49096人阅读 分类: Node.js

Basic Concepts of Generators

Generators are a special type of function in JavaScript that can be defined using the function* syntax. Unlike regular functions, generator functions can pause execution and resume later, making them particularly suitable for handling asynchronous operations and iteration scenarios. When a generator function is executed, it does not immediately run the function body but instead returns a generator object.

function* simpleGenerator() {
  yield 1;
  yield 2;
  yield 3;
}

const gen = simpleGenerator();
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3

Generators use the yield keyword to pause function execution and return a value. Each time the generator's next() method is called, the function resumes execution from where it was last paused until it encounters the next yield or the function ends.

Internal Mechanism of Generators

When a generator function is called, it does not immediately execute the function body but returns a generator object that implements the iterator protocol. This object maintains the function's execution context, including local variables, parameters, and the current execution position. When the next() method is called, the generator resumes execution until it encounters a yield expression.

function* counterGenerator() {
  let count = 0;
  while(true) {
    count += 1;
    yield count;
  }
}

const counter = counterGenerator();
console.log(counter.next().value); // 1
console.log(counter.next().value); // 2
console.log(counter.next().value); // 3

Generators can also delegate to another generator using the yield* expression, which is useful when combining multiple generators:

function* generatorA() {
  yield 'a';
  yield 'b';
}

function* generatorB() {
  yield* generatorA();
  yield 'c';
  yield 'd';
}

const gen = generatorB();
console.log([...gen]); // ['a', 'b', 'c', 'd']

Concept and Implementation of Coroutines

Coroutines are a lighter-weight concurrency programming model compared to threads, allowing cooperative scheduling of multiple execution flows within a single thread. In JavaScript, generators can be seen as an implementation of coroutines because they allow functions to pause and resume execution during their lifecycle.

Key differences between coroutines and threads:

  • Coroutines are cooperative and require explicit yielding of control
  • Coroutines are implemented in user space and do not rely on operating system scheduling
  • Coroutine switching overhead is much lower than thread switching
function* coroutineA() {
  console.log('A1');
  yield;
  console.log('A2');
  yield;
  console.log('A3');
}

function* coroutineB() {
  console.log('B1');
  yield;
  console.log('B2');
  yield;
  console.log('B3');
}

const a = coroutineA();
const b = coroutineB();

a.next(); // A1
b.next(); // B1
a.next(); // A2
b.next(); // B2
a.next(); // A3
b.next(); // B3

Generators and Asynchronous Programming

Generators are commonly used in Node.js to simplify asynchronous code. By combining Promises with generators, asynchronous flow control can be achieved with a synchronous-like coding style. This pattern is widely used in the Koa framework.

function asyncTask(value) {
  return new Promise(resolve => {
    setTimeout(() => resolve(value * 2), 1000);
  });
}

function* asyncGenerator() {
  const result1 = yield asyncTask(1);
  console.log(result1); // 2
  const result2 = yield asyncTask(2);
  console.log(result2); // 4
  return result1 + result2;
}

function runGenerator(gen) {
  const iterator = gen();
  
  function iterate(iteration) {
    if (iteration.done) return Promise.resolve(iteration.value);
    return Promise.resolve(iteration.value)
      .then(x => iterate(iterator.next(x)))
      .catch(e => iterate(iterator.throw(e)));
  }
  
  return iterate(iterator.next());
}

runGenerator(asyncGenerator).then(result => {
  console.log('Final result:', result); // 6
});

Generators and the Iterator Protocol

Generators inherently implement the iterator protocol, allowing them to seamlessly integrate with JavaScript's iteration syntax. They can be used anywhere an iterable object is expected.

function* fibonacci() {
  let [prev, curr] = [0, 1];
  while (true) {
    yield curr;
    [prev, curr] = [curr, prev + curr];
  }
}

// Using for...of loop
for (const n of fibonacci()) {
  if (n > 1000) break;
  console.log(n);
}

// Working with array destructuring
const [first, second, third] = fibonacci();
console.log(first, second, third); // 1 1 2

Advanced Usage of Generators

Generators can not only produce values but also receive external input through the next() method. This enables bidirectional communication, further enhancing their flexibility.

function* twoWayGenerator() {
  const name = yield 'What is your name?';
  const age = yield `Hello ${name}, how old are you?`;
  return `${name} is ${age} years old`;
}

const gen = twoWayGenerator();
console.log(gen.next().value); // "What is your name?"
console.log(gen.next('Alice').value); // "Hello Alice, how old are you?"
console.log(gen.next(30).value); // "Alice is 30 years old"

Generators can also be used to implement state machines, encapsulating complex state transition logic within generator functions:

function* trafficLight() {
  while (true) {
    yield 'red';
    yield 'yellow';
    yield 'green';
    yield 'yellow';
  }
}

const light = trafficLight();
console.log(light.next().value); // 'red'
console.log(light.next().value); // 'yellow'
console.log(light.next().value); // 'green'
console.log(light.next().value); // 'yellow'
console.log(light.next().value); // 'red'

Practical Applications in Node.js

In the Node.js ecosystem, generators are widely used in various scenarios. The Koa framework is built on generators, utilizing them to handle middleware flow.

const Koa = require('koa');
const app = new Koa();

app.use(function* (next) {
  const start = Date.now();
  yield next;
  const ms = Date.now() - start;
  this.set('X-Response-Time', `${ms}ms`);
});

app.use(function* (next) {
  const start = Date.now();
  yield next;
  const ms = Date.now() - start;
  console.log(`${this.method} ${this.url} - ${ms}`);
});

app.use(function* () {
  this.body = 'Hello World';
});

app.listen(3000);

Generators are also commonly used for processing data streams, especially when handling large datasets, to effectively control memory usage:

const fs = require('fs');
const readline = require('readline');

function* readLines(filePath) {
  const fileStream = fs.createReadStream(filePath);
  const rl = readline.createInterface({
    input: fileStream,
    crlfDelay: Infinity
  });

  for await (const line of rl) {
    yield line;
  }
}

async function processLargeFile() {
  const lineReader = readLines('large-file.txt');
  for (let i = 0; i < 10; i++) {
    const { value } = await lineReader.next();
    console.log(value);
  }
}

processLargeFile();

Performance Considerations for Generators and Coroutines

While generators provide powerful control flow abstractions, there are some performance considerations. The creation and switching of generators incur more overhead than regular function calls, so they should be used cautiously in performance-critical code paths.

Node.js's V8 engine has optimized generators, but caution is still needed when using them at scale:

  • Creation and destruction of generator objects incur additional overhead
  • Generator switching is approximately 2-5 times slower than function calls
  • Frequent use of generators in hot code paths may impact performance
// Performance test: generator vs regular function
function normalFunction(n) {
  let sum = 0;
  for (let i = 0; i < n; i++) {
    sum += i;
  }
  return sum;
}

function* generatorFunction(n) {
  let sum = 0;
  for (let i = 0; i < n; i++) {
    sum += i;
    yield;
  }
  return sum;
}

console.time('normal');
normalFunction(1e6);
console.timeEnd('normal'); // ~1-3ms

console.time('generator');
const gen = generatorFunction(1e6);
let result;
while (!(result = gen.next()).done) {}
console.timeEnd('generator'); // ~10-30ms

Error Handling in Generators

Generators provide robust error handling mechanisms, allowing errors to be injected into the generator internally via the throw() method. This enables generators to handle errors similarly to synchronous code.

function* errorHandlingGenerator() {
  try {
    yield 'Step 1';
    yield 'Step 2';
    throw new Error('Something went wrong');
    yield 'Step 3';
  } catch (err) {
    console.log('Caught error:', err.message);
    yield 'Recovery step';
  } finally {
    console.log('Cleaning up');
    yield 'Cleanup step';
  }
}

const gen = errorHandlingGenerator();
console.log(gen.next().value); // "Step 1"
console.log(gen.next().value); // "Step 2"
console.log(gen.throw(new Error('Injected error')).value); // "Recovery step"
console.log(gen.next().value); // "Cleanup step"

Generators and Recursion

Generators can effectively handle recursive algorithms, especially in scenarios requiring lazy evaluation or infinite sequence generation. Generator recursion typically uses the yield* syntax.

function* traverseTree(node) {
  if (!node) return;
  yield node.value;
  yield* traverseTree(node.left);
  yield* traverseTree(node.right);
}

const tree = {
  value: 1,
  left: {
    value: 2,
    left: { value: 4 },
    right: { value: 5 }
  },
  right: {
    value: 3,
    left: { value: 6 },
    right: { value: 7 }
  }
};

console.log([...traverseTree(tree)]); // [1, 2, 4, 5, 3, 6, 7]

For more complex recursive scenarios, generators can prevent stack overflow issues by yielding results incrementally without computing all values at once:

function* fibonacciSequence(n, prev = 0, curr = 1) {
  if (n <= 0) return;
  yield curr;
  yield* fibonacciSequence(n - 1, curr, prev + curr);
}

console.log([...fibonacciSequence(10)]); // [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Generators and Concurrency Patterns

Generators can be used to implement various concurrency patterns, such as producer-consumer models, pipeline processing, and parallel task coordination. These patterns are particularly useful in Node.js's I/O-intensive applications.

function* producer(items) {
  for (const item of items) {
    console.log('Producing', item);
    yield item;
  }
}

function* consumer() {
  let count = 0;
  while (true) {
    const item = yield;
    console.log('Consuming', item);
    count++;
    if (count >= 5) break;
  }
}

function runPipeline() {
  const items = [1, 2, 3, 4, 5, 6, 7, 8];
  const prod = producer(items);
  const cons = consumer();
  
  cons.next(); // Start consumer
  
  for (const item of prod) {
    cons.next(item); // Pass each item to consumer
  }
}

runPipeline();

For more complex concurrency control, coroutine pools can be implemented by combining Promises with generators:

async function runInPool(generators, poolSize) {
  const executing = new Set();
  const results = [];
  
  for (const gen of generators) {
    const promise = Promise.resolve().then(() => gen.next().value);
    executing.add(promise);
    
    const cleanUp = () => executing.delete(promise);
    promise.then(cleanUp).catch(cleanUp);
    
    if (executing.size >= poolSize) {
      await Promise.race(executing);
    }
  }
  
  return Promise.all(executing);
}

const tasks = [
  function*() { yield asyncTask(1); return 1; },
  function*() { yield asyncTask(2); return 2; },
  function*() { yield asyncTask(3); return 3; },
  function*() { yield asyncTask(4); return 4; },
  function*() { yield asyncTask(5); return 5; }
];

runInPool(tasks, 2).then(console.log); // [1, 2, 3, 4, 5]

Alternatives to Generators

While generators are powerful, modern JavaScript's async/await syntax typically provides a more concise solution for most asynchronous programming problems. Understanding the relationship between generators and async/await helps in better tool selection.

// Using generators
function* oldWay() {
  const a = yield Promise.resolve(1);
  const b = yield Promise.resolve(2);
  return a + b;
}

// Using async/await
async function newWay() {
  const a = await Promise.resolve(1);
  const b = await Promise.resolve(2);
  return a + b;
}

However, generators still have advantages in certain scenarios:

  • When fine-grained control over execution flow is needed
  • When implementing custom iteration logic
  • When bidirectional communication coroutines are required
  • When handling non-Promise asynchronous values
// Generators handling non-Promise asynchronous values
function* eventGenerator() {
  const clicks = yield (next) => {
    document.addEventListener('click', next);
    return () => document.removeEventListener('click', next);
  };
  return `Clicked at ${clicks.clientX}, ${clicks.clientY}`;
}

function runEventGenerator(gen) {
  const iterator = gen();
  let cleanup;
  
  function step(value) {
    const result = iterator.next(value);
    if (result.done) return result.value;
    
    if (typeof result.value === 'function') {
      cleanup = result.value(step);
    } else {
      Promise.resolve(result.value).then(step);
    }
  }
  
  step();
  return () => cleanup && cleanup();
}

const stop = runEventGenerator(eventGenerator);
// Prints coordinates when page is clicked
setTimeout(stop, 5000); // Stop listening after 5 seconds

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.