Multiprocessor

Multiprocessing Manager

Multiprocessor is a powerful package enabling developers to manually spawn EVM instances for parallel tasks, allows the programmer to fully leverage the computation power of modern multiple processor design. These jobs run in isolated EU instances and are subject to the same commutativity checks as EOA-submitted transactions. The system automatically detects conflicts and merges safe updates deterministically.

How It Works

The parent execution unit (EU) spawns multiple child EUs to execute transactions in parallel. Each child runs in isolation, and its state changes are passed to the Conflict Detector. Only clean, non-conflicting changes are merged back into the parent EU, ensuring deterministic and safe parallelism.

  • Storage: Child EUs receive a snapshot copy of the parent’s storage state at the moment of forking. They do not write directly to global storage during execution. They accumulate tentative state changes in a local cache. After execution, these changes are passed to the Conflict Detector, only clean, non-conflicting storage changes are committed back to the parent EU.

  • Memory: Each spawned EU gets its own memory space. Memory is not shared, not snapshotted, and has no impact on conflict detection. You can use memory freely within each parallel job without restriction. Since they have no impact on the global state.


This diagram is a visual explanation showing how a parent execution unit (EU) spawns child EUs and how conflict detection and state merging work.

Implementation

The Multiprocess offers a queue, which temporarily holds a list of tasks before execution begins. State consistency is rigorously guaranteed all the time. The actual parallel execution of the task in the queue won't start until the function run() is called. After execution, it checks for conflicts (e.g., write to same storage) and only merges non-conflicting (commutative) state changes back into the main state.

Function
Descriptions

Multiprocess

Manages job queue and controls execution across isolated EUs.

addJob(...)

Adds a job to the queue — each job includes a gas limit, value to transfer (optional, 0 by default), target contract, and calldata.

run()

Executes all jobs in parallel.

Conflict Detection

After run() is called, Arcology automatically performs conflict detection to ensure state consistency. Any jobs that violate commutativity (e.g., write to the same storage key) are reverted and excluded from the final state merge.

Examples

The examples highlight the main features and applications of Arcology's multiprocessing capability. They demonstrate how Arcology's technology can enhance the execution of smart contracts and provide significant benefits in terms of scalability and efficiency.

Parallel Assignment

This contract illustrates how two independent value assignments can be executed in parallel, safely merged back into the global state, and verified for correctness — all within a single transaction.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;

import "@arcologynetwork/concurrentlib/lib/multiprocess/Multiprocess.sol";

contract ParaAssigment {
    uint256 _1;
    uint256 _2;
    
    function call() public  { 
       Multiprocess mp = new Multiprocess(2); // Two parallel processors.
       
       // Adds two parallel jobs into the queue, each with a gas limit, value, 
       // address and calldata-very similar to address.call{}() syntax. 
       mp.addJob(50000, 0, address(this), abi.encodeWithSignature("assigner(uint256)", 0));
       mp.addJob(50000, 0, address(this), abi.encodeWithSignature("assigner(uint256)", 1));
       
       /* 
        Run two jobs with two new EU instances in parallel.
        Conflict detector will check the state they touch to preserve 
        commutativity after execution. Only clean state changs are merged back to the 
        parent EVM instance.
        */
       mp.run(); 

       assert(_1 == 10); // Check if the changes have been merged back.
       assert(_2 == 12);
    }

    function assigner(uint256 v)  public {
        if (v == 0) {
            _1 = v + 10; // For the first call
            return;
        }
        _2 = v + 11; // For the first call
    }
}   
 
1

The contract declares two state variables, _1 and _2, and sets up two parallel jobs using the Multiprocess API with two execution units. Job 0 runs assigner(0), setting _1 = 10, while Job 1 runs assigner(1), setting _2 = 11.

2

These jobs are executed in parallel inside isolated EU instances using mp.run(). After execution, Arcology’s conflict detector verifies that the two jobs modify separate, non-overlapping state, confirming no conflicts.

3

Since the updates are commutative and independent, their changes are safely merged back into the parent context. The final assertions verify that _1 is 10 and _2 is 11, confirming the success of deterministic parallel execution.


A Conflict Example

Similar to the example above, the contract ParaAssignmentWithConflict contains a function call() that utilizes the Multiprocess contract to execute the function assigner() in parallel. The assigner() function takes an input v and assigns the value v + 10 to the element at index v in the results array.

The major difference is when assigner() is no longer thread-safe, a write-write conflict happens at runtime. But the system reverts one transaction to preserve determinism. Only non-conflicting results are merged into the final state.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;

import "@arcologynetwork/concurrentlib/lib/multiprocess/Multiprocess.sol";

contract ParaAssignmentWithConflict {
    uint256[2] results;
    function call() public  { 
       Multiprocess mp = new Multiprocess(2); 
       mp.addJob(50000, 0, address(this), abi.encodeWithSignature("assigner(uint256)", 0));
       mp.addJob(50000, 0, address(this), abi.encodeWithSignature("assigner(uint256)", 0));
       mp.addJob(50000, 0, address(this), abi.encodeWithSignature("assigner(uint256)", 1));
       mp.run();
       
       assert(results[0] == 10);
       assert(results[1] == 11);
    }

    function assigner(uint256 v)  public {
        results[v] = v + 10;
    }
} 

Here's a step-by-step explanation of what happens:

1

The call() function is called, and it creates a new instance of the Multiprocess contract with 2 threads (processes).

2

Two messages are added into the Multiprocess container with the function signature "assigner(uint256)" and respective values 0 and 1.

3

The mp.run() function is called, which executes the two messages in parallel using the specified number of threads (2).

4

Arcology detects the overlapping write to the same storage key. One of the conflicting jobs is reverted. The third job (assigner(1)) writes to results[1] and is conflict-free, so it’s committed.

5

Only one write to results[0] is retained.results[0] == 10, results[1] == 11 so the assertions pass.

Last updated