Как стать автором
Обновить

Комментарии 16

К сожалению вариант с Lock на питоне до 3.10 занимает на порядок больше времени:

Python 3.7.9
Counter: expected val: 1000000, actual val: 748022 @ 0.198 sec
CounterWithConversion: expected val: 1000000, actual val: 184262 @ 0.293 sec
ThreadSafeCounter: expected val: 1000000, actual val: 1000000 @ 4.17 sec

Python 3.8.17
Counter: expected val: 1000000, actual val: 654067 @ 0.226 sec
CounterWithConversion: expected val: 1000000, actual val: 288048 @ 0.284 sec
ThreadSafeCounter: expected val: 1000000, actual val: 1000000 @ 4.54 sec

Python 3.9.17
Counter: expected val: 1000000, actual val: 606116 @ 0.179 sec
CounterWithConversion: expected val: 1000000, actual val: 592690 @ 0.23 sec
ThreadSafeCounter: expected val: 1000000, actual val: 1000000 @ 5.51 sec

Python 3.10.12
Counter: expected val: 1000000, actual val: 1000000 @ 0.177 sec
CounterWithConversion: expected val: 1000000, actual val: 520744 @ 0.231 sec
ThreadSafeCounter: expected val: 1000000, actual val: 1000000 @ 0.407 sec

Python 3.11.4
Counter: expected val: 1000000, actual val: 1000000 @ 0.0982 sec
CounterWithConversion: expected val: 1000000, actual val: 567276 @ 0.158 sec
ThreadSafeCounter: expected val: 1000000, actual val: 1000000 @ 0.312 sec

Такова цена, синхронизация не бесплатная)

вот еще решение:

❯ pypy3.9 main.py
Counter: expected val: 1000000, actual val: 1000000 @ 0.00605 sec
CounterWithConversion: expected val: 1000000, actual val: 1000000 @ 0.014 sec

❯ pypy3.10 main.py
Counter: expected val: 1000000, actual val: 1000000 @ 0.00568 sec
CounterWithConversion: expected val: 1000000, actual val: 1000000 @ 0.0146 sec

Версии без GIL само собой будут пошустрее, но я без понятия как они на проде себя показывают.

Спасибо за ценные комментарии?

Ну я имел ввиду не то, что pypy шустрее, а то, что примеры без lock отработали без ошибок.

Запустил несколько раз, через раз срабатывает на 3.10 а на 3.9 видимо проблемы нет)

Python 3.9.17 (3f3f2298ddc56db44bbdb4551ce992d8e9401646, Jun 15 2023, 11:14:28)
[PyPy 7.3.12 with GCC Apple LLVM 13.1.6 (clang-1316.0.21.2.5)]
Counter: expected val: 1000000, actual val: 1000000
CounterWithConversion: expected val: 1000000, actual val: 900000
ThreadSafeCounter: expected val: 1000000, actual val: 1000000


Python 3.10.12 (af44d0b8114cb82c40a07bb9ee9c1ca8a1b3688c, Jun 15 2023, 12:46:58)
[PyPy 7.3.12 with GCC Apple LLVM 13.1.6 (clang-1316.0.21.2.5)]
Counter: expected val: 1000000, actual val: 965024
CounterWithConversion: expected val: 1000000, actual val: 1000000
ThreadSafeCounter: expected val: 1000000, actual val: 1000000

Python 3.9.17 (3f3f2298ddc56db44bbdb4551ce992d8e9401646, Jun 15 2023, 11:14:28)
[PyPy 7.3.12 with GCC Apple LLVM 13.1.6 (clang-1316.0.21.2.5)]
Counter: expected val: 1000000, actual val: 1000000
CounterWithConversion: expected val: 1000000, actual val: 1000000
ThreadSafeCounter: expected val: 1000000, actual val: 1000000


