diff --git a/docs/features/threading.md b/docs/features/threading.md new file mode 100644 index 00000000..1eb55510 --- /dev/null +++ b/docs/features/threading.md @@ -0,0 +1,140 @@ +--- +icon: dot +title: Threading +--- + +pocketpy organizes its state by `VM` structure. +Users can have at maximum 16 `VM` instances (index from 0 to 15). +Each `VM` instance can only be accessed by exactly one thread at a time. +If you are trying to run two python scripts in parallel refering the same `VM` instance, +you will crash it definitely. + +However, there are two ways to achieve multi-threading in pocketpy. + +One way is to use a native threading library such as `pthread`. +You can wrap the multi-threading logic into a C function and bind it to pocketpy. +Be careful and not to access the same `VM` instance from multiple threads at the same time. +You need to lock critical resources or perform a deep copy of all needed data. + +## ComputeThread + +The other way is to use `pkpy.ComputeThread`. +It is like an isolate in Dart language. +`ComputeThread` is a true multi-threading model to allow you run python scripts in parallel without lock, +backed by a separate `VM` instance. + +`ComputeThread` is highly designed for computational intensive tasks in games. +For example, you can run game logic in main thread (VM 0) and run world generation in another thread (e.g. VM 1). + +```mermaid +graph TD + subgraph Main Thread + A[Game Start] + B[Submit WorldGen Job] + C[Frame 1] + D[Frame 2] + E[Frame 3] + F[...] + G[Get WorldGen Result] + H[Render World] + end + subgraph WorldGen Thread + O[Generate Biomes] + P[Generate Terrain] + Q[Generate Creatures] + R[Dump Result] + end + A --> B + B --> C + C --> D + D --> E + E --> F + F --> G + G --> H + + O --> P + P --> Q + Q --> R + + B --> O + R --> G +``` + +#### `main.py` +```python +import time +from pkpy import ComputeThread + +thread = ComputeThread(1) +print("Game Start") + +# import worldgen.py +thread.exec_blocked('from worldgen import gen_world') + +print("Submit WorldGen Job") +thread.call('gen_world', 3, (100, 100), 10) + +# wait for worldgen to finish +for i in range(1, 100000): + print('Frame:', i) + time.sleep(1) + if thread.is_done: + break + +error = thread.last_error() +if error is not None: + print("Error:", error) +else: + retval = thread.last_retval() + biomes = retval['biomes'] + terrain = retval['terrain'] + creatures = retval['creatures'] + print("World Generation Complete", len(biomes), len(terrain), len(creatures)) +``` + +#### `worldgen.py` +```python +import time +import random + +def gen_world(biome_count: int, terrain_size: tuple[int, int], creature_count: int) -> dict: + # simulate a long computation + time.sleep(3) + + # generate world data + all_biomes = ["forest", "desert", "ocean", "mountain", "swamp"] + all_creatures = ["wolf", "bear", "fish", "bird", "lizard"] + + width, height = terrain_size + + terrain_data = [ + random.randint(1, 10) + for _ in range(width * height) + ] + + creatures = [ + { + "name": random.choice(all_creatures), + "x": random.randint(0, width - 1), + "y": random.randint(0, height - 1), + } + for i in range(creature_count) + ] + + return { + "biomes": all_biomes[:biome_count], + "terrain": terrain_data, + "creatures": creatures, + } +``` + +Run `main.py` and you will see the result like this: +``` +Game Start +Submit WorldGen Job +Frame: 1 +Frame: 2 +Frame: 3 +Frame: 4 +World Generation Complete 3 10000 10 +``` \ No newline at end of file