Skip to content

BUG: Remove unnecessary global fflush(nullptr) causing MetaIO read deadlocks - #6833

Closed
blowekamp wants to merge 1 commit into
InsightSoftwareConsortium:mainfrom
blowekamp:fix-metaio-fflush-concurrency-deadlock
Closed

BUG: Remove unnecessary global fflush(nullptr) causing MetaIO read deadlocks#6833
blowekamp wants to merge 1 commit into
InsightSoftwareConsortium:mainfrom
blowekamp:fix-metaio-fflush-concurrency-deadlock

Conversation

@blowekamp

Copy link
Copy Markdown
Member

Summary

MetaObject::ReadStream() and MetaForm::ReadStream() unconditionally call fflush(nullptr) before parsing header fields. This line dates back to MetaIOs very first file-IO commit, long before multithreading was a design concern for this library.

fflush(NULL) flushes every open FILE* stream in the entire process, not just the one being read. On platforms such as macOS, this requires walking the process-wide list of all open FILE* streams and locking each one in turn (_fwalk -> sflush_locked -> flockfile).

Under heavy concurrent MetaImage/.mha reads (many threads, each opening/reading/closing its own file), this global flush-all creates severe lock contention: every threads fflush(nullptr) call competes to lock every other threads in-flight FILE* handle. In practice this can grind concurrent reads to a halt indefinitely (observed via a native C++ gtest reproducer with 70+ threads reading distinct .mha files concurrently; lldb backtraces showed the large majority of threads parked in flockfile/sflush_locked/_fwalk called from MetaObject::ReadStream).

Reading a file does not require flushing any previously written data (nothing was just written), so this call serves no purpose and can simply be removed.

Test plan

  • Added a native C++ gtest reproducer (ConcurrentImageRead.*, in the companion SimpleITK repo) that spins up ~70 threads each repeatedly reading distinct .mha/.mhd files via itk::simple::ReadImage.
  • Confirmed the test reliably hangs/times out on main before this change.
  • Confirmed the test passes reliably after removing the two fflush(nullptr) calls, including repeated runs with 2x the stress (more iterations per thread).
  • Ruled out several other hypotheses along the way (MetaIOs std::ifstream usage, itk::ImageFileReader::TestFileExistanceAndReadability()s own ifstream probe) via direct experimentation before finding this root cause.

…adlocks

MetaObject::ReadStream() and MetaForm::ReadStream() unconditionally call
fflush(nullptr) before parsing, a leftover from the original MetaIO commit
that predates any multithreading concerns for this library. fflush(NULL)
flushes every open FILE* stream in the entire process, not just the one
being read.

On platforms such as macOS, this requires walking the process-wide list of
all open FILE* streams and locking each one in turn. Under heavy concurrent
MetaImage/MHA reads (many threads each opening/reading/closing their own
files), this global flush-all creates severe lock contention that can grind
concurrent reads to a halt indefinitely, since each thread's fflush(nullptr)
call competes to lock every other thread's in-flight FILE* handle.

Reading does not require flushing any previously written data, so this call
serves no purpose and can simply be removed.
@github-actions github-actions Bot added type:Bug Inconsistencies or issues which will cause an incorrect result under some or all circumstances area:ThirdParty Issues affecting the ThirdParty module labels Sep 3, 2026
@greptile-apps

greptile-apps Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This change removes process-wide fflush(nullptr) calls before MetaIO header parsing, preventing unrelated process streams from being flushed during concurrent reads.

The affected MetaObject and MetaForm read-after-write paths were compiled and exercised against both the parent implementation and this change. Both versions wrote headers, reopened the files, parsed them through the affected ReadStream methods, and preserved all checked values. The investigated regression hypothesis was disproved by these successful before-and-after checks.

No defects were found; the change is safe to merge.

Confidence Score: 5/5

The targeted read paths retain successful header parsing and read-after-write behavior after the global flush is removed.

No final findings remain after directly comparing MetaObject and MetaForm write, reopen, and ReadStream parsing behavior before and after the change.

