测试javascript互斥实现



我已经为javascript/typescript编写了一个互斥实现,但我很难解决如何测试它的问题

class Mutex {
private current = Promise.resolve();
async acquire() {
let release: () => void;
const next = new Promise<void>(resolve => {
release = () => { resolve(); };
});
const waiter = this.current.then(() => release);
this.current = next;
return await waiter;
}
}

用法:

const mut = new Mutex();
async function usesMutex() {
const unlock = await mut.acquire();
try {
await doSomeStuff();
await doOtherStuff();
} finally {
unlock();
}
}

我不确定是否有任何简单的方法来创建这种定时问题,如果互斥对象不能按预期工作,就会导致测试失败。如有任何建议,我们将不胜感激。

您需要一个具有确定性的测试,如果互斥体中断,其行为将发生变化。

下面的例子是一个原子计数器问题。产生了两个工人,每个工人在一个循环中做三件事:

  1. 从全局计数器获取值并将其存储在本地变量中
  2. 增加局部变量中的值
  3. 将局部变量写回全局计数器

至关重要的是,我在这里使用awaitsetTimeout来在执行中创建中断。没有任何awaits的async函数将完全是原子函数,因此我们需要创建一些中断,以允许调度器在任务之间切换。如果互斥被破坏,这些await将允许调度程序从另一个工作线程运行代码,因为每个await都是Javascript调度程序更改作业的机会。

如果互斥锁正在工作,您应该看到以下内容。在每一步之间,工作程序都会进入睡眠状态并重新醒来,但互斥锁不允许其他工作程序做任何事情:

  1. Worker 1获取锁
  2. Worker 1从全局计数器读取值0
  3. Worker 1将值从0递增到1
  4. Worker 1将值1写回全局计数器
  5. 工人1释放锁
  6. Worker 2获取锁
  7. Worker 2从全局计数器读取值1
  8. Worker 2将值从1递增到2
  9. Worker 2将值2写回全局计数器
  10. 工人2释放锁

结果:2,预期:2

如果互斥锁不起作用(或者没有使用(,那么两个工作线程将相互绊倒,最终结果将是不正确的。和以前一样,工人们每次执行一个动作就要睡觉:

  1. Worker 1从全局计数器读取值0
  2. Worker 2从全局计数器读取值0
  3. Worker 1将值从0递增到1
  4. Worker 2将值从0递增到1
  5. Worker 1将值1写回全局计数器
  6. Worker 2将值1写回全局计数器

结果:1,预期:2

在这两种情况下,两名工人都在执行相同的操作,但如果命令没有得到执行,则结果是不正确的。

这个例子是人为的,但可重复,而且大多是确定性的。当互斥对象工作时,您总是会得到相同的最终结果。如果不是这样,你总是会得到一个错误的结果。

工作演示:

var state = {
isMutexBroken: false,
counter: 0,
worker1LastAction: '',
worker2LastAction: '',
worker1IsActive: false,
worker2IsActive: false,
}
class Mutex {
constructor() {
this.current = Promise.resolve();
}
async acquire() {
if (state.isMutexBroken) {
return () => {};
}
let release;
const next = new Promise(resolve => {
release = () => {
resolve();
};
});
const waiter = this.current.then(() => release);
this.current = next;
return await waiter;
}
}
var mutex = new Mutex();
const renderState = () => {
document.getElementById('mutex-status').textContent = state.isMutexBroken ? 'Mutex is *not* working correctly. Press "fix mutex" to fix it.' : 'Mutex is working correctly. Press "break mutex" to break it.';
document.getElementById('counter').textContent = `Counter value: ${state.counter}`;
document.getElementById('worker1').textContent = `Worker 1 - last action: ${state.worker1LastAction}`;
document.getElementById('worker2').textContent = `Worker 2 - last action: ${state.worker2LastAction}`;
document.getElementById('start-test').disabled = state.worker1IsActive || state.worker2IsActive;
document.getElementById('break-mutex').disabled = state.worker1IsActive || state.worker2IsActive;
document.getElementById('fix-mutex').disabled = state.worker1IsActive || state.worker2IsActive;
}
// https://stackoverflow.com/a/39914235/8166701
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const worker = async(delay, count, id) => {
state[`${id}IsActive`] = true;
let workerCopyOfCounter;
for (let i = 0; i < count; i++) {
const unlock = await mutex.acquire();
state[`${id}LastAction`] = `Aquired lock.`;
renderState();
await sleep(delay);
workerCopyOfCounter = state.counter;
state[`${id}LastAction`] = `Acquired global counter: ${workerCopyOfCounter}`;
renderState();
await sleep(delay);
workerCopyOfCounter++;
state[`${id}LastAction`] = `Incremented counter: ${workerCopyOfCounter}`;
renderState();
await sleep(delay);
state.counter = workerCopyOfCounter;
state[`${id}LastAction`] = `Wrote ${workerCopyOfCounter} back to global counter.`;
renderState();
await sleep(delay);
unlock();
state[`${id}LastAction`] = `Released lock.`;
renderState();
await sleep(delay);
}
state[`${id}LastAction`] = `Finished.`;
state[`${id}IsActive`] = false;
renderState();
}
document.getElementById('break-mutex').onclick = () => {
state.isMutexBroken = true;
renderState();
}
document.getElementById('fix-mutex').onclick = () => {
state.isMutexBroken = false;
renderState();
}
document.getElementById('start-test').onclick = () => {
document.getElementById('test-result').textContent = '';
document.getElementById('start-test').textContent = 'Reset and start test';
state.counter = 0;
state.worker1LastAction = '';
state.worker2LastAction = '';
renderState();

const slow = document.getElementById('slow').checked;
const multiplier = slow ? 10 : 1;
Promise.all([
worker(20 * multiplier, 10, 'worker1'),
worker(55 * multiplier, 5, 'worker2')
]).then(() => {
const elem = document.getElementById('test-result');
elem.classList.remove('pass');
elem.classList.remove('fail');
elem.classList.add(state.counter === 15 ? 'pass' : 'fail');
elem.textContent = state.counter === 15 ? 'Test passed' : 'Test failed';
});
}
renderState();
.flex-column {
display: flex;
flex-direction: column;
}
.flex-row {
display: flex;
}
.top-padding {
padding-top: 8px;
}
.worker-state-container {
background-color: #0001;
margin-top: 8px;
padding: 5px;
}
.pass {
background-color: limegreen;
color: white;
}
.fail {
background-color: red;
color: white;
}
<div class="flex-column">
<div className="flex-row">
<button id="break-mutex">Break mutex</button>
<button id="fix-mutex">Fix mutex</button>
<div id="mutex-status"></div>
</div>
<div className="flex-row">
<input type="checkbox" id="slow" name="slow"><label for="slow">slow</label>
</div>
<div class="flex-row top-padding">
<button id="start-test">Start test</button>
</div>
<div id="counter"></div>
<div>Expected end value: 15</div>
<div id="test-result"></div>
<div class="top-padding">
<div id="worker1" class="worker-state-container">
</div>
<div id="worker2" class="worker-state-container">
</div>
</div>
</div>

最低版本:

var state = { counter: 0 }
// https://stackoverflow.com/a/39914235/8166701
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const worker = async (delay, count) => {
let workerCopyOfCounter;
for (let i = 0; i < count; i++) {
// Lock the mutex
const unlock = await mutex.acquire();
// Acquire the counter
workerCopyOfCounter = state.counter;
await sleep(delay);
// Increment the local copy
workerCopyOfCounter++;
await sleep(delay);
// Write the local copy back to the global counter
state.counter = workerCopyOfCounter;
await sleep(delay);
// Unlock the mutex
unlock();
await sleep(delay);
}
}
// Create two workers with different delays. If the code is working,
// state.counter will equal 15 when both workers are finished.
Promise.all([
worker(20, 10),
worker(55, 5),
]).then(() => {
console.log('Expected: 15');
console.log('Actual:', state.counter);
});

我会创建一个实现,它可能会将对象按顺序放在共享数组上。您可以模拟使用互斥锁按顺序访问数组。您的测试只需断言正确的插入顺序即可。我会利用setTimeout来安排锁定互斥对象的尝试,当成功获取时,然后向共享数组中添加一个元素。

最新更新