Problém je v tom, že fork() pouze zkopíruje volající vlákno a všechny mutexy držené v podřízených vláknech budou navždy uzamčeny v rozvětveném potomku. Řešení pthread bylo pthread_atfork()
manipulátory. Myšlenka byla, že můžete zaregistrovat 3 handlery:jeden prefork, jeden rodičovský handler a jeden podřízený handler. Když fork()
prefork je volán před forkem a očekává se, že získá všechny mutexy aplikace. Nadřazený i podřízený prvek musí uvolnit všechny mutexy v nadřazeném a podřízeném procesu.
To však není konec příběhu! Knihovny volají pthread_atfork
pro registraci obslužných rutin pro mutexy specifické pro knihovny, například Libc to dělá. To je dobrá věc:aplikace nemůže vědět o mutexech držených knihovnami třetích stran, takže každá knihovna musí volat pthread_atfork
zajistit, aby byly jeho vlastní mutexy vyčištěny v případě fork()
.
Problém je v tom, že pořadí pthread_atfork
handlery jsou volány pro nesouvisející knihovny není definován (záleží na pořadí, v jakém jsou knihovny načteny programem). To tedy znamená, že technicky může dojít k uváznutí uvnitř manipulátoru před vidlice kvůli závodnímu stavu.
Zvažte například tuto sekvenci:
- Volání vlákna T1
fork()
- obslužné nástroje prefork libc se volají v T1 (např. T1 nyní obsahuje všechny zámky libc)
- Dále ve vláknu T2 získá knihovna A třetí strany svůj vlastní mutex AM a poté provede volání libc, které vyžaduje mutex. Toto blokuje, protože mutexy libc jsou drženy T1.
- Vlákno T1 spouští obslužný program prefork pro knihovnu A, který blokuje čekání na získání AM, které má T2.
Došlo k vašemu uváznutí a nesouvisí to s vašimi vlastními mutexy nebo kódem.
To se skutečně stalo na projektu, na kterém jsem kdysi pracoval. Rada, kterou jsem tehdy našel, byla vybrat si vidlici nebo nitě, ale ne obojí. Ale pro některé aplikace to pravděpodobně není praktické.
Je bezpečné rozvětvovat se ve vícevláknovém programu, pokud jste velmi pozor na kód mezi forkem a exec. V tomto rozsahu můžete provádět pouze re-enterant (neboli asynchronně-bezpečná) systémová volání. Teoreticky tam nemáte povoleno malloc ani osvobodit, ačkoli v praxi je výchozí linuxový alokátor bezpečný a linuxové knihovny se na něj začaly spoléhat Konečným výsledkem je, že musíte použijte výchozí alokátor.