Files Needing Attention: No additional files need attention; the checked changes are limited to MetaIO stream-reading setup in metaObject.cxx and metaForm.cxx.

T-Rex T-Rex Logs

What T-Rex did

  • I compiled the MetaIO sources with GCC 12.2 and zlib and ran an end-to-end check against both the parent and changed sources; the test wrote headers, reopened streams, exercised ReadStream APIs, and verified multiple fields, with all 12 write/open/read/value checks passing for both implementations.
  • The parent and PR read-after-write outputs both show 12 PASS checks, as captured by the MetaIO read-after-write validation script and the corresponding logs.
  • The before-capture read-after-write validation run passed all 12 checks, the after-capture run passed the identical 12 checks, and the review harness emitted a non-fatal overload-ambiguity warning in both runs that does not affect the result.

View all artifacts

T-Rex Ran code and verified through T-Rex

Reviews (1): Last reviewed commit: "BUG: Remove unnecessary global fflush(nu..." | Re-trigger Greptile

blowekamp added a commit to blowekamp/SimpleITK that referenced this pull request Sep 3, 2026
Use fixed, moderate thread/file/repeat/timeout counts instead of scaling
nThreads off std::thread::hardware_concurrency(), which spawned up to ~70+
threads on many-core machines. Verified the reduced load (16 threads, 16
files, 4 repeats, 60s timeout) still reliably reproduces the MetaIO
concurrency hang fixed by ITK PR InsightSoftwareConsortium/ITK#6833 when
run against unpatched ITK, while completing in a few seconds against
patched ITK.

MetaObject::M_Destroy();

fflush(nullptr);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the record, this fflush call was already there in https://github.com/Kitware/MetaIO/blob/bd09e2993574d7b7f2890038f8dffde4a07d7160/src/metaObject.cxx#L143 as added with commit Kitware/MetaIO@bd09e29, @aylward, Aug 6, 2002.

@blowekamp

Copy link
Copy Markdown
Member Author

@N-Dekker The fix has been ported upstream.

dzenanz pushed a commit to Kitware/MetaIO that referenced this pull request Sep 4, 2026
…adlocks

MetaObject::ReadStream() and MetaForm::ReadStream() unconditionally call
fflush(nullptr) before parsing, a leftover from the original MetaIO commit
that predates any multithreading concerns for this library. fflush(NULL)
flushes every open FILE* stream in the entire process, not just the one
being read.

On platforms such as macOS, this requires walking the process-wide list of
all open FILE* streams and locking each one in turn. Under heavy concurrent
MetaImage/MHA reads (many threads each opening/reading/closing their own
files), this global flush-all creates severe lock contention that can grind
concurrent reads to a halt indefinitely, since each thread's fflush(nullptr)
call competes to lock every other thread's in-flight FILE* handle.

Reading does not require flushing any previously written data, so this call
serves no purpose and can simply be removed.

Ported from InsightSoftwareConsortium/ITK#6833.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@blowekamp

Copy link
Copy Markdown
Member Author

Superseded by: #6835

@blowekamp blowekamp closed this Sep 4, 2026
dzenanz pushed a commit that referenced this pull request Sep 4, 2026
…adlocks

MetaObject::ReadStream() and MetaForm::ReadStream() unconditionally call
fflush(nullptr) before parsing, a leftover from the original MetaIO commit
that predates any multithreading concerns for this library. fflush(NULL)
flushes every open FILE* stream in the entire process, not just the one
being read.

On platforms such as macOS, this requires walking the process-wide list of
all open FILE* streams and locking each one in turn. Under heavy concurrent
MetaImage/MHA reads (many threads each opening/reading/closing their own
files), this global flush-all creates severe lock contention that can grind
concurrent reads to a halt indefinitely, since each thread's fflush(nullptr)
call competes to lock every other thread's in-flight FILE* handle.

Reading does not require flushing any previously written data, so this call
serves no purpose and can simply be removed.

Backport of the same fix on main (PR #6833).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:ThirdParty Issues affecting the ThirdParty module type:Bug Inconsistencies or issues which will cause an incorrect result under some or all circumstances

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants