I have been building a program that play Reversi using two algorithm: Minimax and Monte Carlo Tree Search (MCTS). This program pits those algorithms against each other in various Reversi matches, each with different configurations such as the Minimax depths, MCTS iterations, board size.
It's quite intriguing how a small program can grow slowly in complexity. First with about 600 lines of codes. Now, it's big enough I have to spilt the functions and classes to libraries in order to make it clearer to see. I also I also added multiprocessing support for this program, cause let me tell you, when one match could last up to 10 hours. So long, I later added a stop early flag to break the game, and that long game was with multiprocessing. The multiprocessing is to distribute the games out to many cores, so each CPU core plays one game. The reason for multiprocessing and not multithreading is because this program is written in Python, thus subjected to Global Interpreter Lock (GIL).
A bug my partner found was that Python could not find WEIGHTS in MinimaxPlayer, the class for the Minimax algorithm. Now, this wouldn't be funny if my code didn't work on my side, but it did still work. There was no error. He said it only happened when he used multiprocessing mode. I also knew that he used Windows and I used Linux. So I got to work. The function to import json in as weights for MinimaxPlayer was supposed to import weights in a static variable for MinimaxPlayer, so that those weights can be available to any MinimaxPlayer declared.
Normally, Python’s multiprocessing.Pool creates separate processes, which do not share memory or class-level state. In the main process, the child processes (used by starmap) won’t inherit that WEIGHTS value. But WEIGHTS is a static variable in MinimaxPlayer, that should mean it sticks through new processes, right? I found out that on Windows, the multiprocessing module uses spawn. This means:
- Each subprocess starts from scratch by re-importing the module.
- Nothing from global scope (like class variables, or initialized data) is automatically inherited.
- Class-level MinimaxPlayer.WEIGHTS doesn’t exist unless explicitly initialized in each subprocess.
By contrast, on Linux/macOS, multiprocessing uses fork, which copies memory from the parent process, including class-level state — that’s why it “just works” there.
Now understanding the issue, it is easy to fix. Make WEIGHTS global in the main file, then pass it in MinimaxPlayer when creating a new object.
This bug highlights the difference in OS architecture between Windows and Unix. In fork in Unix, the child process is a copy of the parent process (using copy-on-write), while spawn in Windows means starting a fresh, clean process. If my friend hadn't use Windows to test, we wouldn't have caught the bug, considering its elusiveness.