Python 3.10.12 (af44d0b8114cb82c40a07bb9ee9c1ca8a1b3688c, Jun 15 2023, 12:46:58)
[PyPy 7.3.12 with GCC Apple LLVM 13.1.6 (clang-1316.0.21.2.5)]
Counter: expected val: 1000000, actual val: 1000000
CounterWithConversion: expected val: 1000000, actual val: 1000000
ThreadSafeCounter: expected val: 1000000, actual val: 1000000

Придумал кейс который ломает pypy, видимо JIT бессилен перед таким способом прибавить единицу)

class CounterForPypy:
  def __init__(self):
    self.val = 0

  def change(self):
    self.val += random.randint(1, 1)
Python 3.9.17 (3f3f2298ddc56db44bbdb4551ce992d8e9401646, Jun 15 2023, 11:14:28)
[PyPy 7.3.12 with GCC Apple LLVM 13.1.6 (clang-1316.0.21.2.5)]
CounterForPypy: expected val: 1000000, actual val: 645618


Python 3.10.12 (af44d0b8114cb82c40a07bb9ee9c1ca8a1b3688c, Jun 15 2023, 12:46:58)
[PyPy 7.3.12 with GCC Apple LLVM 13.1.6 (clang-1316.0.21.2.5)]
CounterForPypy: expected val: 1000000, actual val: 447503

Но почему она такая дорогая в Python < 3.10? И почему так улучшились дела в 3.10+?

Мне кажется проблема все-таки не в lock:

class ThreadSafeCounter:
  def change(self):
    with self.lock:
      self.val += 1

class ThreadSafeCounterWithRandom:
  def change(self):
    with self.lock:
      self.val += random.randint(1, 1)
❯ ./script.sh
Python 3.7.9
ThreadSafeCounter: expected val: 1000000, actual val: 1000000 @ 3.3 sec
ThreadSafeCounterWithRandom: expected val: 1000000, actual val: 1000000 @ 1.69 sec

Python 3.8.17
ThreadSafeCounter: expected val: 1000000, actual val: 1000000 @ 4.89 sec
ThreadSafeCounterWithRandom: expected val: 1000000, actual val: 1000000 @ 1.55 sec

Python 3.9.17
ThreadSafeCounter: expected val: 1000000, actual val: 1000000 @ 3.33 sec
ThreadSafeCounterWithRandom: expected val: 1000000, actual val: 1000000 @ 1.18 sec

Python 3.10.12
ThreadSafeCounter: expected val: 1000000, actual val: 1000000 @ 0.375 sec
ThreadSafeCounterWithRandom: expected val: 1000000, actual val: 1000000 @ 1.24 sec

Python 3.11.4
ThreadSafeCounter: expected val: 1000000, actual val: 1000000 @ 0.296 sec
ThreadSafeCounterWithRandom: expected val: 1000000, actual val: 1000000 @ 0.767 sec

И вот тут мы плавно подходим к тому что из-за GIL писать на питоне многопоточный код совершенно бессмысленно, т.к. казалось бы независимые вычисления тормозят друг друга.

Мне нравится, как сделано в расте, никогда не забудешь взять мьютекс или освободить его:

// Here we're using an Arc to share memory among threads, and the data inside
// the Arc is protected with a mutex.
let data = Arc::new(Mutex::new(0_u32));

// …
{
    let mut data = data.lock().unwrap();
    *data += 1;
    // the lock is unlocked here when `data` goes out of scope.
}

И зачем для счётчика целый класс? Просто переменную использовать не судьба. a=0,a=a+1... профит

А мьютекс где хранить?

Можно в генератор всё упаковать.

Пример показательный, но ... цели не очень ясны. Что потоки "не есть гут" - так это известно очень давно... Дело, как представляется, не в них. Было бы понятнее, если бы уточнить постановку проблемы и ее решение. Речь о том, что в данном случае счетчик - это все же общий ресурс. Потоки - пользователи ресурса. Решение - демонстрация приемов работы с общим ресурсом со стороны множества параллельных процессов. И совсем было бы замечательно, если бы эти приемы не были привязаны к языку.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории