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.867 • labbench: Device1 connect 1.004 s elapsed
INFO 2024-03-20 13:43:05,875.875 • labbench: Device2 connect 2.005 s elapsed
INFO 2024-03-20 13:43:06,880.881 • labbench: Device1 fetch 1.003 s elapsed
INFO 2024-03-20 13:43:08,885.886 • labbench: Device2 acquire 2.003 s elapsed
INFO 2024-03-20 13:43:08,888.888 • labbench: 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.903 • labbench: Device1 connect 1.003 s elapsed
INFO 2024-03-20 13:43:10,904.905 • labbench: Device2 connect 2.004 s elapsed
INFO 2024-03-20 13:43:11,910.911 • labbench: Device1 fetch 1.003 s elapsed
INFO 2024-03-20 13:43:12,913.913 • labbench: Device2 acquire 2.005 s elapsed
INFO 2024-03-20 13:43:12,915.916 • labbench: 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
andfetch
/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.