实现模拟线程#sleep()

时间:2018-08-13 16:39:03

标签: java multithreading simulation

背景

我正在设计软件,以便可以轻松执行单元测试。我有一个IClock接口,除其他方法外,该接口还具有 IClock#wait(TimeUnit timeUnit, long duration)。此方法将在 timeUnit持续时间(即1秒)内暂停当前线程。

IClock接口有两种实现:

  • SimulatedClock:具有手动增加时钟中存储时间的方法
  • RealClock:通过引用System.currentTimeMillis()
  • 自动增加时间

这是IClock#wait(...)的默认方法:

/**
     * Locks current thread for specified time
     *
     * @param timeUnit
     * @param dt
     */
    default void wait(TimeUnit timeUnit, long dt)
    {
        Lock lock = new ReentrantLock();
        scheduleIn(timeUnit, dt, lock::unlock);
        lock.lock();
    }

问题

我希望模拟单元测试的当前工作方式是

  1. 启动线程
  2. 等待,直到所有线程完成或处于阻塞状态(我假设它们是否被阻塞,它们已调用IClock#wait(...)
  3. 如果所有线程均已完成,请完成。否则,将SimulatedClock时间增加一毫秒。

但是,实际情况是:

  1. 启动线程
  2. 即使线程不是第一次调用IClock#wait(),也要开始增加时间。

因此,我需要做的是确定所有线程何时完成或被阻塞。尽管可以使用Thread#getState()完成此操作,但我宁愿采用更优雅的解决方案并与ForkJoinPool一起使用。

完整代码

GitHub

SimulatedClock

package com.team2502.ezauton.utils;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class SimulatedClock implements IClock
{

    private long time = 0;

    private List<Job> jobs = new ArrayList<>();

    public SimulatedClock() {}

    public void init()
    {
        init(System.currentTimeMillis());
    }

    public void init(long time)
    {
        setTime(time);
    }

    /**
     * Add time in milliseconds
     *
     * @param dt millisecond increase
     * @return The new time
     */
    public long addTime(long dt)
    {
        setTime(getTime() + dt);
        return getTime();
    }

    /**
     * Adds time with units
     *
     * @param timeUnit
     * @param value
     */
    public void addTime(TimeUnit timeUnit, long value)
    {
        addTime(timeUnit.toMillis(value));
    }

    /**
     * Add one millisecond and returns new value
     *
     * @return The new time
     */
    public long incAndGet()
    {
        return addTime(1);
    }

    /**
     * Increment a certain amount of times
     *
     * @param times
     */
    public void incTimes(long times, long dt)
    {
        long init = getTime();
        long totalDt = times * dt;
        for(int i = 0; i < times; i++)
        {
            if(!jobs.isEmpty())
            {
                addTime(dt);
            }
            else
            {
                break;
            }
        }
        setTime(init + totalDt);
    }

    /**
     * Increment a certain amount of times
     *
     * @param times
     * @return
     */
    public void incTimes(long times)
    {
        incTimes(times, 1);
    }

    @Override
    public long getTime()
    {
        return time;
    }

    public void setTime(long time)
    {
        jobs.removeIf(job -> {
            if(job.getMillis() < time)
            {
                job.getRunnable().run();
                return true;
            }
            return false;
        });

        this.time = time;
    }

    @Override
    public void scheduleAt(long millis, Runnable runnable)
    {
        if(millis < getTime())
        {
            throw new IllegalArgumentException("You are scheduling a task for before the current time!");
        }
        jobs.add(new Job(millis, runnable));
    }

    private static class Job
    {
        private final long millis;
        private final Runnable runnable;

        public Job(long millis, Runnable runnable)
        {
            this.millis = millis;
            this.runnable = runnable;
        }

        public long getMillis()
        {
            return millis;
        }

        public Runnable getRunnable()
        {
            return runnable;
        }
    }
}

模拟

package com.team2502.ezauton.command;

import com.team2502.ezauton.utils.SimulatedClock;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;

public class Simulation
{

    private final SimulatedClock simulatedClock;
    private List<IAction> actions = new ArrayList<>();

    public Simulation()
    {
        simulatedClock = new SimulatedClock();
    }

    public SimulatedClock getSimulatedClock()
    {
        return simulatedClock;
    }

    public Simulation add(IAction action)
    {
        actions.add(action);
        return this;
    }

    /**
     * @param timeoutMillis Max millis
     */
    public void run(long timeoutMillis)
    {
        simulatedClock.init();

        actions.forEach(action -> new ThreadBuilder(action, simulatedClock).buildAndRun());

        simulatedClock.incTimes(timeoutMillis);

        // Need to wait until all threads are finished
        if(!ForkJoinPool.commonPool().awaitQuiescence(1, TimeUnit.SECONDS))
        {
            throw new RuntimeException("Simulator did not finish in a second.");
        }

    }

    public void run(TimeUnit timeUnit, long value)
    {
        run(timeUnit.toMillis(value));
    }
}

单元测试示例

@Test
public void testSimpleAction()
{
    AtomicBoolean atomicBoolean = new AtomicBoolean(false);
    Simulation simulation = new Simulation();
    simulation.add(new DealyedAction((TimeUnit.SECONDS, 5) -> atomicBoolean.set(true)));
    simulation.run(TimeUnit.SECONDS, 100);
    Assert.assertTrue(atomicBoolean.get());
}

4 个答案:

答案 0 :(得分:0)

在调用simulatedClock.incTimes()之前,各个线程似乎都没有及时处理。

通常在多线程测试中,一开始会有某种“约会”-允许所有线程在安全启动并运行后才能检入。如果您知道要准备多少个线程,可以使用CountDownLatch来简化。

例如在Simulation.run()中:

    simulatedClock.init(new CountDownLatch(actions.size()));

稍后将保留对CountDownLatch的引用。

每个线程到达SimulatedClock.scheduleAt()时,它可以将锁存器递减一:

@Override
public void scheduleAt(long millis, Runnable runnable)
{
    if(millis < getTime())
    {
        throw new IllegalArgumentException("You are scheduling a task for before the current time!");
    }
    jobs.add(new Job(millis, runnable));

    countDownLatch.countDown();
}

然后incTimes()可以等待所有线程出现:

public void incTimes(long times, long dt)
{
    countDownLatch.await();

    long init = getTime();
    ...

答案 1 :(得分:0)

我知道这不能完全回答您的问题,但是您不应该手动创建Thread并使用现有的并发框架。

您可以这样做:

public static void main(String[] args) {
    AtomicBoolean bool = new AtomicBoolean(false);
    ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
    ScheduledFuture<?> future = executorService.schedule(() -> bool.set(true), 5, TimeUnit.SECONDS);
    try {
        boolean b = future.get(100, TimeUnit.SECONDS);
    } catch (Exception e) {
        fail();
    }
    assertTrue(b);
}

如果您的scheduleAt返回Future,您也可以将其集成到框架中。

interface Clock {
    Future<?> scheduleAt(long millis, Runnable r);
}

class SchedulerService implements Clock {
    ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();

    public Future<?> scheduleAt(long millis, Runnable r) {
        Instant scheduleTime = Instant.ofEpochMilli(millis);
        Instant now = Instant.now();
        if (scheduleTime.isBefore(now)) {
            throw new IllegalArgumentException("You are scheduling a task for before the current time!");
        }
        long delay = scheduleTime.minus(now).toEpochMilli();
        return executorService.schedule(r, delay, TimeUnit.MILLISECONDS);
    }
}

答案 2 :(得分:0)

首先,应将 IClock.wait 重命名为 IClock.sleep ,以避免混淆。

对于 RealClock ,此方法可以委托给 Thread.sleep()

对于 SimulatedClock ,此方法可以按照以下几行实现:

void sleep(TimeUnit timeUnit, long dt) {
    final Object mon = new Object();
    scheduleIn(timeUnit, dt, 
        () -> {
            synchronized(mon) {
               mon.notify();
            }
        });
    synchronized(mon) {
        try {
            mon.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }   
}

您可以修改它以引发 InterruptedException

答案 3 :(得分:0)

如果您在Linux上运行,则有一个偷偷摸摸的方法。有一个实用程序(可让我不记得它的名字,而Google搜索目前无法为我找到它)可以用来为您运行程序。每当您的程序调用导致睡眠的内容时,它就会拦截该睡眠并将其替换为零长度睡眠。我认为它也会在一天中的某个时间开始撒谎,以弥补损失。

由于该方法适用于使用glibc编译的任何内容(包括JRE),因此它也适用于任何Java应用程序。

再次抱歉,无法提供有用的链接-我会继续搜寻书签/搜索,因为这听起来很有用,而我由于不记得它的名字而感到烦恼。

编辑

这样做的目的是您不必在自己的程序中有两个单独的实现。用于睡眠的标准库调用在程序的较低层被拦截。

这不是我所想到的,但它遵循相同的原则:timeskew。这似乎可以修补nanosleep()clock_gettime()select()以及一些相关功能,因此很有可能掩盖了基础知识。

相关问题