Skip to content

Commit

Permalink
boa: support running Python code in non-blocking way (#602)
Browse files Browse the repository at this point in the history
It uses the Node.js worker_threads to start new thread to run Python functions.
Note that, we share the same Python interrupter.
  • Loading branch information
yorkie authored Oct 10, 2020
1 parent 63a39f3 commit 1b50e4e
Show file tree
Hide file tree
Showing 16 changed files with 1,581 additions and 447 deletions.
70 changes: 70 additions & 0 deletions docs/manual/intro-to-boa.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,76 @@ In Node.js version < `v14.x`, you also need to add the [`--experimental-modules`
$ node --experimental-modules --experimental-loader @pipcook/boa/esm/loader.mjs app.mjs
```

## Python functions in `worker_threads`

The `@pipcook/boa` package calls Python function in blocking way, which is because of the Python(CPython)'s object model is not thread-safe, thus Python limits to run Python functions in different threads.

However the `@pipcook/boa` package allows running Python function in another thread with Node.js `worker_threads`, an non-blocking example is:

```js
const { Worker, isMainThread, workerData, parentPort } = require('worker_threads');
const boa = require('@pipcook/boa');
const pybasic = boa.import('tests.base.basic'); // a Python example
const { SharedPythonObject, symbols } = boa;

class Foobar extends pybasic.Foobar {
hellomsg(x) {
return `hello <${x}> on ${this.test}(${this.count})`;
}
}

if (isMainThread) {
const foo = new Foobar();
const worker = new Worker(__filename, {
workerData: {
foo: new SharedPythonObject(foo),
},
});
let alive = setInterval(() => {
const ownership = foo[symbols.GetOwnershipSymbol]();
console.log(`ownership should be ${expectedOwnership}.`);
}, 1000);

worker.on('message', state => {
if (state === 'done') {
console.log('task is completed');
setTimeout(() => {
clearInterval(alive);
console.log(foo.ping('x'));
}, 1000);
}
});
} else {
const { foo } = workerData;
console.log(`worker: get an object${foo} and sleep 5s in Python`);
foo.sleep(); // this is a blocking function which is implemented at Python to sleep 1s

console.log('python sleep is done, and sleep in nodejs(thread)');
setTimeout(() => parentPort.postMessage('done'), 1000);
}
```

In the new sub-thread created by `worker_threads`, the `@pipcook/boa` won't create the new interrupter, it means all the threads by Node.js shares the same Python interpreter to avoid the Python GIL.

To make sure the thread-safty works, we introduce a `SharedPythonObject` class to share the Python objects between threads via the following:

```js
// main thread
const foo = new Foobar(); // Python object
const worker = new Worker(__filename, {
workerData: {
foo: new SharedPythonObject(foo),
},
});

// worker thread
const { workerData } = require('worker_threads');
const boa = require('@pipcook/boa');
console.log(workerData.foo);
```

The `SharedPythonObject` accepts a Python object created by `@pipcook/boa`, once created, the original object won't be used util the worker thread exits, this is to make sure the thread-safty of the shared objects, it means an object could only be used by worker or main thread at the same time.

## Build from source

```bash
Expand Down
30 changes: 30 additions & 0 deletions packages/boa/lib/factory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use strict';

const fs = require('fs');
const path = require('path');
const { Python } = require('bindings')('boa');

// read the conda path from the .CONDA_INSTALL_DIR
// eslint-disable-next-line no-sync
const condaPath = fs.readFileSync(path.join(__dirname, '../.CONDA_INSTALL_DIR'), 'utf8');
if (!process.env.PYTHONHOME) {
process.env.PYTHONHOME = condaPath;
}

// create the global-scoped instance
let pyInst = global.__pipcook_boa_pyinst__;
if (pyInst == null) {
pyInst = new Python(process.argv.slice(1));
global.__pipcook_boa_pyinst__ = pyInst;
}

// FIXME(Yorkie): move to costa or daemon?
const globals = pyInst.globals();
const builtins = pyInst.builtins();

module.exports = {
condaPath,
pyInst,
globals,
builtins,
};
Loading

0 comments on commit 1b50e4e

Please sign in to comment.