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.


Example 4

The SubcurrencyCaller contract utilizes Arcology's Multiprocess library to perform parallel processing of multiple minting transactions on the Subcurrency contract, greatly improving the overall efficiency.

Note: The example highlights the main features and applications of Arcology's multiprocessing capability. It demonstrates how Arcology's technology can enhance the execution of smart contracts and provide significant benefits in terms of scalability and efficiency.

Code Analysis

  1. Parallel Minting Transactions: In the call function of the SubcurrencyCaller contract, four minting transactions are added to the Multiprocess queue. These transactions are designed to mint tokens for four different addresses: Alice, Bob, Carol, and Dave.

  2. Execution and Verification: After adding the minting transactions to the Multiprocess queue using mp.push(), the contract checks if the number of transactions in the queue is equal to 4 by using mp.length(). This ensures that all four minting transactions have been successfully added to the queue.

  3. Parallel Execution: The mp.run() function is then called, which executes all the minting transactions in parallel. Arcology's parallel processing capability allows these transactions to be processed simultaneously, reducing the overall execution time and gas cost.

  4. Verification of Minted Balances: After the parallel execution, the contract verifies the balances of the four addresses by calling the getter function of the Subcurrency contract. This demonstrates that the transactions were executed correctly, and the minted amounts were applied to the corresponding addresses.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.19 <0.9.0;

import "../../lib/multiprocess/Multiprocess.sol";
import "./Subcurrency.sol";

contract SubcurrencyCaller {
    function call(address coin) public{        
        address Alice = 0x1111111110123456789012345678901234567890;
        address Bob = 0x2222222220123456789012345678901234567890;
        address Carol = 0x4444444890123456789012345678901234567890;
        address Dave = 0x3333337890123456789012345678901234567890;
   
        Multiprocess mp = new Multiprocess(4); // Number of threads
        mp.push(100000, address(coin), abi.encodeWithSignature("mint(address,uint256)", Alice, 1111));
        mp.push(100000, address(coin), abi.encodeWithSignature("mint(address,uint256)", Bob, 2222));
        mp.push(100000, address(coin), abi.encodeWithSignature("mint(address,uint256)", Carol, 3333));
        mp.push(100000, address(coin), abi.encodeWithSignature("mint(address,uint256)", Dave, 4444));
        require(mp.length() == 4);
        mp.run(); // Start parallel processing

        require(Coin(coin).getter(Alice) == 1111);
        require(Coin(coin).getter(Bob) == 2222);
        require(Coin(coin).getter(Carol) == 3333);
        require(Coin(coin).getter(Dave) == 4444);
    }
}

Conclusion

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