<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Blokady on SpaceShaman</title>
    <link>https://spaceshaman.github.io/pl/tags/blokady/</link>
    <description>Recent content in Blokady on SpaceShaman</description>
    <generator>Hugo</generator>
    <language>pl-PL</language>
    <copyright>SpaceShaman</copyright>
    <lastBuildDate>Thu, 08 Oct 2026 08:00:00 +0000</lastBuildDate>
    <atom:link href="https://spaceshaman.github.io/pl/tags/blokady/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Jak zablokować równoległe wywołania funkcji między procesami w Pythonie</title>
      <link>https://spaceshaman.github.io/pl/posts/how-to-lock-a-function-across-processes-with-a-decorator/</link>
      <pubDate>Thu, 08 Oct 2026 08:00:00 +0000</pubDate>
      <guid>https://spaceshaman.github.io/pl/posts/how-to-lock-a-function-across-processes-with-a-decorator/</guid>
      <description>Jak użyć dekoratora i blokady pliku, aby zapobiec równoległemu wykonywaniu funkcji przez różne procesy w Pythonie.</description>
      <content:encoded><![CDATA[<p>Czasami ta sama funkcja może zostać wywołana równolegle przez kilka procesów, choć jej logika zupełnie się do tego nie nadaje. Jeden worker zmienia hasło do zewnętrznego systemu, drugi robi dokładnie to samo, a trzeci próbuje się właśnie zalogować. Każdy chciał dobrze, tylko teraz nikt nie zna aktualnego hasła XD.</p>
<p>Podobny problem pojawia się przy odświeżaniu wspólnych danych, generowaniu tego samego raportu czy modyfikowaniu pliku. Jeśli operacje wejdą sobie w drogę, otrzymujemy klasyczny <em>race condition</em>, czyli wyścig, w którym zwycięzcą bywa zgłoszenie błędu.</p>
<p>Chciałem rozwiązać to za pomocą dekoratora, który pozwala wybrać jedno z dwóch zachowań:</p>
<ul>
<li><strong><code>skip</code></strong> — jeśli ktoś już wykonuje funkcję, pomiń kolejne wywołanie.</li>
<li><strong><code>wait</code></strong> — poczekaj, aż funkcja będzie dostępna, i dopiero wtedy ją wykonaj.</li>
</ul>
<p>Do tego potrzebowałem opcjonalnego opóźnienia po przejęciu wcześniej zajętej blokady. Niektóre zewnętrzne systemy potrzebują chwili, żeby przetrawić zmianę. Najwyraźniej też lubią przerwę na kawę.</p>
<h2 id="dekorator">Dekorator</h2>
<p>Do blokowania użyłem <a href="https://docs.python.org/3/library/fcntl.html#fcntl.flock"><code>fcntl.flock</code></a>, które pozwala założyć systemową blokadę na otwarty plik. Moduł <code>fcntl</code> jest dostępny na systemach uniksowych, więc ten przykład jest przeznaczony przede wszystkim dla Linuksa. Składnia parametrów typów wymaga Pythona 3.12 lub nowszego.</p>
<p>Tak wygląda cała implementacja:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">fcntl</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">collections.abc</span> <span class="kn">import</span> <span class="n">Callable</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">functools</span> <span class="kn">import</span> <span class="n">wraps</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">inspect</span> <span class="kn">import</span> <span class="n">getfile</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">tempfile</span> <span class="kn">import</span> <span class="n">gettempdir</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">time</span> <span class="kn">import</span> <span class="n">sleep</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Literal</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">lock_function</span><span class="p">[</span><span class="o">**</span><span class="n">P</span><span class="p">,</span> <span class="n">R</span><span class="p">](</span>
</span></span><span class="line"><span class="cl">    <span class="n">mode</span><span class="p">:</span> <span class="n">Literal</span><span class="p">[</span><span class="s2">&#34;skip&#34;</span><span class="p">,</span> <span class="s2">&#34;wait&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&#34;skip&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">delay</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Callable</span><span class="p">[[</span><span class="n">Callable</span><span class="p">[</span><span class="n">P</span><span class="p">,</span> <span class="n">R</span><span class="p">]],</span> <span class="n">Callable</span><span class="p">[</span><span class="n">P</span><span class="p">,</span> <span class="n">R</span> <span class="o">|</span> <span class="kc">None</span><span class="p">]]:</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">decorator</span><span class="p">(</span><span class="n">func</span><span class="p">:</span> <span class="n">Callable</span><span class="p">[</span><span class="n">P</span><span class="p">,</span> <span class="n">R</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="n">Callable</span><span class="p">[</span><span class="n">P</span><span class="p">,</span> <span class="n">R</span> <span class="o">|</span> <span class="kc">None</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">        <span class="n">source</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">Path</span><span class="p">(</span><span class="n">getfile</span><span class="p">(</span><span class="n">func</span><span class="p">))</span><span class="o">.</span><span class="n">resolve</span><span class="p">())</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">&#34;/&#34;</span><span class="p">,</span> <span class="s2">&#34;_&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">&#34;.py&#34;</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">filename</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">source</span><span class="si">}</span><span class="s2">_</span><span class="si">{</span><span class="n">func</span><span class="o">.</span><span class="vm">__name__</span><span class="si">}</span><span class="s2">.lock&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="n">lock_path</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="n">gettempdir</span><span class="p">())</span> <span class="o">/</span> <span class="s2">&#34;locks&#34;</span> <span class="o">/</span> <span class="n">filename</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="nd">@wraps</span><span class="p">(</span><span class="n">func</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">def</span> <span class="nf">wrapper</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">:</span> <span class="n">P</span><span class="o">.</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">:</span> <span class="n">P</span><span class="o">.</span><span class="n">kwargs</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">R</span> <span class="o">|</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">lock_path</span><span class="o">.</span><span class="n">parent</span><span class="o">.</span><span class="n">mkdir</span><span class="p">(</span><span class="n">parents</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">exist_ok</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="k">with</span> <span class="n">lock_path</span><span class="o">.</span><span class="n">open</span><span class="p">(</span><span class="s2">&#34;a&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">lock_file</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                    <span class="n">fcntl</span><span class="o">.</span><span class="n">flock</span><span class="p">(</span><span class="n">lock_file</span><span class="p">,</span> <span class="n">fcntl</span><span class="o">.</span><span class="n">LOCK_EX</span> <span class="o">|</span> <span class="n">fcntl</span><span class="o">.</span><span class="n">LOCK_NB</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                <span class="k">except</span> <span class="ne">BlockingIOError</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                    <span class="k">if</span> <span class="n">mode</span> <span class="o">==</span> <span class="s2">&#34;skip&#34;</span> <span class="ow">and</span> <span class="n">delay</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                        <span class="k">return</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">                    <span class="n">fcntl</span><span class="o">.</span><span class="n">flock</span><span class="p">(</span><span class="n">lock_file</span><span class="p">,</span> <span class="n">fcntl</span><span class="o">.</span><span class="n">LOCK_EX</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                    <span class="n">contended</span> <span class="o">=</span> <span class="kc">True</span>
</span></span><span class="line"><span class="cl">                <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                    <span class="n">contended</span> <span class="o">=</span> <span class="kc">False</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">                <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                    <span class="k">if</span> <span class="n">contended</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                        <span class="n">sleep</span><span class="p">(</span><span class="n">delay</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                        <span class="k">if</span> <span class="n">mode</span> <span class="o">==</span> <span class="s2">&#34;skip&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                            <span class="k">return</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">                    <span class="k">return</span> <span class="n">func</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                <span class="k">finally</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                    <span class="n">fcntl</span><span class="o">.</span><span class="n">flock</span><span class="p">(</span><span class="n">lock_file</span><span class="p">,</span> <span class="n">fcntl</span><span class="o">.</span><span class="n">LOCK_UN</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">wrapper</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">decorator</span>
</span></span></code></pre></div><h2 id="jak-go-używać">Jak go używać</h2>
<p>Gdy kolejne równoległe wywołanie jest zbędne, wystarczy domyślny tryb <code>skip</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="nd">@lock_function</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">refresh_shared_cache</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span></code></pre></div><p>Pierwszy proces przejmuje blokadę i odświeża dane. Jeśli drugi trafi na zajętą blokadę, pomija ciało funkcji i otrzymuje <code>None</code>.</p>
<p>Można też dodać opóźnienie:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="nd">@lock_function</span><span class="p">(</span><span class="n">mode</span><span class="o">=</span><span class="s2">&#34;skip&#34;</span><span class="p">,</span> <span class="n">delay</span><span class="o">=</span><span class="mi">60</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">change_password</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span></code></pre></div><p>Tutaj wywołanie, które trafiło na zajętą blokadę, <strong>czeka na jej przejęcie, odczekuje dodatkowe 60 sekund i dopiero wtedy kończy się bez wykonania funkcji</strong>. To celowe zachowanie: proces wróci do dalszej pracy po zakończeniu konkurencyjnej operacji i dodatkowej pauzie.</p>
<p>Jeśli każde wywołanie powinno zostać wykonane, wybieramy <code>wait</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="nd">@lock_function</span><span class="p">(</span><span class="n">mode</span><span class="o">=</span><span class="s2">&#34;wait&#34;</span><span class="p">,</span> <span class="n">delay</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">update_shared_file</span><span class="p">(</span><span class="n">value</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span></code></pre></div><p>Załóżmy, że funkcję próbują uruchomić trzy procesy. Pierwszy wykonuje ją od razu. Pozostałe dwa czekają. Po zwolnieniu blokady jeden z nich przejmuje ją, odczekuje dwie sekundy i wykonuje funkcję. Następnie przychodzi kolej na ostatni proces.</p>
<p>Wszystkie trzy wywołania zostaną wykonane, ale pojedynczo. Nie należy przy tym zakładać kolejności zgłoszeń — blokada nie jest kolejką z numerkami.</p>
<p><strong><code>delay</code> działa tylko wtedy, gdy pierwsza próba przejęcia blokady nie powiodła się.</strong> Jeśli blokada była wolna, funkcja rusza bez opóźnienia. Parametr nie jest też limitem czasu oczekiwania: w trybie <code>wait</code> proces może czekać tak długo, jak blokada pozostaje zajęta.</p>
<h2 id="jak-to-wszystko-działa">Jak to wszystko działa</h2>
<h3 id="wspólny-plik-blokady">Wspólny plik blokady</h3>
<p>Na początku dekorowania funkcji ustalam ścieżkę pliku:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">source</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">Path</span><span class="p">(</span><span class="n">getfile</span><span class="p">(</span><span class="n">func</span><span class="p">))</span><span class="o">.</span><span class="n">resolve</span><span class="p">())</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">&#34;/&#34;</span><span class="p">,</span> <span class="s2">&#34;_&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">&#34;.py&#34;</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">filename</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">source</span><span class="si">}</span><span class="s2">_</span><span class="si">{</span><span class="n">func</span><span class="o">.</span><span class="vm">__name__</span><span class="si">}</span><span class="s2">.lock&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">lock_path</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="n">gettempdir</span><span class="p">())</span> <span class="o">/</span> <span class="s2">&#34;locks&#34;</span> <span class="o">/</span> <span class="n">filename</span>
</span></span></code></pre></div><p><code>getfile()</code> zwraca lokalizację funkcji, a <code>resolve()</code> tworzy ścieżkę bezwzględną. Zamieniam ukośniki na podkreślenia i dokładam nazwę funkcji, żeby uzyskać nazwę pliku we wspólnym katalogu tymczasowym.</p>
<p>Dzięki temu funkcje o tej samej nazwie w różnych plikach zwykle otrzymają osobne blokady. Argumenty wywołania nie wpływają na nazwę: <code>update_shared_file(&quot;a&quot;)</code> i <code>update_shared_file(&quot;b&quot;)</code> konkurują o ten sam lock.</p>
<h3 id="opakowanie-oryginalnej-funkcji">Opakowanie oryginalnej funkcji</h3>
<p><code>decorator</code> przyjmuje funkcję, a <code>wrapper</code> zastępuje ją podczas wywołań. To wewnątrz <code>wrapper</code> przejmujemy blokadę i ewentualnie uruchamiamy oryginalne ciało:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">return</span> <span class="n">func</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
</span></span></code></pre></div><p><code>@wraps(func)</code> zachowuje metadane funkcji, a parametry typów <code>P</code> i <code>R</code> opisują jej argumenty oraz wynik. Wynik dekorowanej funkcji może dodatkowo być <code>None</code>, ponieważ tryb <code>skip</code> pozwala pominąć wykonanie.</p>
<h3 id="próba-przejęcia-blokady">Próba przejęcia blokady</h3>
<p>Przy każdym wywołaniu tworzę katalog, jeśli go brakuje, i otwieram plik. Następnie próbuję przejąć blokadę:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">fcntl</span><span class="o">.</span><span class="n">flock</span><span class="p">(</span><span class="n">lock_file</span><span class="p">,</span> <span class="n">fcntl</span><span class="o">.</span><span class="n">LOCK_EX</span> <span class="o">|</span> <span class="n">fcntl</span><span class="o">.</span><span class="n">LOCK_NB</span><span class="p">)</span>
</span></span></code></pre></div><p><code>LOCK_EX</code> oznacza blokadę wyłączną, a <code>LOCK_NB</code> wyłącza oczekiwanie. Jeśli blokada jest zajęta, dostaję <code>BlockingIOError</code>. Wtedy albo od razu pomijam wywołanie, albo ponawiam próbę bez <code>LOCK_NB</code>, tym razem czekając na dostęp. Szczegóły tych flag opisuje <a href="https://man7.org/linux/man-pages/man2/flock.2.html">dokumentacja <code>flock</code></a>.</p>
<p>Zmienna <code>contended</code> zapamiętuje, czy pierwsza próba trafiła na zajętą blokadę.</p>
<h3 id="pauza-i-wykonanie">Pauza i wykonanie</h3>
<p>Po przejęciu wcześniej zajętej blokady wykonuję <code>sleep(delay)</code>, a następnie pomijam funkcję lub ją uruchamiam, zależnie od trybu.</p>
<p>Przez całą pauzę <strong>trzymam blokadę</strong>. Dzięki temu inny proces nie wskoczy przede mnie podczas oczekiwania.</p>
<h3 id="zwalnianie-blokady">Zwalnianie blokady</h3>
<p>Na końcu działa <code>finally</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">finally</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">fcntl</span><span class="o">.</span><span class="n">flock</span><span class="p">(</span><span class="n">lock_file</span><span class="p">,</span> <span class="n">fcntl</span><span class="o">.</span><span class="n">LOCK_UN</span><span class="p">)</span>
</span></span></code></pre></div><p>Blokada zostaje zwolniona także wtedy, gdy funkcja zgłosi wyjątek. Sam wyjątek trafia dalej do wywołującego, a <code>with</code> zamyka plik.</p>
<p>Pliku nie usuwam. Jego istnienie nie oznacza zajętej blokady — decyduje o tym systemowy lock. Usunięcie i ponowne utworzenie pliku mogłoby sprawić, że procesy blokowałyby różne pliki pod tą samą ścieżką.</p>
<h2 id="gdzie-są-granice-tego-rozwiązania">Gdzie są granice tego rozwiązania</h2>
<p>Procesy muszą widzieć ten sam plik blokady. Osobne katalogi tymczasowe w kontenerach czy inne lokalizacje kodu mogą oznaczać osobne locki. To rozwiązanie do współpracy procesów we wspólnym środowisku, nie gotowa blokada rozproszona.</p>
<p>Wszystkie konkurujące wywołania powinny też korzystać z dekoratora. Blokada ma charakter umowny: kod, który ją ignoruje, nadal może zmodyfikować wspólny zasób. <a href="https://man7.org/linux/man-pages/man2/flock.2.html"><code>flock</code></a> nie przypilnuje za nas całej aplikacji.</p>
<h2 id="podsumowanie">Podsumowanie</h2>
<p>Kilka linijek dekoratora wystarcza, żeby przenieść obsługę blokady poza ciało funkcji. <code>skip</code> pozwala pominąć konkurencyjne wywołanie, <code>wait</code> wykonuje je po przejęciu locka, a <code>delay</code> daje dodatkową chwilę oddechu po zajętej blokadzie.</p>
<p>Wyścigów w całym projekcie to nie rozwiąże, ale przynajmniej procesy przestaną przepychać się w drzwiach do tej jednej funkcji 😉.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
