futexは、Linuxが提供する低レベルの待機・ウェイクアップメカニズムです。一般的なミューテックスは、競合がないパスをユーザー空間に留め、ブロックやウェイクアップが必要になった場合にのみカーネルに入ります。futexは完全なロックそのものではなく、すべての言語、プラットフォーム、あるいはすべての同期プリミティブにとって唯一の実装経路でもありません。
同期プロトコルを設計する際には、以下の点を明確に答える必要があります:リソースを誰が保持するか、データをどのように公開するか、誰がスリープすべきか、状態が変更された後に誰をウェイクアップすべきか。futexは、主に後2つの項目と共有状態の間の接続を解決します。
なぜ単純にチェックしてからスリープするだけではダメなのか
Wがreadyが0であることを確認し、スリープしようとしていると仮定します。その後、Pがreadyを1に変更してウェイクアップを試みますが、この時点ではWはまだキューに入っていません。もしWが無条件にスリープし、その後に通知がない場合、既に発生した変化を見逃すことになります。
FUTEX_WAITは、futex wordを読み取り、expectedと比較し、待機に入るという一連の操作を原子性のある「比較してブロック(compare-and-block)」として実行します。もし値が既に異なっていれば、EAGAINを返してスリープしません。FUTEX_WAIT
これには、公開側がまず状態を変更し、その後プロトコルに従ってウェイクアップを行う必要があります。観測可能な状態を残さずにWAKEだけを呼び出しても、まだ待機していないスレッドを確実に通知することはできません。値が同じ値に戻って繰り返される場合、代数、シーケンス番号、またはより完全なプロトコルを使用して世代を区別する必要があり、1回の比較ですべてのABA問題が解消されるわけではありません。
2つの基本操作
| 操作 | 意味 | 戻り値後に必要な処理 |
| FUTEX_WAIT(word, expected) | 値が一致している場合のみスリープする | 条件を再確認するか、ロックを獲得する |
| FUTEX_WAKE(word, n) | 最大n個の待機者をウェイクアップする | 公開側は依然として共有状態プロトコルに従う必要がある |
従来のfutex wordは、64ビットプラットフォームで実行されていても、4バイト境界に整列された32ビット値です。通常のWAITはユーザーのロック状態を変更せず、WAKEはロックの所有権を渡すものでもなく、一般的なFIFOの公平性の保証もありません。futex(2)
WAITが0を返すのは、今回の待機がウェイクアップされたことを示すだけで、これに基づいてすぐにクリティカルセクションに入ることができるわけではありません。EAGAINは比較時に状態が変更されたことを示し、EINTRはシグナルによって中断されたことを示し、タイムアウトはビジネス条件が成立しないことを必ずしも意味しません。これらのパスでは、すべて同期プロトコルに戻って処理する必要があります。
FUTEX_WAITのtimeoutは相対時間ですが、他の操作では絶対的な期限を使用する場合があります。相対待機を再試行する際、毎回元の期間を完全に与えると、総タイムアウトが予期せず延長される可能性があります。同じ期限に基づいて残り時間を計算するか、適切な絶対時間インターフェースを使用すべきです。
ウェイクアップが保存されていない場合、WAITはどうしてスリープを見逃さないのか?
まず公開してからWAKEする場合、ウェイクアップ数は0になり得ます。その後、WAITがword=1と比較して、EAGAINを返してスリープしません。逆に、まずWAITしてから公開とWAKEを行う場合、待機者はキューから外れます。WAKEのみを行い、公開を行わない場合も可能です:待機者は目覚めますが、wordは依然として0であり、これはウェイクアップと条件の成立が別物であることを意味します。
図を準備しています
ウェイクアップが保存されていない場合、WAITはどうしてスリープを見逃さないのか?WAKEは将来の通知を保存せず、WAITは現在のwordを比較してキュー入りを決定する。ウェイクアップされた後も条件を再確認する必要があり、ロック獲得と見なしてはならない。
{"id":"futex-compare-and-block","title":"ウェイクアップが保存されていない場合、WAITはどうしてスリープを見逃さないのか?","summary":"WAKEは将来の通知を保存せず、WAITは現在のwordを比較してキュー入りを決定する。ウェイクアップされた後も条件を再確認する必要があり、ロック獲得と見なしてはならない。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"actions\"><button id=\"publish\" data-i18n=\"publish\"></button><button id=\"wake\" data-i18n=\"wake\"></button><button id=\"wait\" data-i18n=\"wait\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><div class=\"track-row\"><span data-i18n=\"queue\"></span><div id=\"queue\" class=\"track\"></div></div><div class=\"metrics\"><div><span data-i18n=\"word\"></span><strong id=\"word\"></strong></div><div><span data-i18n=\"returned\"></span><strong id=\"returned\"></strong></div><div><span data-i18n=\"woken\"></span><strong id=\"woken\"></strong></div></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n.metrics strong{font-size:17px}#returned{min-height:4em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}let word=0,waiting=false,result='none',woken=null;function draw(){set('word',word);set('returned',t(result));set('woken',woken??'—');cells('queue',[{text:waiting?'W':'∅',cls:waiting?'on':'empty'}]);$('#wait').disabled=waiting;$('#publish').disabled=word===1;$('#queue').dataset.waiting=waiting;}$('#publish').onclick=()=>{word=1;draw()};$('#wake').onclick=()=>{woken=waiting?1:0;if(waiting){waiting=false;result='check'}draw()};$('#wait').onclick=()=>{if(word===0){waiting=true;result='sleep'}else result='again';draw()};$('#reset').onclick=()=>{word=0;waiting=false;result='none';woken=null;draw()};draw();","audio":false,"strings":{"again":"EAGAIN:スリープせず","check":"0:ウェイクアップ後に再確認","heading":"ウェイクアップが保存されていない場合、WAITはどうしてスリープを見逃さないのか?","intro":"公開、WAKE、WAIT(expected=0)をそれぞれ実行し、順序を自由に設定できる。","none":"未呼び出し","publish":"word=1を公開","queue":"待機キュー","reset":"リセット","returned":"直近のWAIT結果","scope":"公開者1つと待機者1つ。wordは0から1に変化するのみ。タイムアウトとシグナルは無視。完全なミューテックスではなく、データの公開には適切なメモリ順序が必要。","scopeLabel":"モデルの範囲と規約","sleep":"待機中","wait":"WAIT(expected=0)","wake":"WAKE(1)","woken":"直近のWAKEでウェイクアップされた数","word":"共有word"}}
3状態の一般的なミューテックスモデル
以下はアルゴリズムを説明するための擬似コードであり、pthread_mutex_tの内部レイアウトや、libcの実装を直接置き換えるものではありません。状態0は空き、1はロック済みで待機マークなし、2はロック済みで待機者が存在する可能性があることを示します。
lock:
CAS_acquire(state, 0, 1) が成功した場合:戻る
ループ:
old = exchange_acquire(state, 2)
old == 0 の場合:戻る
WAIT(state, expected=2)
戻り値に応じてエラー処理を行い、再競争する
unlock:
old = exchange_release(state, 0)
old == 2 の場合:WAKE(state, 1)
状態を2にマークするのは、解放者がウェイクアップが必要かもしれないことを知るためであり、待機者の正確な数を統計するためではありません。ウェイクアップされたスレッドが0から2への変更に成功し、マークを保持することで、後続の解放が他の待機者を押し進めることができます。新しく到着したスレッドは、ウェイクアップされたスレッドよりも先にロックを獲得する可能性があるため、このモデルは公平性を保証しません。
解放がWAITの前に発生した場合、比較の不一致によりスリープを見逃しません。待機が先にキューに入ると、後続のWAKEによって呼び出されます。このアルゴリズムには、アドレスのライフサイクル、キャンセル、タイムアウト、異常終了、境界エラーの処理も必要です。これこそが、アプリケーションが通常、成熟したpthreadや言語標準ライブラリのロックを使用する理由です。
ウェイクアップされた側が、新しく到着したスレッドに負ける可能性がある
解放者がまずstateを0に書き込み、Wをウェイクアップします。Wがまだスケジュールされる前に、新しいスレッドNがCAS(0, 1)に成功します。Wが実行されると、state=1を見て、再競争する必要があり、WAITが0を返したからといってすぐにクリティカルセクションに入れるわけではありません。
| 順序 | state | ロックの所有者 |
| Pがまだクリティカルセクション内にいる、Wは待機中 | 2 | P |
| Pが解放し、Wをウェイクアップ | 0 | 一時的に誰もいない |
| Nが先にCASに成功 | 1 | N |
| Wが実行再開、exchangeで2を取得し、旧値1を得る | 2 | 依然としてN |
| Nが解放し、旧値2を見て、引き続きウェイクアップ | 0 | 次の競争を待つ |
状態2の価値は、「後続の解放で通知が必要かもしれない」という情報を保持することにあります。これはWの所有権証憑でもなく、現在の待機者数でもありません。このスケジューリングは、通常のfutexロックが良好なスループットを持つ可能性がある一方で、待機の公平性や最悪の待機時間を直接約束できない理由を説明しています。
比較が一致しない場合、WAITはスリープしない
ここでは、単一スレッドのfutexインターフェースの比較と戻り値のみを検証し、並行ロックは実装しません。wordは既に1ですが、値0を待とうとしています。タイムアウトポインタがNULLであっても、直ちにEAGAINを得るはずです。
#define _GNU_SOURCE
#include <errno.h>
#include <linux/futex.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/syscall.h>
#include <unistd.h>
int main(void) {
_Alignas(4) uint32_t word = 1;
errno = 0;
long result = syscall(SYS_futex, &word, FUTEX_WAIT_PRIVATE,
0, NULL, NULL, 0);
int saved = errno;
printf("mismatch=%d\n", result == -1 && saved == EAGAIN);
return !(result == -1 && saved == EAGAIN);
}
futex-mismatch.cとして保存し、コンパイルして実行すると、mismatch=1が期待されます。この例ではlibcのsyscallを使用しているため、-1とerrnoが表示されます。通常のuint32_tの並行変更はありません。これをマルチスレッドプロトコルに拡張する場合、以下の原子オブジェクトとメモリ順序の制約を再満たす必要があります。WAITの比較セマンティクス
原子変数のメモリ順序は省略できない
ロックで保護されたデータは通常、unlockのreleaseと成功したlockのacquireの間に同期関係を作成します。futexのキューの原子性は、言語メモリモデルにおけるデータ公開プロトコルを代替できず、通常の共有整数の並行読み書きを自動的に有効な原子操作に変えることもできません。
volatileは特定の設定のコンパイラによるアクセスセマンティクスに影響を与えるのみで、スレッド間の原子性やacquire/releaseを代替できません。futexラッパーを直接実装する場合、原子オブジェクトのサイズ、アラインメント、ロックフリーサポート、およびターゲットABIとカーネルインターフェースの互換性を確認する必要があります。
PRIVATEフラグは、待機する両方が同じアドレス空間でそのオブジェクトを使用していることを示し、カーネルによる検索の最適化を許可します。プロセス間で共有されるオブジェクトには、共有マップに適した操作を使用する必要があり、PRIVATEを無条件により高速なオプションとして扱うことはできません。futexアドレスの破棄、マップ解除、または再利用の前には、古い操作がそれを参照していないことを確実にする必要があります。
PIとrobustは異なる問題を解決する
優先度継承(PI)は、低優先度のロック保持者が中優先度のタスクによって絶えずプリエンプトされ、高優先度の待機者が進めなくなる状況に対処します。カーネルは待機チェーンに沿って保持者の有効優先度を上げ、クリティカルセクションの完了を助けます。PIには特定の所有者エンコーディングと操作プロトコルが必要であり、通常のWAIT/WAKEで適当に実装することはできません。
pthreadインターフェースでは、ミューテックス属性を通じてPTHREAD_PRIO_INHERITを選択します。これはデッドロックを解決するものでもなく、クリティカルセクション自体を短縮するものでもなく、任意のスケジューリング条件下でデッドラインを達成することを保証するものでもありません。
Robust mutexは、ロック保持スレッドの死亡に対処します。次のpthread_mutex_lockはEOWNERDEADを返す可能性があり、この場合、呼び出し元はロックを獲得していますが、保護されたデータが一貫していない可能性があります。アプリケーションはデータを修復し、pthread_mutex_consistentを呼び出してロックを解除する必要があります。一貫性が回復されていない状態でロックを解除すると、後続の操作でENOTRECOVERABLEを得る可能性があります。
EOWNERDEADを単に「ロック取得失敗、ロックを獲得していない」として扱うことはできず、データをチェックせずに一律にconsistentとマークすることもできません。カーネルは異常な所有者状態を提供しますが、ビジネス修正を自動的にロールバックしません。Robust mutex
完全な回復の例:コミット済みコピーを使用して未完了の更新を回復
以下は、プロセス内で作業スレッドを1つ作成する例です。正常な不変条件はworking == committedです。作業スレッドはロックを取得してworkingを99に変更した後、直接終了し、コミットされていない更新をシミュレートします。回復側は「コミットされていない内容を破棄」を選択し、workingをcommitted=7に戻してから一貫性をマークします。
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
static pthread_mutex_t mutex;
static int committed = 7, working = 7;
static void require(int result) { if (result != 0) abort(); }
static void *unfinished(void *unused) {
(void)unused;
require(pthread_mutex_lock(&mutex));
working = 99;
return NULL;
}
int main(void) {
pthread_mutexattr_t attr;
require(pthread_mutexattr_init(&attr));
require(pthread_mutexattr_setrobust(&attr, PTHREAD_MUTEX_ROBUST));
require(pthread_mutex_init(&mutex, &attr));
require(pthread_mutexattr_destroy(&attr));
pthread_t thread;
require(pthread_create(&thread, NULL, unfinished, NULL));
require(pthread_join(thread, NULL));
int result = pthread_mutex_lock(&mutex);
if (result != EOWNERDEAD) abort();
if (working != 99 || committed != 7) abort();
working = committed;
require(pthread_mutex_consistent(&mutex));
require(pthread_mutex_unlock(&mutex));
require(pthread_mutex_lock(&mutex));
int recovered = working == 7 && working == committed;
require(pthread_mutex_unlock(&mutex));
require(pthread_mutex_destroy(&mutex));
printf("recovered=%d\n", recovered);
return !recovered;
}
robust-demo.cとして保存し、cc -Wall -Wextra -pthread robust-demo.c -o robust-demoでコンパイルします。期待される結果はrecovered=1であり、回復後の次のlockは正常に0を返します。Robust mutex 回復プロトコル
この例では、コミット済みコピーは異常なスレッドによって変更されていないため、回復ルールは明確です。複雑なオブジェクトの場合、部分的な書き込み、リンクリスト構造、外部副作用などが関与する可能性があります。もしcommitted自体が不完全である可能性がある場合、単純な代入は有効な回復とはなりません。検証可能な不変条件と回復データを事前に定義し、いつconsistentを呼び出すかを決定する必要があります。
EOWNERDEAD:ロックは獲得したが、データはまだ修復されていない
まず終了をシミュレートしてからlockすることで、EOWNERDEADと「回復スレッドがロックを保持」の両方を同時に確認できます。2つの後続パスを比較します:直接unlockすると回復不能状態に入ります。workingを修復し、consistentを宣言してからunlockすると、次のlockが正常に成功します。データの修復と一貫性の宣言は2つの操作であり、後者を省略しても回復プロトコルは完了しません。
図を準備しています
EOWNERDEAD:ロックは獲得したが、データはまだ修復されていないEOWNERDEADはロックを獲得したが、データが一貫していない可能性があることを示す。不変条件を回復し、consistentを宣言してから継続できる。回復されていない状態でロックを解除すると、後続の操作でENOTRECOVERABLEを得る。
{"id":"robust-mutex-recovery","title":"EOWNERDEAD:ロックは獲得したが、データはまだ修復されていない","summary":"EOWNERDEADはロックを獲得したが、データが一貫していない可能性があることを示す。不変条件を回復し、consistentを宣言してから継続できる。回復されていない状態でロックを解除すると、後続の操作でENOTRECOVERABLEを得る。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"actions\"><button id=\"crash\" data-i18n=\"crash\"></button><button id=\"lock\" data-i18n=\"lock\"></button><button id=\"repair\" data-i18n=\"repair\"></button><button id=\"consistent\" data-i18n=\"consistent\"></button><button id=\"unlock\" data-i18n=\"unlock\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><section class=\"panel\"><h3 data-i18n=\"data\"></h3><div id=\"data\"></div></section><div class=\"metrics\"><div><span data-i18n=\"owner\"></span><strong id=\"owner\"></strong></div><div><span data-i18n=\"result\"></span><strong id=\"result\"></strong></div><div><span data-i18n=\"status\"></span><strong id=\"status\"></strong></div></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n#data .tr{grid-template-columns:1fr 1fr}.metrics strong{font-size:16px}#status{min-height:5em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}let owner='original',state='healthy',working=7,result='—';function draw(){table('data',[['committed',7],['working',working]]);set('owner',t(owner));set('result',result);set('status',t(state));$('#status').dataset.state=state;$('#crash').disabled=owner!=='original';$('#lock').disabled=owner!=='none';$('#repair').disabled=owner!=='recovery'||state!=='inconsistent'||working===7;$('#consistent').disabled=owner!=='recovery'||state!=='inconsistent'||working!==7;$('#unlock').disabled=owner!=='recovery';}$('#crash').onclick=()=>{working=99;owner='none';state='dead';draw()};$('#lock').onclick=()=>{if(state==='unrecoverable')result='ENOTRECOVERABLE';else {owner='recovery';result=state==='dead'?'EOWNERDEAD':'0';if(state==='dead')state='inconsistent'}draw()};$('#repair').onclick=()=>{working=7;draw()};$('#consistent').onclick=()=>{state='healthy';draw()};$('#unlock').onclick=()=>{owner='none';if(state==='inconsistent')state='unrecoverable';draw()};$('#reset').onclick=()=>{owner='original';state='healthy';working=7;result='—';draw()};draw();","audio":false,"strings":{"consistent":"consistentを宣言","crash":"ロック保持者がコミットせずに終了","data":"保護されたデータ","dead":"所有者死亡、回復待ち","heading":"EOWNERDEAD:ロックは獲得したが、データはまだ修復されていない","healthy":"一貫","inconsistent":"ロック保持中だが、一貫性宣言待ち","intro":"ロック保持者が途中で終了するのをシミュレートし、修復後にロックを解除するか、直接解除するかを選択。","lock":"回復側がlock","none":"誰もいない","original":"元のロック保持者","owner":"現在の所有者","recovery":"回復スレッド","repair":"working=committedを回復","reset":"リセット","result":"直近のlock結果","scope":"単一のrobust mutex。コミット済みコピーcommitted=7は完全で信頼でき、回復ルールはworking=committed。モデルは偽りの一貫性宣言を許可しない。これはAPIがビジネスデータを自動的にチェックする能力ではない。","scopeLabel":"モデルの範囲と規約","status":"ミューテックスの一貫性状態","unlock":"回復側がunlock","unrecoverable":"回復不能"}}
その他の操作とfutex2の境界
FUTEX_REQUEUEなどの操作は待機キューを移動でき、一部の状態変数パターンにおけるスプーリング(驚群)を削減します。FUTEX_WAKE_OPは、2番目のwordに対する原子操作と条件付きウェイクアップを結合します。これは「原子加算の後にWAITを行う」ものではなく、FUTEX_OPは操作をエンコードするマクロです。FUTEX_WAKE_OP
futex_waitvは比較対象となるwordのセットを受け取り、そのいずれかがウェイクアップされるのを待機します。これは複数の待機ソースに適しています。あるインデックスで戻ったとしても、ビジネス状態を確認する必要があり、複数のロックを同時に獲得したことを意味しません。このインターフェースは絶対タイムアウトと指定されたクロックを使用します。
「futex2」には多项拡張の構想が含まれていましたが、提案書にあるすべての幅、NUMA、スケジューリング特性が6.xカーネルですでに備わっている事実として記述することはできません。本文で確認したカーネルのfutex2ドキュメントは、waitvに対してFUTEX_32を明確に要求しており、構造体フィールドは拡張のためにスペースを残しています。カーネル futex2 ドキュメント
パフォーマンスをどう理解するか
競合がないパスではシステムコールがない場合もありますが、原子読み書きには依然としてキャッシュ一貫性のオーバーヘッドが発生します。競合時には、スピニング、キュー操作、コンテキストスイッチ、スケジューリング遅延、NUMA間のデータ転送が含まれる可能性があります。「20ns」という固定値や、競合時に毎回必ずWAITとWAKEが1回ずつ行われるという普遍的な結論は存在しません。
ロックが遅い場合、保持時間、待機時間、争奪頻度、およびスケジューリングの影響を区別して観察します。関数エントリから戻りまでの追跡方法については、ftraceとbpftraceを参照してください。
キーワード:futex, compare and block, lost wakeup, FUTEX_WAIT, FUTEX_WAKE, acquire release, PI, robust mutex, EOWNERDEAD, futex_waitv