Способы обхода GIL для повышения производительности

84f772461d28333d5eadf605224b14fc.jpg

Привет, Хабр!

Global Interpreter Lock в Питоне предотвращает одновременное выполнение нескольких потоков в одном процессе интерпретатора Python. Т.е даже на многоядерном процессоре многопоточные Python-приложения будут выполняться только в одном потоке за раз. Это было введено для некой потокобезопасности при работе с объектами Python, упрощая тем самым разработку на уровне интерпретатора.

На первый взгляд, GIL кажется разумным компромиссом для упрощения разработки. Однако, когда есть многоядерные процессоры и появляется необходимость в высокопроизводительных вычислениях GIL серьезно ограничивает возможности масштабирования и параллельную работу.

В этой статье рассмотрим способы обхода GIL и первый способ — использование многопроцессности вместо многопоточности.

Использование многопроцессности вместо многопоточности

Многопоточность оперирует потоками внутри одного процесса, деля память и состояние, тогда как многопроцессность запускает отдельные процессы, каждый со своей памятью и состоянием.

Реализовать многопроцессность можно с помощью либы multiprocessing. Она дает возможность каждому процессу работать с собственным интерпретатором Python и, соответственно, собственным GIL.Т. е каждый процесс может полноценно использовать отдельное ядро процессора, обходя ограничения, налагаемые GIL на многопоточное выполнение кода.

Основные функции multiprocessing

Блокировки используются для синхронизации доступа к ресурсам между разными процессами. Например, можно использовать Lock для гарантии того, что только один процесс может выполнять определенный участок кода одновременно:

from multiprocessing import Process, Lock

def printer(item, lock):
    lock.acquire()
    try:
        print(item)
    finally:
        lock.release()

if __name__ == '__main__':
    lock = Lock()
    items = ['тест1', 'тест2', 'тест3']
    for item in items:
        p = Process(target=printer, args=(item, lock))
        p.start()

Семафоры похожи на блокировки, но позволяют ограничить доступ к ресурсу не одним, а несколькими процессами одновременно:

from multiprocessing import Semaphore, Process

def worker(semaphore):
    with semaphore:
        # работа, требующая синхронизации
        print('Работает')

if __name__ == '__main__':
    semaphore = Semaphore(2)
    for _ in range(4):
        p = Process(target=worker, args=(semaphore,))
        p.start()

События позволяют процессам ожидать сигнал от других процессов для начала выполнения определенных действий:

from multiprocessing import Process, Event
import time

def waiter(event):
    print('Ожидание события')
    event.wait()
    print('Событие произошло')

if __name__ == '__main__':
    event = Event()

    for _ in range(3):
        p = Process(target=waiter, args=(event,))
        p.start()

    print('Главный процесс спит')
    time.sleep(3)
    event.set()

Очереди в multiprocessing позволяют безопасно обмениваться данными между процессами:

from multiprocessing import Process, Queue

def worker(queue):
    queue.put('Элемент от процесса')

if __name__ == '__main__':
    queue = Queue()
    p = Process(target=worker, args=(queue,))
    p.start()
    p.join()
    print(queue.get())

Асинхронное программирование

asyncio — это библиотека для написания конкурентного кода с использованием синтаксиса async/await, введенного в Python 3.5. Она служит базой для многих асинхронных фреймворков Python

В отличие от многопоточного исполнения, asyncio использует единственный поток и event loop для управления асинхронными операциями, что позволяет обходить ограничения, связанные с GIL.

Предположим, нужно собрать заголовки с нескольких веб-страниц. Будем юзатьaiohttp в качестве асинхронного HTTP-клиента для отправки запросов:

import asyncio
import aiohttp

