2016-05-09 13 views
6

Ho un'applicazione che sta acquisendo un blocco in un loop in un thread per eseguire alcune attività. C'è anche una seconda discussione che vuole anche acquisire il blocco da di volta in volta. Il problema è che questo secondo thread ottiene a malapena l'opportunità di eseguire il lavoro , in quanto il primo quasi sempre si blocca per primo. Mi auguro che il seguente codice di chiarirà quello che sto cercando di dire:Thread starvation mentre si blocca un loop in Python

import time 
from threading import Lock, Thread 

lock = Lock() 


def loop(): 
    while True: 
     with lock: 
      time.sleep(0.1) 

thread = Thread(target=loop) 
thread.start() 

before = time.time() 
lock.acquire() 
print('Took {}'.format(time.time() - before)) 

Se l'applicazione arriva a print si noterà che aveva bisogno di molto più di solo 0,1 s. Ma a volte capita anche che aspetti solo indefinitamente. Ho testato questo sia in Python 2.7.11 che in Python 3.4.3 su Debian Linux 8 e funziona in modo identico.

Questo comportamento è controintuitivo per me. Dopo tutto, quando il blocco è stato rilasciato in loop, lo lock.acquire era già in attesa della sua versione e dovrebbe acquisire immediatamente il blocco. Ma sembra che il ciclo acquisisca prima il blocco , anche se non era in attesa del suo rilascio nel momento di rilascio .

La soluzione che ho trovato è di dormire tra ogni iterazione del ciclo in stato sbloccato, ma che non mi sembra una soluzione elegante, essa non spiegarmi cosa è accadendo.

Cosa mi manca?

+0

Se si desidera un comportamento deterministico anziché un'esclusione reciproca, allora 'Lock' non è l'oggetto giusto da usare. Potresti essere in grado di forzare un cambio di attività emettendo un' time.sleep (0) 'ma io sono certo che fallirebbe su alcune piattaforme .. – thebjorn

+0

Bene, ho capito che quando ho solo due thread, quando uno aspetta un lock, lo otterrà sempre dopo che l'altro si sblocca. Quello che mi chiedo è perché non sia così. Inoltre, cosa pensi che sarebbe un oggetto appropriato da usare? –

+1

Difficile dire perché stia accadendo, ma se dovessi indovinare sarebbe qualcosa che ha a che fare con i commutatori di attività in modo tale che il ciclo while riesca a richiamare di nuovo aquire prima che il codice relase abbia raggiunto il punto in cui seleziona tra i thread in attesa. Ma questo è solo indovinare. – thebjorn

risposta

4

Sembra che ciò sia dovuto alla pianificazione dei thread del sistema operativo. La mia ipotesi è che entrambi i sistemi operativi danno una priorità molto alta ai thread intensivi della cpu (qualunque cosa significhi) o scegliere un thread successivo per acquisire il blocco (eseguito dal sistema operativo) richiede più tempo rispetto all'acquisizione del blocco dal secondo thread. In ogni caso, non si può dedurre molto senza conoscere gli interni del sistema operativo.

ma non è GIL poiché questo codice:

#include <mutex> 
#include <iostream> 
#include <chrono> 
#include <thread> 

std::mutex mutex; 

void my_thread() { 
    int counter = 100; 
    while (counter--) { 
     std::lock_guard<std::mutex> lg(mutex); 
     std::this_thread::sleep_for(std::chrono::milliseconds(500)); 
     std::cout << "." << std::flush; 
    } 
} 

int main (int argc, char *argv[]) { 
    std::thread t1(my_thread); 
    auto start = std::chrono::system_clock::now(); 
    // added sleep to ensure that the other thread locks lock first 
    std::this_thread::sleep_for(std::chrono::milliseconds(1000)); 
    { 
     std::lock_guard<std::mutex> lg(mutex); 
     auto end = std::chrono::system_clock::now(); 
     auto diff = end - start; 
     std::cout << "Took me " << diff.count() << std::endl; 
    } 
    t1.join(); 
    return 0; 
}; 

, che è solo una versione C++ 11 del codice dà esattamente lo stesso risultato (testato su Ubuntu 16.04).

2

Il multithreading in CPython è piuttosto complicato. Per rendere più semplice l'implementazione (della gestione della memoria tra le altre cose), CPython ha un "Global Interpreter Lock" incorporato. Questo blocco garantisce che solo un thread alla volta può eseguire il bytecode Python.

Un thread rilascia il GIL quando esegue I/O o raggiunge in un'estensione C. E se non lo fa, il GIL verrà prelevato da esso a determinati intervalli. Quindi se un thread è impegnato a girare come il tuo thread, a un certo punto sarà costretto a rinunciare al GIL. E tu vorrebbe aspettarsi che in quel caso un altro thread ha la possibilità di correre. Ma poiché i thread Python sono fondamentalmente thread di sistema operativo, il sistema operativo ha anche voce in capitolo nella pianificazione. E un thread costantemente occupato può avere una priorità più alta e quindi avere più possibilità di essere eseguito.

Per un aspetto più approfondito, guarda il video understanding the Python GIL di David Beazley.

+0

Non è GIL (l'idea mi è sembrata puzzolente sin dall'inizio), vedi la mia risposta. – freakish

+0

@freakish Leggi di nuovo. –

+1

L'ho letto più volte. GIL è completamente estraneo al problema. Parlarne è fonte di confusione e non aiuta affatto. La tua intera risposta dovrebbe essere ridotta a 1 frase: "il sistema operativo ha anche voce in programmazione". – freakish

Problemi correlati