Simplified Concurrency

labbench includes simplified concurrency support for this kind of I/O-constrained operations like waiting for instruments to perform long operations. It is not suited for parallelizing CPU-intensive tasks because the operations share a single process on one CPU core, instead of multiprocessing, which may be able to spread operations across multiple CPU cores.

Here are simple Device objects that use time.sleep() as a stand-in for long-running remote operations:

import labbench as lb

# a placeholder for long-running remote operations
from time import sleep

class Device1(lb.VISADevice):
    def open(self):
        # open() is called on connection to the device
        with lb.stopwatch('Device1 connect'):
            sleep(1)
            
    def fetch(self):
        with lb.stopwatch('Device1 fetch'):
            sleep(1)
            return 5

class Device2(lb.VISADevice):
    def open(self):
        # open() is called on connection to the device
        with lb.stopwatch('Device2 connect'):
            sleep(2)
            
    def acquire(self):
        with lb.stopwatch('Device2 acquire'):
            sleep(2)
            return None

Suppose we need to both fetch from Device1 and acquire in Device2, and that the time-sequencing is not important. One approach is to simply call one and then the other:

from labbench import testing
from time import perf_counter

# allow simulated connections to the specified VISA devices
lb.visa_default_resource_manager(testing.pyvisa_sim_resource)

d1 = Device1('TCPIP::localhost::INSTR')
d2 = Device2('USB::0x1111::0x2222::0x1234::INSTR')

t0 = perf_counter()
with d1, d2:
    print(f'connect both (total time): {perf_counter()-t0:0.1f} s')
    with lb.stopwatch('both Device1.fetch and Device2.acquire (total time)'):
        d1.fetch()
        d2.acquire()
connect both (total time): 3.0 s
 INFO   2024-03-20 13:43:03,867.867labbench: Device1 connect 1.004 s elapsed
 INFO   2024-03-20 13:43:05,875.875labbench: Device2 connect 2.005 s elapsed
 INFO   2024-03-20 13:43:06,880.881labbench: Device1 fetch 1.003 s elapsed
 INFO   2024-03-20 13:43:08,885.886labbench: Device2 acquire 2.003 s elapsed
 INFO   2024-03-20 13:43:08,888.888labbench: both Device1.fetch and Device2.acquire (total time) 3.011 s elapsed

For each of the connection and fetch/acquire operations, the total duration was about 3 seconds, because the 1 and 2 second operations are executed sequentially.

Suppose that we want to perform each of the open and fetch/acquire operations concurrently. Enter labbench.concurrently():

from labbench import testing
from time import perf_counter

# allow simulated connections to the specified VISA devices
lb.visa_default_resource_manager(testing.pyvisa_sim_resource)

d1 = Device1('TCPIP::localhost::INSTR')
d2 = Device2('USB::0x1111::0x2222::0x1234::INSTR')

t0 = perf_counter()
with lb.concurrently(d1, d2):
    print(f'connect both (total time): {perf_counter()-t0:0.1f} s')
    with lb.stopwatch('both Device1.fetch and Device2.acquire (total time)'):
        ret = lb.concurrently(d1.fetch, d2.acquire)
        
print('Return value: ', ret)
connect both (total time): 2.0 s
Return value:  {'fetch': 5}
 INFO   2024-03-20 13:43:09,902.903labbench: Device1 connect 1.003 s elapsed
 INFO   2024-03-20 13:43:10,904.905labbench: Device2 connect 2.004 s elapsed
 INFO   2024-03-20 13:43:11,910.911labbench: Device1 fetch 1.003 s elapsed
 INFO   2024-03-20 13:43:12,913.913labbench: Device2 acquire 2.005 s elapsed
 INFO   2024-03-20 13:43:12,915.916labbench: both Device1.fetch and Device2.acquire (total time) 2.008 s elapsed

Each call to labbench.concurrently() executes each callable in separate threads, and returns after the longest-running call.

  • As a result, in this example, for each of the open and fetch/acquire, the total time is reduced from 3 s to 2 s.

  • The return values of threaded calls are packaged into a dictionary for each call that does not return None. The syntax is a little more involved when you want to pass in arguments to multiple callables. For information on doing this, see the detailed instructions.