Комментарии 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
Оказалось что я перепутал, pypy это версия с GIL + JIT
Но почему она такая дорогая в 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
Мне нравится, как сделано в расте, никогда не забудешь взять мьютекс или освободить его:
// 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... профит
Пример показательный, но ... цели не очень ясны. Что потоки "не есть гут" - так это известно очень давно... Дело, как представляется, не в них. Было бы понятнее, если бы уточнить постановку проблемы и ее решение. Речь о том, что в данном случае счетчик - это все же общий ресурс. Потоки - пользователи ресурса. Решение - демонстрация приемов работы с общим ресурсом со стороны множества параллельных процессов. И совсем было бы замечательно, если бы эти приемы не были привязаны к языку.
Многопоточность в Python: очевидное и невероятное