mboost-dp1

Python & GIL


Gå til bund
Gravatar #1 - arne_v
31. jul. 2023 14:52
Python steering committee annoncerede i fredags at de regner med at godkende PEP 703.

PEP 703 er et forslag om at slippe af med GIL.

Dog er de ret forsigtige. Planen er:

kort sigt (3.13/3.14): usupporteret build uden GIL

mellem sigt: supporteret men ikke default build uden GIL

langt sigt (5+ år) : default build

https://discuss.python.org/t/a-steering-council-no...

https://peps.python.org/pep-0703/

Gravatar #2 - Claus Jørgensen
31. jul. 2023 18:00
Python udviklerne er generelt ekstremt forsigtige

Python 3 udkom i 2008, men Python 2 blev først EOL i 2020 (5 år efter den oprindelige planlagte end of life)

Så det er ikke overraskende at de skal bruge 5 år på dette
Gravatar #3 - arne_v
31. jul. 2023 19:05
#2

Python 2 -> 3 var en breaking change. Der var masser af python kode som skulle ændres. Det har det med at forsinke opdatering.
Gravatar #4 - larsp
31. jul. 2023 21:27
Der er vel to sider af sagen. Hvad der er korrekt Python kode, og hvad der forventes af C / C++ koden for native kode moduler. Jeg tænker at den største udfordring er at porte alverdens native kode moduler væk fra at bruge GIL?

Kommer der mon til at være forskelle hvad angår acceptabel Python kode? Jeg har altid undgået at tilgå de samme lists og dictionaries, f.eks., fra forskellige tråde samtidigt, uden at have undersøgt om man egentlig godt kunne nu når der er en GIL. Går det mon fra at være acceptabelt til ulovligt?
Gravatar #5 - arne_v
31. jul. 2023 22:44
larsp (4) skrev:

Der er vel to sider af sagen. Hvad der er korrekt Python kode, og hvad der forventes af C / C++ koden for native kode moduler. Jeg tænker at den største udfordring er at porte alverdens native kode moduler væk fra at bruge GIL?


Ændringen er meget relevant for native kode.

Og det ved de godt,

Citat fra PEP:


This PEP poses a number of backwards compatibility issues when building CPython with the --disable-gil flag, but those issues do not occur when using the default build configuration. Nearly all the backwards compatibility concerns involve the C-API:

CPython builds without the GIL will not be ABI compatible with the standard CPython build or with the stable ABI due to changes to the Python object header needed to support biased reference counting. C-API extensions will need to be rebuilt specifically for this version.
C-API extensions that rely on the GIL to protect global state or object state in C code will need additional explicit locking to remain thread-safe when run without the GIL.
C-API extensions that use borrowed references in ways that are not safe without the GIL will need to use the equivalent new APIs that return non-borrowed references. Note that only some uses of borrowed references are a concern; only references to objects that might be freed by other threads pose an issue.


larsp (4) skrev:

Kommer der mon til at være forskelle hvad angår acceptabel Python kode? Jeg har altid undgået at tilgå de samme lists og dictionaries, f.eks., fra forskellige tråde samtidigt, uden at have undersøgt om man egentlig godt kunne nu når der er en GIL. Går det mon fra at være acceptabelt til ulovligt?


Jeg er ikke ekspert i Python threading, men jeg vil formode at uden GIL vil Python have alle de samme potentielle concurrency issues som native, Java, .NET etc..

Gravatar #6 - Claus Jørgensen
1. aug. 2023 02:53
arne_v (3) skrev:
#2

Python 2 -> 3 var en breaking change. Der var masser af python kode som skulle ændres. Det har det med at forsinke opdatering.

Meh

Swift 1 -> 2 -> 3 -> 4 have alle breaking changes (ABI stability var først i Swift 5)

Men scripting sprog som Python og Ruby (og PHP for den sags skyld) har altid elendige håndtering af versioning. Der er åbenbart ikke interassant nok for sprog designerene at tænke ind fra dag 1