async def fetch_title(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            html = await response.text()
            return html.split('')[1].split('')[0]

async def main(urls):
    tasks = [fetch_title(url) for url in urls]
    titles = await asyncio.gather(*tasks)
    for title in titles:
        print(title)

urls = [
    'https://example.com',
    'https://example.org',
    'https://example.net',
    # предположим, здесь список из тысячи URL
]

asyncio.run(main(urls))

Функция fetch_title асинхронно извлекает HTML-контент для заданного URL и возвращает содержимое тега </code>. А <code>main</code> создает задачи для каждого URL и запускает их параллельно с помощью <code>asyncio.gather()</code>. Таким образом можно тысячи веб-запросов одновременно, оптимизируя время ожидания ответов от серверов и эффективно используя ресурсы.</p> <h3>Интеграция с внешними С/С++ модулями</h3> <p>Весьма годный способ обхода ограничений GIL и повышения производительности при работе с CPU-интенсивными задачами. Создание расширений позволяет напрямую обращаться к системным вызовам и C-библиотекам, минуя оверхед интерпретатора Python и GIL.</p> <p>Расширение Python на C или C++ представляет собой разделяемую библиотеку, которая экспортирует функцию инициализации. Функция возвращает полностью инициализированный модуль или экземпляр <code>PyModuleDef</code>. Для модулей с именами в ASCII необходимо, чтобы функция инициализации называлась <code>PyInit_<имямодуля></code>. Для не ASCII имен модулей используется кодировка punycode и префикс <code>PyInitU_</code>.</p> <p>Для создания модуля на C, начинаем с определения методов модуля и таблицы методов, а затем определяем сам модуль: </p> <pre><code class="cs">static PyObject *method_fputs(PyObject *self, PyObject *args) { char *str, *filename = NULL; int bytes_copied = -1; if (!PyArg_ParseTuple(args, "ss", &str, &filename)) { return NULL; } FILE *fp = fopen(filename, "w"); bytes_copied = fputs(str, fp); fclose(fp); return PyLong_FromLong(bytes_copied); } static PyMethodDef CustomMethods[] = { {"fputs", method_fputs, METH_VARARGS, "Python interface to fputs C library function"}, {NULL, NULL, 0, NULL} }; static struct PyModuleDef custommodule = { PyModuleDef_HEAD_INIT, "custom", "Python interface for the custom C library function", -1, CustomMethods }; PyMODINIT_FUNC PyInit_custom(void) { return PyModule_Create(&custommodule); }</code></pre> <p>После определения функции инициализации и методов модуля создаем <code>setup.py</code> файл, чтобы скомпилировать модуль: </p> <pre><code class="python">from distutils.core import setup, Extension setup(name="custom", version="1.0", description="Python interface for the custom C library function", ext_modules=[Extension("custom", ["custommodule.c"])])</code></pre> <p>Выполнив команду <code>python3 setup.py install</code> в терминале модуль скомпилируется и установится став доступным для импорта в Python.</p> <p>Python и C/C++ имеют разные системы исключений. Если к примеру нужно выбросить исключение Python из C-расширения можно использовать API Python для работы с исключениями. Например, чтобы выбросить <code>ValueError</code> если строка меньше 10 символов: </p> <pre><code class="cs">if (strlen(str) < 10) { PyErr_SetString(PyExc_ValueError, "String length must be greater than 10"); return NULL; }</code></pre> <p>Таким образом можно использовать предопределенные исключения Python или создать свои собственные.</p> <p>Также некоторые библиотеки, к примеру как NumPy, Numba и Cython имеют встроенные возможности для обхода GIL.</p> <p><em>В завершение хочу порекомендовать вам </em><em>бесплатный вебинар</em><em> про очереди и отложенное выполнение на примере RabbitMQ в .Net</em> .</p> <p class="copyrights"><span class="source">© <a target="_blank" rel="nofollow" href="https://habr.com/ru/companies/otus/articles/806999/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=806999">Habrahabr.ru</a></span></p> </div> <br> <!--<div align="left"> <script type="text/topadvert"> load_event: page_load feed_id: 12105 pattern_id: 8187 tech_model: </script><script type="text/javascript" charset="utf-8" defer="defer" async="async" src="//loader.topadvert.ru/load.js"></script> </div> <br>--> <div style="padding-left: 20px;"> <script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-2514821055276660" crossorigin="anonymous"></script> <!-- PCNews 336x280 --> <ins class="adsbygoogle" style="display:block" data-ad-client="ca-pub-2514821055276660" data-ad-slot="1200562049" data-ad-format="auto"></ins> <script> (adsbygoogle = window.adsbygoogle || []).push({}); </script> </div> <!-- comments --> <noindex> <div style="margin: 25px;" id="disqus_thread"></div> <script type="text/javascript"> var disqus_shortname = 'pcnewsru'; var disqus_identifier = '1430921'; var disqus_title = 'Способы обхода GIL для повышения производительности'; var disqus_url = 'http://pcnews.ru/blogs/sposoby_obhoda_gil_dla_povysenia_proizvoditelnosti-1430921.html'; (function() { var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true; dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js'; (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq); })(); </script> <!--<noscript>Please enable JavaScript to view the <a rel="nofollow" href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>--> <!--<a href="http://disqus.com" rel="nofollow" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>--> </noindex> </div> <br class="clearer"/> </div> <br class="clearer"/> <div id="footer-2nd"></div> <div id="footer"> <br/><br/> <ul class="horz-menu"> <li class="about"><a href="/info/about.html" title="О проекте">О проекте</a></li> <li class="additional-menu"><a href="/archive.html" title="Архив материалов">Архив</a> </li> <li class="additional-menu"><a href="/info/reklama.html" title="Реклама" class="menu-item"><strong>Реклама</strong></a> <a href="/info/partners.html" title="Партнёры" class="menu-item">Партнёры</a> <a href="/info/legal.html" title="Правовая информация" class="menu-item">Правовая информация</a> <a href="/info/contacts.html" title="Контакты" class="menu-item">Контакты</a> <a href="/feedback.html" title="Обратная связь" class="menu-item">Обратная связь</a></li> <li class="email"><a href="mailto:pcnews@pcnews.ru" title="Пишите нам на pcnews@pcnews.ru"><img src="/media/i/email.gif" alt="e-mail"/></a></li> <li style="visibility: hidden"> <noindex> <!-- Rating@Mail.ru counter --> <script type="text/javascript"> var _tmr = window._tmr || (window._tmr = []); _tmr.push({id: "93125", type: "pageView", start: (new Date()).getTime()}); (function (d, w, id) { if (d.getElementById(id)) return; var ts = d.createElement("script"); ts.type = "text/javascript"; ts.async = true; ts.id = id; ts.src = (d.location.protocol == "https:" ? "https:" : "http:") + "//top-fwz1.mail.ru/js/code.js"; var f = function () { var s = d.getElementsByTagName("script")[0]; s.parentNode.insertBefore(ts, s); }; if (w.opera == "[object Opera]") { d.addEventListener("DOMContentLoaded", f, false); } else { f(); } })(document, window, "topmailru-code"); </script> <noscript> <div style="position:absolute;left:-10000px;"> <img src="//top-fwz1.mail.ru/counter?id=93125;js=na" style="border:0;" height="1" width="1" alt="Рейтинг@Mail.ru"/> </div> </noscript> <!-- //Rating@Mail.ru counter --> </noindex> </li> </ul> </div> <!--[if lte IE 7]> <iframe id="popup-iframe" frameborder="0" scrolling="no"></iframe> <![endif]--> <!--<div id="robot-image"><img class="rbimg" src="i/robot-img.png" alt="" width="182" height="305" /></div>--> <!--[if IE 6]> <script>DD_belatedPNG.fix('#robot-image, .rbimg');</script><![endif]--> </div> <!--[if lte IE 7]> <iframe id="ie-popup-iframe" frameborder="0" scrolling="no"></iframe> <![endif]--> <div id="footer-adlinks"></div> <noindex> <!--LiveInternet counter--><script type="text/javascript"> document.write("<a rel='nofollow' href='//www.liveinternet.ru/click' "+ "target=_blank><img src='//counter.yadro.ru/hit?t45.6;r"+ escape(document.referrer)+((typeof(screen)=="undefined")?"": ";s"+screen.width+"*"+screen.height+"*"+(screen.colorDepth? screen.colorDepth:screen.pixelDepth))+";u"+escape(document.URL)+ ";"+Math.random()+ "' alt='' title='LiveInternet' "+ "border='0' width='1' height='1'><\/a>") </script><!--/LiveInternet--> <!-- Rating@Mail.ru counter --> <script type="text/javascript"> var _tmr = window._tmr || (window._tmr = []); _tmr.push({id: "93125", type: "pageView", start: (new Date()).getTime()}); (function (d, w, id) { if (d.getElementById(id)) return; var ts = d.createElement("script"); ts.type = "text/javascript"; ts.async = true; ts.id = id; ts.src = "https://top-fwz1.mail.ru/js/code.js"; var f = function () {var s = d.getElementsByTagName("script")[0]; s.parentNode.insertBefore(ts, s);}; if (w.opera == "[object Opera]") { d.addEventListener("DOMContentLoaded", f, false); } else { f(); } })(document, window, "topmailru-code"); </script><noscript><div> <img src="https://top-fwz1.mail.ru/counter?id=93125;js=na" style="border:0;position:absolute;left:-9999px;" alt="Top.Mail.Ru" /> </div></noscript> <!-- //Rating@Mail.ru counter --> <!-- Yandex.Metrika counter --> <script type="text/javascript"> (function (d, w, c) { (w[c] = w[c] || []).push(function () { try { w.yaCounter23235610 = new Ya.Metrika({ id: 23235610, clickmap: true, trackLinks: true, accurateTrackBounce: true, webvisor: true, trackHash: true }); } catch (e) { } }); var n = d.getElementsByTagName("script")[0], s = d.createElement("script"), f = function () { n.parentNode.insertBefore(s, n); }; s.type = "text/javascript"; s.async = true; s.src = "https://mc.yandex.ru/metrika/watch.js"; if (w.opera == "[object Opera]") { d.addEventListener("DOMContentLoaded", f, false); } else { f(); } })(document, window, "yandex_metrika_callbacks"); </script> <noscript> <div><img src="https://mc.yandex.ru/watch/23235610" style="position:absolute; left:-9999px;" alt=""/> </div> </noscript> <!-- /Yandex.Metrika counter --> <!-- Default Statcounter code for PCNews.ru http://pcnews.ru--> <script type="text/javascript"> var sc_project=9446204; var sc_invisible=1; var sc_security="14d6509a"; </script> <script type="text/javascript" src="https://www.statcounter.com/counter/counter.js" async></script> <!-- End of Statcounter Code --> <script> (function (i, s, o, g, r, a, m) { i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () { (i[r].q = i[r].q || []).push(arguments) }, i[r].l = 1 * new Date(); a = s.createElement(o), m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m) })(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga'); ga('create', 'UA-46280051-1', 'pcnews.ru'); ga('send', 'pageview'); </script> <script async="async" src="/assets/uptolike.js?pid=49295"></script> </noindex> <!--<div id="AdwolfBanner40x200_842695" ></div>--> <!--AdWolf Asynchronous Code Start --> <script type="text/javascript" src="https://pcnews.ru/js/blockAdblock.js"></script> <script type="text/javascript" src="/assets/jquery.min.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/jquery/jquery.json.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/jquery/jquery.form.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/jquery/jquery.easing.1.2.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/jquery/effects.core.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/js/browser-sniff.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/js/scripts.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/js/pcnews-utils.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/js/pcnews-auth.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/js/pcnews-fiximg.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/js/pcnews-infobox.js"></script> </body> </html>