Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

boa: support running Python code in non-blocking way #602

Merged
merged 19 commits into from
Oct 10, 2020
Merged
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