<?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>Concurrency on SpaceShaman</title>
    <link>https://spaceshaman.github.io/tags/concurrency/</link>
    <description>Recent content in Concurrency on SpaceShaman</description>
    <generator>Hugo</generator>
    <language>en-US</language>
    <copyright>SpaceShaman</copyright>
    <lastBuildDate>Thu, 08 Oct 2026 00:08:00 +0000</lastBuildDate>
    <atom:link href="https://spaceshaman.github.io/tags/concurrency/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>How to Lock a Function Across Processes with a Decorator in Python</title>
      <link>https://spaceshaman.github.io/posts/how-to-lock-a-function-across-processes-with-a-decorator/</link>
      <pubDate>Thu, 08 Oct 2026 00:08:00 +0000</pubDate>
      <guid>https://spaceshaman.github.io/posts/how-to-lock-a-function-across-processes-with-a-decorator/</guid>
      <description>How to use a decorator and a file lock to prevent different processes from executing a function concurrently in Python.</description>
      <content:encoded><![CDATA[<p>Sometimes several processes can call the same function at once, even though its logic is completely unsuited to that. One worker changes the password for an external system, another does exactly the same thing, and a third is trying to log in. Everyone meant well, but now nobody knows the current password XD.</p>
<p>A similar problem comes up when refreshing shared data, generating the same report, or modifying a file. If these operations get in each other&rsquo;s way, we get a classic <em>race condition</em>: a race where the winner sometimes turns out to be an error message.</p>
<p>I wanted to solve this with a decorator that lets you choose between two behaviors:</p>
<ul>
<li><strong><code>skip</code></strong> — if someone is already executing the function, skip the next call.</li>
<li><strong><code>wait</code></strong> — wait until the function is available, then execute it.</li>
</ul>
<p>I also needed an optional delay after acquiring a lock that had previously been held. Some external systems need a moment to digest a change. Apparently, they enjoy coffee breaks too.</p>
<h2 id="the-decorator">The Decorator</h2>
<p>For locking, I used <a href="https://docs.python.org/3/library/fcntl.html#fcntl.flock"><code>fcntl.flock</code></a>, which lets you place an operating system lock on an open file. The <code>fcntl</code> module is available on Unix systems, so this example is primarily intended for Linux. The type parameter syntax requires Python 3.12 or later.</p>
<p>Here&rsquo;s the complete implementation:</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="how-to-use-it">How to Use It</h2>
<p>When another concurrent call is unnecessary, the default <code>skip</code> mode is enough:</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>The first process acquires the lock and refreshes the data. If the second encounters a lock that&rsquo;s already held, it skips the function body and receives <code>None</code>.</p>
<p>You can also add a delay:</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>Here, a call that encounters a lock that&rsquo;s already held <strong>waits to acquire it, waits another 60 seconds, and only then returns without executing the function</strong>. This behavior is intentional: the process resumes its other work after the competing operation has finished and the extra pause has elapsed.</p>
<p>If every call should be executed, choose <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>Suppose three processes try to run the function. The first executes it immediately. The other two wait. Once the lock is released, one of them acquires it, waits two seconds, and executes the function. Then it&rsquo;s the last process&rsquo;s turn.</p>
<p>All three calls will execute, but one at a time. Don&rsquo;t assume they&rsquo;ll run in the order they arrived, though — a lock isn&rsquo;t a queue with numbered tickets.</p>
<p><strong><code>delay</code> applies only when the first attempt to acquire the lock fails.</strong> If the lock was free, the function starts without a delay. This parameter isn&rsquo;t a timeout either: in <code>wait</code> mode, a process can wait for as long as the lock remains held.</p>
<h2 id="how-it-all-works">How It All Works</h2>
<h3 id="a-shared-lock-file">A Shared Lock File</h3>
<p>When decorating the function, I first determine the file path:</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> returns the function&rsquo;s location, and <code>resolve()</code> produces an absolute path. I replace slashes with underscores and append the function name to produce a filename in the shared temporary directory.</p>
<p>This means functions with the same name in different files will usually get separate locks. Call arguments don&rsquo;t affect the name: <code>update_shared_file(&quot;a&quot;)</code> and <code>update_shared_file(&quot;b&quot;)</code> compete for the same lock.</p>
<h3 id="wrapping-the-original-function">Wrapping the Original Function</h3>
<p><code>decorator</code> takes a function, and <code>wrapper</code> replaces it when it&rsquo;s called. Inside <code>wrapper</code>, we acquire the lock and, if appropriate, run the original body:</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> preserves the function&rsquo;s metadata, while the type parameters <code>P</code> and <code>R</code> describe its arguments and return value. The decorated function can also return <code>None</code>, because <code>skip</code> mode allows execution to be skipped.</p>
<h3 id="attempting-to-acquire-the-lock">Attempting to Acquire the Lock</h3>
<p>On each call, I create the directory if it&rsquo;s missing and open the file. Then I try to acquire the lock:</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> means an exclusive lock, and <code>LOCK_NB</code> disables waiting. If the lock is already held, I get a <code>BlockingIOError</code>. I then either skip the call immediately or retry without <code>LOCK_NB</code>, this time waiting for access. The <a href="https://man7.org/linux/man-pages/man2/flock.2.html"><code>flock</code> documentation</a> describes these flags in detail.</p>
<p>The <code>contended</code> variable records whether the first attempt encountered a lock that was already held.</p>
<h3 id="pausing-and-executing">Pausing and Executing</h3>
<p>After acquiring a lock that was previously held, I run <code>sleep(delay)</code> and then either skip the function or execute it, depending on the mode.</p>
<p>I <strong>hold the lock</strong> throughout the pause. This prevents another process from jumping ahead of me while I wait.</p>
<h3 id="releasing-the-lock">Releasing the Lock</h3>
<p>Finally, the <code>finally</code> block runs:</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>The lock is released even if the function raises an exception. The exception itself propagates to the caller, and <code>with</code> closes the file.</p>
<p>I don&rsquo;t delete the file. Its existence doesn&rsquo;t mean the lock is held — that&rsquo;s determined by the operating system lock. Deleting and recreating the file could cause processes to lock different files at the same path.</p>
<h2 id="the-limits-of-this-approach">The Limits of This Approach</h2>
<p>The processes must see the same lock file. Separate temporary directories in containers or different code locations can mean separate locks. This is a solution for coordinating processes in a shared environment, rather than a ready-made distributed lock.</p>
<p>All competing calls should also use the decorator. The lock is advisory: code that ignores it can still modify the shared resource. <a href="https://man7.org/linux/man-pages/man2/flock.2.html"><code>flock</code></a> won&rsquo;t keep the entire application in check for us.</p>
<h2 id="summary">Summary</h2>
<p>A few lines of decorator code are enough to move lock handling out of the function body. <code>skip</code> lets you skip a competing call, <code>wait</code> executes it after acquiring the lock, and <code>delay</code> provides a little extra breathing room after encountering a lock that was already held.</p>
<p>This won&rsquo;t solve every race condition in the project, but at least the processes will stop jostling in the doorway to this one function 😉.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