Modsat sprog udviklet af virksomheder (Microsoft, Google, Apple, et. al.) hvor det er der fra dag 1.
Gravatar #7 - larsp
1. aug. 2023 07:17
#6 Hvad gjorde de bedre i Swift og de andre sprog "udviklet af virksomheder" for at håndtere breaking changes mellem versioner?
Gravatar #8 - larsp
1. aug. 2023 07:35
#4 #5 For at svare på mit eget spørgsmål, CPython med GIL sikrer atomicitet af operationer på native data strukturer, fra: https://docs.python.org/3/glossary.html#term-globa... :
(Om GIL: ...) This simplifies the CPython implementation by making the object model (including critical built-in types such as dict) implicitly safe against concurrent access (...)

Men det er ofte anbefalet ikke at stole på dette, f.eks. fra Google's Python style guide: https://github.com/google/styleguide/blob/91d6e367...
Do not rely on the atomicity of built-in types.

While Python's built-in data types such as dictionaries appear to have atomic operations, there are corner cases where they aren't atomic (e.g. if __hash__ or __eq__ are implemented as Python methods) and their atomicity should not be relied upon. Neither should you rely on atomic variable assignment (since this in turn depends on dictionaries).

Relevant stack overflow q.: https://stackoverflow.com/questions/6953351/thread...

Med fravær af GIL er det bestemt ikke forsvarligt. Spørgsmålet er så om man ville kunne opdatere en simpel variabel fra flere tråde sikkert, som man ville gøre det i C med "volatile". Jeg vil gætte på nej. (edit: doh, det bliver netop frarådet i citatet fra Googles style guide).
Gravatar #9 - Claus Jørgensen
1. aug. 2023 08:00
larsp (7) skrev:
#6 Hvad gjorde de bedre i Swift og de andre sprog "udviklet af virksomheder" for at håndtere breaking changes mellem versioner?

Automated tooling til opgradering

Og en generel attitude at man selvfølelig opdatere til den seneste udgave med det samme hos folk der arbejder med sprogene
Gravatar #10 - Claus Jørgensen
1. aug. 2023 08:03
#8

volatile gør ikke en variable thread safe. Bare fordi compileren ikke cache din variable, gør det ikke den threadsafe.

Gravatar #11 - larsp
1. aug. 2023 09:02
Claus Jørgensen (10) skrev:
#8

volatile gør ikke en variable thread safe. Bare fordi compileren ikke cache din variable, gør det ikke den threadsafe.


Det er rigtigt, hvis det er en ikke native size int f.eks.
Gravatar #12 - larsp
1. aug. 2023 09:12
Claus Jørgensen (9) skrev:
Automated tooling til opgradering

Det findes, https://docs.python.org/3/library/2to3.html Det er bare ikke så nemt, ofte findes der ikke direkte ækvivalente moduler i Python 3.

Det er vel forskellen på hvad man kan med en streng virksomhedsstyret kultur inden for et sprog, med udviklere der er "på job" og kan bruge tid på ting som at opgradere til nyere versioner hele tiden.

... og så et stort spravlende open source community med tonsvis af moduler i mere eller mindre vedligeholdt tilstand, fordi forfatterne nok har travlt med andre ting. Det er ikke så underligt, at det er svært at indføre breaking changes i sådan et miljø. Linux kernel udviklerne har som bekendt et yderst vigtigt aksiom, "don't break userland" af samme årsager.
Gravatar #14 - arne_v
1. aug. 2023 13:09
#concurrency

Der er 2 potentielle problemer med concurrency og memory.

Atomicity.

int32_t v = 0x00010001;

thread 1:

v = 0x00020002;

thread 2:

noget = v;

Thread 2 bør få enten 0x00010001 eller 0x00020002, men hvis 32 bit opdateringer ikke er atomiske og der ikke tages specielle tiltag så kan thread 2 få 0x00010002 eller 0x00020001.

Visibility.

main:

bool stop = false;
start_thread(t)
...
stop = true;

t:

while(!stop) {
...
}

uden specielle tiltag er der ingen garanti for at den tråd stopper. Hvis main bare opdaterer stop flaget i sin CPU core cache eller register mens t bare læser fra sin CPU core cache eller register så ser t aldrig true.
Gravatar #15 - arne_v
1. aug. 2023 13:15
#volatile

Men ja i traditionel C er løsningen volatile.


An object that has volatile-qualified type may be modified in ways unknown to the
implementation or have other unknown side effects. Therefore any expression referring
to such an object shall be evaluated strictly according to the rules of the abstract machine,
as described in 5.1.2.3. Furthermore, at every sequence point the value last stored in the
object shall agree with that prescribed by the abstract machine, except as modified by the
unknown factors mentioned previously. 134) What constitutes an access to an object that
has volatile-qualified type is implementation-defined.
Gravatar #16 - arne_v
2. aug. 2023 16:19
#15

Typisk vil man dog nok bruge noget platform specifikt. Eksempler:

Win32 API - EnterCriticalSection og LeaveCriticalSection

POSIX threads - pthread_mutex_lock og pthread_mutex_unlock
Gravatar #17 - larsp
3. aug. 2023 06:50
#16 En CriticalSection er (i microcontroller verdenen) en billig måde at opnå atomicitet. Man slår simpelthen interrupts fra og til rundt om operationen. Jeg ved ikke præcis hvad en critical section gør i f.eks. Windows. Holder den f.eks. andre tråde i en multikerne CPU væk? Det kan vel kun gøres ved at stoppe alle relevante tråde i denne section. Ikke så billigt længere!

En Mutex er et noget mere kompliceret dyr, der kan have fancy features som priority inheritance (på et RTOS).

#volatile

Jeg researchede emnet lidt nærmere. Claus har ret i at man, i dag, aldrig bør stole på at en variabel er threadsafe i C/C++. Volatile ændrer ikke på dette. Man bør bruge enten specielle atomic angivelser for den givne compiler, eller C11 "_Atomic" eller C++11 "std::atomic".

Det er ganske relevant at have atomiske variable i high performance kode eller systemprogrammering. Hvis man kan lave en data struktur lock-free er det ofte en fordel. Et typisk eksempel er en ringbuffer som nemt kan laves lock-free med atomiske og volatile head og tail indeks variable.

Ved mere simple CPUer plejede det at være "godt nok" at bruge en volatile int. Men den går ikke længere. FreeRTOS har egg-on-the-face i denne sammenhæng! https://www.freertos.org/FreeRTOS_Support_Forum_Ar...
Unless I am mistaken, the only reason FreeRTOS doesn’t fall apart during untimely context switches is because most platforms which run it are single-core microprocessors where a write to an address is not buffered or cached in some way. In these cases, volatile has been “good enough” to stop breaking optimizations. However, this is a very poor safety net.


Der er også andre eksempler, f.eks. gode gamle Atmel AVR, som er udbredt i Arduinoer. Denne processor er 8-bit og har 8-bit registre, men de valgte at gøre "int" størrelsen 16-bit i AVR-GCC, så en "volatile int" er absolut ikke atomisk her. En "volatile int8_t" er tilgengæld sikker.
Gravatar #18 - arne_v
3. aug. 2023 15:17
larsp (17) skrev:

#volatile

Jeg researchede emnet lidt nærmere. Claus har ret i at man, i dag, aldrig bør stole på at en variabel er threadsafe i C/C++. Volatile ændrer ikke på dette. Man bør bruge enten specielle atomic angivelser for den givne compiler, eller C11 "_Atomic" eller C++11 "std::atomic".

Det er ganske relevant at have atomiske variable i high performance kode eller systemprogrammering. Hvis man kan lave en data struktur lock-free er det ofte en fordel. Et typisk eksempel er en ringbuffer som nemt kan laves lock-free med atomiske og volatile head og tail indeks variable.

Ved mere simple CPUer plejede det at være "godt nok" at bruge en volatile int. Men den går ikke længere. FreeRTOS har egg-on-the-face i denne sammenhæng! https://www.freertos.org/FreeRTOS_Support_Forum_Ar...
Unless I am mistaken, the only reason FreeRTOS doesn’t fall apart during untimely context switches is because most platforms which run it are single-core microprocessors where a write to an address is not buffered or cached in some way. In these cases, volatile has been “good enough” to stop breaking optimizations. However, this is a very poor safety net.


Der er også andre eksempler, f.eks. gode gamle Atmel AVR, som er udbredt i Arduinoer. Denne processor er 8-bit og har 8-bit registre, men de valgte at gøre "int" størrelsen 16-bit i AVR-GCC, så en "volatile int" er absolut ikke atomisk her. En "volatile int8_t" er tilgengæld sikker.


Volatile garanterer ikke thread safe kode - ingen konstruktioner garanterer thread safe kode uanset brug!

Volatile garanterer heller ikke atomisk write/read.

Der er ingen tvivl om at:

volatile large_non_atomic_int_t v = 0;

v = -1; ---- temp = v;

ikke garanterer at temp bliver 0 eller -1.

Hvis v er 32 bit og mindste atomic move er 16 bit så giver:

movw #-1,v ------ mb
movw #-1,v+2 ---- movw v,temp
mb -------------- movw v+2,temp+2

ingen garanti for temp.

Spørgsmålet er om:

volatile small_atomic_int_t flag = 0;
volatile large_non_atomic_int_t v = 0;

v = -1;
flag = 1; ---- while(!flag);
-------------- temp = v;

giver garanti for temp.

Argumentet for er:
- v skal skrives til RAM senest ved semikolon
- flag skal skrives til RAM senest ved semikolon altså efter v
- test for flag skal læse fra RAM ved hvert gennemløb og går først videre når sat i RAM
- v skal læses fra RAM tidligst lige efter foregående semikolon efter at flag er sat i RAM

Argumentet imod må være at selvom C genererer instruktioner i en bestemt rækkefølge kan CPU eventuelt udføre dem i en anden rækkefølge.

Som altid er C standarden lidt tynd og angiver ikke den slags nærmere.

Min påstand som ikke C ekspert er:
- argumentet imod er en uheldig fralægning af ansvar fra C compileren
- imod fænomenet kan formentligt kun ske ved meget primitive CPU uden data cache, da en avanceret CPU med data cache vil indsætte en MB instruktion for at flushe L1/L2/L3 cache og medmindre CPU design er sindsygt vil der ikke ske instruktion reordring på tværs af sådan en MB.

Men der er gode grunde til at man i nyere versioner af standarden har indført nye features med mere let-tilgængelig måde at være sikker på atomic update.
Gravatar #19 - arne_v
3. aug. 2023 15:32
larsp (17) skrev:

#16 En CriticalSection er (i microcontroller verdenen) en billig måde at opnå atomicitet. Man slår simpelthen interrupts fra og til rundt om operationen. Jeg ved ikke præcis hvad en critical section gør i f.eks. Windows. Holder den f.eks. andre tråde i en multikerne CPU væk? Det kan vel kun gøres ved at stoppe alle relevante tråde i denne section. Ikke så billigt længere!

En Mutex er et noget mere kompliceret dyr, der kan have fancy features som priority inheritance (på et RTOS).


En Win32 critical section eller POSIX mutex virker lidt ligesom Java synchronized og C# lock.

Kun en tråd af gangen kan eje den critical section / mutex.

Og det bruges til at beskytte en shared resource.

Andre tråde der ikke skal bruge den resource kører full speed.

Tråde der skal bruge den resource er nødt til at vente indtil de kan få den.

Dyrt?

Der er overhead ved at have tråde der skal vente på en anden tråd.

Men overhead ved at bruge en critical section / mutex til at få dem til at vente er minimalt.

Så det dyre er i designet ikke i synkroniserings mekanismen.

Og den slags synkroniserings mekanismer har normalt udover en "block until" metode også en "try and if not available don't block" metode hvor en tråd kan checke og lave noget andet mens den venter på resource.

Og på moderne OS er det ikke et problem med en masse ventende tråde. Som tommelfingeregel regner jeg med at de kan klare 250-500 tråde per core i fin stil. Det koster selvfølgelig lidt memory da hver tråd har sin egen stak, men RAM er billigt.




Gravatar #20 - larsp
4. aug. 2023 10:42
arne_v (19) skrev:
En Win32 critical section eller POSIX mutex virker lidt ligesom Java synchronized og C# lock.

Kun en tråd af gangen kan eje den critical section / mutex.

Og det bruges til at beskytte en shared resource.

Andre tråde der ikke skal bruge den resource kører full speed.

Tråde der skal bruge den resource er nødt til at vente indtil de kan få den.

Dyrt?

Der er overhead ved at have tråde der skal vente på en anden tråd.

Men overhead ved at bruge en critical section / mutex til at få dem til at vente er minimalt.

Så det dyre er i designet ikke i synkroniserings mekanismen.

Jeg tjente mine lærepenge i multithreading ved at bruge FreeRTOS på simple CPUer, og her er der en stor forskel på en mutex og en critical section.

- En critical section er skåret ind til benet kun én instruktion ved indgang: SEI og én instruktion ved udgang: CLI. Dette slår interrupts fra og til og sikrer mod afbrydelser. (I praktisk bliver processorens tilstands register pushet, ændret og poppet, så det ender med lidt flere instruktioner)

- En mutex er en mere kompliceret struktur med en kø i maven og fancy features som priority escalation. Hvis en høj priority task venter på en mutex som en low priority task har, får den lave prioritets task midlertidigt samme højere prioritet så højprioritet-tasken ikke bliver defacto udsat for lav prioritet i perioden.

Ved moderne multicore CPUer og moderne OSer er SEI/CLI conceptet nok en saga blot, så, javel, en critical section er bare en mutex / lock. Gad vide om disse mutexer er ligeså avancerede som dem man finder i et RTOS, jeg tvivler.

Men intensivt brug af locks til at dele ressourcer er og bliver en skrammel måde at lave multithreading. Message køer skal der til ;)
Gravatar #21 - arne_v
4. aug. 2023 13:12
Jeg er ikke ekspert i POSIX threads mutex, men det ser ret simpelt ud:

pthread_mutex_t mtx;
...
pthread_mutex_init(&mtx, NULL);
...
pthread_mutex_lock(&mtx);
// only one thread executing this at a time
pthread_mutex_unlock(&mtx);
...
pthread_mutex_destroy(&mtx);

meget ligesom Java:

Object lck = new Object();
...
synchronized(lck) {
// only one thread executing this at a time
}


Gravatar #22 - arne_v
4. aug. 2023 19:39
Og for en god ordens skyld - Python:

lck = threading.RLock()
...
lck.acquire(1)
# only one thread executing this at a time
lck.release()
Gravatar #23 - larsp
5. aug. 2023 08:58
#22 RLock er en rekursiv lock der kan tages flere gange af samme tråd.

Mere pythonisk ville være at bruge "with":

lock = threading.Lock()
with lock:
. pass # only one thread executing this at a time


Gravatar #24 - larsp
5. aug. 2023 09:06
#posix. Jeg lavede min egen message queue i C til Linux btw. da jeg ikke kunne finde noget i posix og standard libraries til interne køer i et program. Jeg endte med at bruge to semaforer og en mutex til én kø for at få korrekt opførsel:

"more_room" counting semafor med antal frie pladser.
"more_data" counting semafor med antal elementer i kø.
"data_lock" mutex der beskytter selve køens data og pointere.

Når man tager fra køen ventes der først på more_data semaforen. Elementet tages fra køen, beskyttet af data_lock, og der gives til more_room.

Når man lægger i kø ventes der på more_room semaforen. Elementet lægges i kø, beskyttet af data_lock, og der gives til more_data.
Gravatar #25 - arne_v
5. aug. 2023 23:23
#24

I gamle dage lavede man det i Java med synchronized, wait og notifyAll (i C# er det lock, Wait og PulseAll) men nu om dage (siden Java 5 i 2004) er der java.util.concurrent.BlockingQueue / java.util.concurrent.ArrayBlockingQueue.

Det kan også laves i C men kræver nok lidt mere.

Hvis jeg skulle lave det i C, så ville jeg nok vælge ZeroMQ med push pull pattern.
Gravatar #26 - arne_v
5. aug. 2023 23:25
#24

I gamle dage lavede man det i Java med synchronized, wait og notifyAll (i C# er det lock, Wait og PulseAll) men nu om dage (siden Java 5 i 2004) er der java.util.concurrent.BlockingQueue med implementation java.util.concurrent.ArrayBlockingQueue.

Det kan også laves i C men kræver nok lidt mere.

Hvis jeg skulle lave det i C, så ville jeg nok vælge ZeroMQ med push pull pattern.
Gravatar #27 - arne_v
5. aug. 2023 23:28
(undskyld dobbeltpost)

#24

Iøvrigt sjovt som ord kan have forskellig betydning i forskellige kontekster.

Som f.eks. "message queue".

Det var ret klart hvad du mente.

Men i visse kredse er message queue en separat server ActiveMQ-ArtemisMQ, RabbitMQ, IBM MQ, MSMQ etc. med netværks protokoller, optional persistence af messages, support for transactioner og normalt også support for XA transaktioner.


Gravatar #28 - Claus Jørgensen
6. aug. 2023 14:49
Eller man kan bygge det ind i selve sproget og sikre compile-time safe concurrency, som f.eks. Swift har gjort med Actors

https://docs.swift.org/swift-book/documentation/th...

Det er faktisk utrolig at så få programmeringssprog har forsøgt at forbedre concurrency support i selve sproget / kompileren, når det jo har været klart at det er absolut nødvendigt de sidste 20 år.

Og nej, jeg mener ikke at locks er en god løsning.
Gravatar #29 - arne_v
7. aug. 2023 00:47
#28

Jeg vil tro at de fleste er enige om at Actor modellen er relevant for mange problemstillinger.

Uenigheden starter nok omkring hvorvidt det skal være en feature i sproget eller en feature i standard biblioteket.

Men uanset hvad er det vel en lidt anden problematik end letvægts message queue. Actor modellen er en programmerings model mens en letvægts queue er en kommunikations metode. En actor model kan bygges ovenpå en letvægts message queue.

Gravatar #30 - arne_v
23. aug. 2023 01:08
#28

Nu har jeg kigget lidt på den Swift actor.

Og jeg forstår den ikke helt.

Det virker på mig som en 3 lags model:

et transparent kalde API
en traditionel actor model
en letvægts message queue

Ved en traditionel actor model forstår jeg en logik som:


class SomeActor {
receive loop {
msg = receive_message()
switch(msg) {
... => ...
... => ...
... => ...
}
}
}


Jeg kender kun et tilfælde af et tilsvarende transparent kalde API og det er Python Pykka proxy funktionaliteten.

Og jeg synes ikke at det giver meget mening.

Actor paradigmet er "Don't ask tell" og et transparent kalde API er vel at gøre alt til ask!?!?

Og jeg synes at der var simplere måder at løse concurrency problemet på.

Lad os prøve at tage den i C#.

Udgangspunktet er den her kode:


public class TemperatureLogger
{
private List<int> measurements;
private int max;
public TemperatureLogger(int measurement)
{
measurements = new List<int>();
measurements.Add(measurement);
max = measurement;
}
public int GetMax()
{
return max;
}
public void Update(int measurement)
{
measurements.Add(measurement);
if(measurement > max)
{
max = measurement;
}
}
}


Den kode vil hvis den bliver kaldt i en multi-threaded sammenhæng give både forkerte resultater og exceptions.

Det kan løses med at skifte til en actor model.

Med Akka.NET ser det ud som:


public class TemperatureLogger : ReceiveActor
{
public enum Command
{
GetMax,
Stop
}
public class Update
{
public int Measurement { get; set; }
}
private List<int> measurements;
private int max;
public TemperatureLogger(int measurement)
{
measurements = new List<int>();
measurements.Add(measurement);
max = measurement;
Receive<Command>((Command cmd) => {
switch (cmd)
{
case Command.GetMax:
Sender.Tell(max);
break;
case Command.Stop:
Self.GracefulStop(TimeSpan.FromMilliseconds(100));
break;
}

});
Receive<Update>((Update updmsg) => {
measurements.Add(updmsg.Measurement);
if (updmsg.Measurement > max)
{
max = updmsg.Measurement;
}
});

}
}


Men koden er lang, vanskelig læselig, dårligt performende og kræver at kalder selv er en actor.

Og det er en anden simplere løsning:


public class TemperatureLogger
{
private List<int> measurements;
private int max;
public TemperatureLogger(int measurement)
{
measurements = new List<int>();
measurements.Add(measurement);
max = measurement;
}
public int GetMax()
{
lock(this)
{
return max;
}
}
public void Update(int measurement)
{
lock(this)
{
measurements.Add(measurement);
if(measurement > max)
{
max = measurement;
}
}
}
}

Gravatar #31 - arne_v
6. sep. 2023 17:40
Men jeg endte naturligvis med at skrive lidt om Actor.

:-)

https://www.vajhoej.dk/arne/articles/actor.html
Gå til top

Opret dig som bruger i dag

Det er gratis, og du binder dig ikke til noget.

Når du er oprettet som bruger, får du adgang til en lang række af sidens andre muligheder, såsom at udforme siden efter eget ønske og deltage i diskussionerne.

Opret Bruger Login