{"atom":"regression/concurrent-output","family":"regression","slug":"concurrent-output","header":{"atom":"regression/concurrent-output","kind":"whole-program — a minimal repro, not yet reduced to an atom","source":"curated"},"axes":null,"cols":null,"engines":["rakudo-2026.07","rakupp-3.14.0"],"kinds":["ref","pp"],"defFrom":"rakupp:t/regression/concurrent-output.raku","counts":{"a":1,"f":0,"d":0,"c":0,"r":0,"n":0},"files":["atoms/regression/concurrent-output.grid"],"records":[["0001","","# Regression: two things about where output goes, found together.\n#\n# 1. CONCURRENT WRITES. Every runtime write to the process's own streams now\n#    takes one lock (rtOutMutex in Builtins.cpp). std::cout already guaranteed\n#    the BYTES of a single `<<` would not interleave — libc++ writes through the\n#    FILE*, which locks — but the stream's own state word is touched by every\n#    sentry without synchronisation, so ThreadSanitizer reported a data race on\n#    ANY program printing from two threads. Since v3.0.0 made parallel the\n#    default, that is any program at all. A real lock rather than a suppression,\n#    so the report goes away honestly and the NEXT race stays visible.\n#\n# 2. `printf` IGNORED `$*OUT`. It wrote std::cout directly, so\n#    `my $*OUT = open(…); printf(…)` printed to the terminal while `say` on the\n#    next line went to the file. Both the sub and the `.printf` method now go\n#    through ioEmit, which is what honours the rebinding — and takes the lock.\n#\n# Contract: exit 0 + last line PASS.\nmy @fail;\n\n# --- 1. concurrent output is whole-write atomic ------------------------------\n# Each thread says one long uniform line; any line that is not 200 copies of a\n# single character means two writes interleaved.\n{\n    my $tmp = $*TMPDIR.add(\"rakupp-concurrent-output-{$*PID}.txt\");\n    {\n        my $fh = $tmp.open(:w);\n        my $*OUT = $fh;\n        await ^12 .map: -> $i { start { say ($i.base(16).lc x 200) } }\n        $fh.close;\n    }\n    my @lines = $tmp.lines;\n    @fail.push(\"expected 12 lines, got {@lines.elems}\") unless @lines.elems == 12;\n    for @lines -> $l {\n        @fail.push(\"interleaved line: {$l.chars} chars, {$l.comb.unique.elems} distinct\")\n            unless $l.chars == 200 && $l.comb.unique.elems == 1;\n    }\n    $tmp.unlink;\n}\n\n# --- 2. printf honours a rebound $*OUT, in order -----------------------------\n{\n    my $tmp = $*TMPDIR.add(\"rakupp-printf-out-{$*PID}.txt\");\n    {\n        my $fh = $tmp.open(:w);\n        my $*OUT = $fh;\n        say \"one\";\n        \"%s\\n\".printf(\"two-method\");     # the .printf METHOD (invocant is the format)\n        printf(\"%s\\n\", \"three-builtin\"); # the printf SUB\n        say \"four\";\n        $fh.close;\n    }\n    my $got = $tmp.slurp;\n    my $want = \"one\\ntwo-method\\nthree-builtin\\nfour\\n\";\n    @fail.push(\"printf/\\$*OUT: got {$got.raku}, want {$want.raku}\") unless $got eq $want;\n    $tmp.unlink;\n}\n\n# --- and printing still works at all -----------------------------------------\n{\n    my @r = await ^8 .map: { start { 7 } };\n    @fail.push(\"threads broke: {@r.raku}\") unless @r.elems == 8 && all(@r.map: * == 7);\n}\n\nif @fail {\n    note \"FAILED:\\n\" ~ @fail.map({ \"  - $_\" }).join(\"\\n\");\n    exit 1;\n}\nsay \"PASS\";","output PASS","a",["PASS","PASS"],0,[["budget","20s"]]]]}