Skip to content

Three pieces of thread state that belonged to somebody else

Choose a tag to compare

@pisarev pisarev released this 07 Aug 12:53
· 10 commits to main since this release

Three things the library kept in thread storage turned out to belong to a
narrower scope. Each is reproducible in a single thread, and each has a test that
fails on the old code.

The loop guard is armed in a pair

ArmLoopGuard and DisarmLoopGuard replace writing to ParseLoopLeft and
ParseBreak by hand. Disarming restores whatever was there before, so guards
nest: a formula that calls Parse can set a budget of its own without clearing
the outer one.

Writing by hand leaked. A budget that runs out is recorded as a negative number,
and that number outlived the run that spent it. Whatever came next in the same
thread inherited the refusal even though it had never armed a guard: a different
parser, a later button press, a plain ten turn loop that should have finished.
tests/LoopScopeTest.dpr covers this in 16 checks.

Two things stay as they were, and the tests now state them on purpose. A nested
evaluation spends the same budget, because a budget for each evaluation is no
budget at all: a nested loop hangs a page as readily as an outer one. A nested
evaluation watches the same cancellation flag, because an owner asking you to
stop means all of it.

The exception mask belongs to the evaluation

The floating point exception mask was installed by the parser constructor, so it
belonged to whichever thread created the object. That went wrong in both
directions:

  • Evaluate on a shared parser from a worker thread and division by zero raised
    EZeroDivide instead of answering infinity. That is the arrangement the
    plotting component uses: one parser, four workers.
  • While a parser was alive the mask stood for the whole program, so neighbouring
    code in the same thread quietly stopped getting its own exceptions.

An evaluation now installs the mask and restores the caller's when it
returns. The accelerator does the same around machine code, which runs outside
the interpreter. The mask an evaluation installs is the ExceptionMask
property. Narrow it if you want the exceptions inside formulas too.
tests/FpuMaskTest.dpr, 13 checks.

Two cases make this release worth taking. You share one parser between threads,
or your program narrows the mask for its own arithmetic.

The lock around Deriv and Parse is per parser

It used to be one lock for every parser in the process. Four threads with four
unrelated parsers queued behind each other on any formula holding a derivative.
Parse now holds the lock only while it compiles. Keeping it across the
evaluation meant holding it across arbitrary user code, which is how deadlocks
are made. tests/MethodLockTest.dpr, 8 checks, both of them deterministic.

Compatibility

Delphi 13, Free Pascal 3.2.2 and 3.3.1. The Linux matrix builds and runs the
parser on both, in two locales.