Multiprocess

Multiprocessing Manager

Multiprocessing is a powerful package enabling developers to manually spawn EVM instances for parallel processing, allows the programmer to fully leverage the computation power of modern multiple processor design. It effectively overcomes the limitations of the single-threaded design in EVM.

Contract

The Multiprocess offers a queue, which temporarily holds a list of tasks before execution begins. State consistency is rigorously guaranteed all the time.

Constructor

constructor (uint256 threads)

constructor(uint256 processes)

Constructor to initialize the Multiprocess container.

  • Parameters:

    • processes: The number of parallel proceses(ranging from 1 to 255) for parallel processing.

Public Functions

push

function push(uint256 gaslimit, address contractAddr, bytes memory funcCall) public virtual

Push an executable message into the container with specified gas limit, contract address, and function call data.

  • Parameters:

    • gaslimit: The gas limit for the execution of the function call.

    • contractAddr: The address of the smart contract to execute the function on.

    • funcCall: The encoded function call data.

push

function push(uint256 gaslimit, uint256 ethVal, address contractAddr, bytes memory funcCall) public virtual

Push an executable message into the container with specified gas limit, eth value, contract address, and function call data.

  • Parameters:

    • gaslimit: The gas limit for the execution of the function call.

    • ethVal: The number of wei sent with the message.

    • contractAddr: The address of the smart contract to execute the function on.

    • funcCall: The encoded function call data.

pop

function pop() public virtual returns (bytes memory)

Pop an executable message from the container.

  • Returns:

    • bytes: The popped executable message.

get

function get(uint256 idx) public virtual returns (bytes memory)

Get an executable message from the container at the specified index.

  • Parameters:

    • idx : The index of the executable message to retrieve.

  • Returns:

    • bytes: The executable message at the specified index.

set

function set(uint256 idx, bytes memory elem) public

Set an executable message at the specified index in the container.

  • Parameters:

    • idx: The index where the executable message should be stored.

    • elem: The executable message data to be stored at the specified index.

run

function run() public

This function processes the executable messages concurrently with the number of threads specified in the constructor.

Note: Executions causing potential state inconsistency will be reverted automatically. This is guaranteed at the system level.

Example 1

The contract ParaNativeAssignmentTest 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.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 pushed 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).

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

import "./U256Cum.sol";
import "../multiprocess/Multiprocess.sol";

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

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

Analysis

The final result of results[0] will be 10, and the final result of results[1] will be 11. This is because the assigner() function is executed twice, once with v as 0, which assigns 0 + 10 = 10 to results[0], and once with v as 1, which assigns 1 + 10 = 11 to results[1]


Example 2

The NativeStorageAssignmentTest contract demonstrates how to use the Multiprocess contract to execute multiple tasks concurrently on a shared instance of the NativeStorage contract, allowing for efficient parallel processing. The end result of calling the call() function should be that results.getX() returns 2 and results.getY() returns 102.

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

import "./U256Cum.sol";
import "../multiprocess/Multiprocess.sol";

contract NativeStorage {   
    uint256 public x = 1 ;
    uint256 public y = 100 ;

    function incrementX() public {x ++;}
    function incrementY() public {y += 2;}

    function getX() public view returns(uint256) {return x;}
    function getY() public view returns(uint256) {return y;}
}

contract NativeStorageAssignmentTest {
    NativeStorage results = new NativeStorage() ;
    function call() public  { 
        Multiprocess mp = new Multiprocess(2); 
        mp.push(50000, address(results), abi.encodeWithSignature("incrementX()"));
        mp.push(50000, address(results), abi.encodeWithSignature("incrementY()"));
        mp.run();
        require(results.getX() == 2);
        require(results.getY() == 102);
    }
}

Example 3

This example illustrates how the system manages scenarios in which multiple threads attempt to update a shared variable. Without proper handling, this could lead to data consistency problems.

The third example is quite similar to the previous example. They both use the NativeStorage contract and the Arcology's concurrency framework to perform parallel execution of tasks. The only difference is that this example calls the incrementX() twice in parallel.

The incrementX() function inside the NativeStorage contract increases the value of the x variable by 1.

Since the two instances of NativeStorageAssignmentTest are running in parallel, they both attempt to modify the x and Y variable simultaneously. Arcology's concurrency framework detects the conflict. Only one of the modifications will succeed, and the other one will fail.

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

contract NativeStorage {   
    uint256 public x = 1 ;
    uint256 public y = 100 ;

    function incrementX() public {x ++;}
    function incrementY() public {y += 2;}

    function getX() public view returns(uint256) {return x;}
    function getY() public view returns(uint256) {return y;}
}

contract NativeStorageAssignmentTest {
    NativeStorage results = new NativeStorage() ;
    function call() public  { 
        Multiprocess mp = new Multiprocess(2); 
        mp.push(50000, address(results), abi.encodeWithSignature("incrementX()"));
        mp.push(50000, address(results), abi.encodeWithSignature("incrementX()"));
        mp.push(50000, address(results), abi.encodeWithSignature("incrementY()"));  
        mp.run();
        require(results.getX() == 2);
        require(results.getY() == 102);
    }
}

Analysis

After both calls to incrementX() are executed in parallel, the value of x will increase by 1, not 2. This is because one of the incrementX() calls and one of the calls succeeded, while the other one was automatically reverted by the system due to conflict.

The concurrency control system consistently monitors state accesses that could potentially cause conflicts and automatically reverts the associated transactions.

Arcology ensures data consistency and integrity when concurrent operations are performed on shared state variables, allowing developers to harness the power of parallel execution without worrying about data corruption or conflicts.

Last updated