Hallo allerseits, heute möchte ich über einige der Schwierigkeiten und Missverständnisse sprechen, die viele Bewerber haben. Unser Unternehmen wächst aktiv und ich führe häufig Interviews durch oder nehme daran teil. Infolgedessen habe ich mehrere Probleme identifiziert, die viele Kandidaten in eine schwierige Position gebracht haben. Schauen wir sie uns gemeinsam an. Ich werde Python-spezifische Fragen behandeln, aber insgesamt funktioniert dieser Artikel für jedes Vorstellungsgespräch. Für erfahrene Entwickler werden hier keine Wahrheiten enthüllt, aber für diejenigen, die gerade ihre Reise beginnen, wird es einfacher sein, sich für die Themen der nächsten Tage zu entscheiden.
Der Unterschied zwischen Prozessen und Threads unter Linux
Nun, Sie wissen, eine so typische und im Allgemeinen einfache Frage, nur zum Verständnis, ohne sich mit Details und Feinheiten zu befassen. Natürlich werden die meisten Bewerber Ihnen sagen, dass Threads leichter sind, der Kontext schneller zwischen ihnen wechselt und im Allgemeinen innerhalb des Prozesses leben. Und das alles ist richtig und wunderbar, wenn wir nicht über Linux sprechen. Im Linux-Kernel werden Threads wie normale Prozesse implementiert. Ein Thread ist einfach ein Prozess, der einige Ressourcen mit anderen Prozessen teilt.
Es gibt zwei Systemaufrufe, mit denen Prozesse unter Linux erstellt werden können:
Ich möchte auf Folgendes hinweisen: Wenn Sie einen
fork()
Prozess erstellen, erhalten Sie nicht sofort eine Kopie des Speichers des übergeordneten Prozesses. Ihre Prozesse werden mit einer einzelnen In-Memory-Instanz ausgeführt. Wenn Sie also insgesamt einen Speicherüberlauf hätten haben müssen, funktioniert alles weiter. Der Kernel markiert die Speicherseitendeskriptoren des übergeordneten Prozesses als schreibgeschützt. Wenn versucht wird, in sie zu schreiben (vom untergeordneten oder übergeordneten Prozess), wird eine Ausnahme ausgelöst und behandelt, wodurch eine vollständige Kopie erstellt wird. Dieser Mechanismus wird als Copy-on-Write bezeichnet.
Ich denke, Linux ist ein großartiges Buch über Linux-Geräte. Systemprogrammierung "von Robert Love.
Probleme mit der Ereignisschleife
Asynchrone Dienste und Mitarbeiter in Python oder Go sind in unserem Unternehmen allgegenwärtig. Daher halten wir es für wichtig, ein gemeinsames Verständnis der Asynchronität und der Funktionsweise der Ereignisschleife zu haben. Viele Kandidaten sind bereits ziemlich gut darin, Fragen zu den Vorteilen des asynchronen Ansatzes zu beantworten, und stellen die Ereignisschleife korrekt als eine Art Endlosschleife dar, mit der Sie nachvollziehen können, ob ein bestimmtes Ereignis vom Betriebssystem stammt (z. B. Daten in einen Socket schreiben). Aber der Kleber fehlt: Wie erhält das Programm diese Informationen vom Betriebssystem?
Am einfachsten ist es natürlich, sich daran zu erinnern
Select
... Mit seiner Hilfe wird eine Liste von Dateideskriptoren erstellt, die Sie überwachen möchten. Der Client-Code muss alle übergebenen Handles auf Ereignisse überprüfen (und ihre Anzahl ist auf 1024 begrenzt), was ihn langsam und unpraktisch macht.
Die Antwort über ist
Select
mehr als genug, aber wenn Sie sich an
Poll
oder erinnern
Epoll
und über die Probleme sprechen, die sie lösen, ist dies ein großes Plus für Ihre Antwort. Um keine unnötigen Sorgen zu machen: Wir werden nicht nach C-Code und detaillierten Spezifikationen gefragt, wir sprechen nur über ein grundlegendes Verständnis dessen, was passiert. Lesen Sie mehr über die Unterschiede
Select
,
Poll
und
Epoll
kann in diesem Artikel .
Ich rate Ihnen auch, sich das Thema Asynchronität in Python von David Beasley anzuschauen .
Die GIL schützt, aber nicht Sie
Ein weiteres häufiges Missverständnis ist, dass die GIL entwickelt wurde, um Entwickler vor gleichzeitigen Datenzugriffsproblemen zu schützen. Aber das ist nicht so. Die GIL verhindert natürlich, dass Sie Ihr Programm mit Threads (aber nicht mit Prozessen) parallelisieren. Einfacher ausgedrückt ist die GIL eine Sperre, die vor jedem Aufruf von Python aufgehoben werden muss (nicht so wichtig. Python-Code wird ausgeführt oder Python C-API-Aufrufe). Daher schützt die GIL interne Strukturen vor inkonsistenten Zuständen, aber Sie müssen wie in jeder anderen Sprache Synchronisationsprimitive verwenden.
Sie sagen auch, dass die GIL nur benötigt wird, damit der GC richtig funktioniert. Für sie wird er natürlich gebraucht, aber das ist noch nicht alles.
Unter dem Gesichtspunkt der Ausführung wird selbst die einfachste Funktion in mehrere Schritte unterteilt:
import dis
def sum_2(a, b):
return a + b
dis.dis(sum_2)
4 0 LOAD_FAST 0 (a)
2 LOAD_FAST 1 (b)
4 BINARY_ADD
6 RETURN_VALUE
Aus Sicht des Prozessors ist jede dieser Operationen nicht atomar. Python führt viele Prozessoranweisungen für jede Bytecode-Zeile aus. In diesem Fall sollten Sie nicht zulassen, dass andere Threads den Status des Stapels ändern oder andere Speicheränderungen vornehmen. Dies führt zu einem Segmentierungsfehler oder einem falschen Verhalten. Daher fordert der Interpreter eine globale Sperre für jeden Bytecode-Befehl an. Der Kontext kann jedoch zwischen einzelnen Anweisungen geändert werden, und hier rettet uns die GIL in keiner Weise. Weitere Informationen zum Bytecode und zur Arbeit damit finden Sie in der Dokumentation .
Ein einfaches Beispiel zum Thema GIL-Sicherheit finden Sie hier:
import threading
a = 0
def x():
global a
for i in range(100000):
a += 1
threads = []
for j in range(10):
thread = threading.Thread(target=x)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
assert a == 1000000
Auf meinem Computer stürzt der Fehler stabil ab. Wenn es plötzlich bei Ihnen nicht mehr funktioniert, führen Sie es mehrmals aus oder fügen Sie Threads hinzu. Bei einer kleinen Anzahl von Threads tritt ein schwebendes Problem auf (der Fehler wird angezeigt und nicht). Das heißt, solche Situationen haben neben falschen Daten auch ein Problem in Form ihrer schwebenden Natur. Dies bringt uns auch zum nächsten Problem: Synchronisationsprimitiven.
Und wieder kann ich mich nur auf David Beasley beziehen .
Synchronisationsprimitive
Im Allgemeinen sind Synchronisationsprimitive nicht die beste Frage für Python, aber sie zeigen ein allgemeines Verständnis des Problems und wie tief Sie in diese Richtung gegraben haben. Das Thema Multithreading wird zumindest bei uns als Bonus gestellt und ist nur dann ein Plus (wenn Sie antworten). Aber es ist okay, wenn Sie es noch nicht angetroffen haben. Wir können sagen, dass diese Frage nicht an eine bestimmte Sprache gebunden ist.
Viele unerfahrene Pythonisten hoffen, wie ich oben schrieb, auf die wundersame Kraft der GIL, damit sie sich nicht mit dem Thema Synchronisationsprimitive befassen. Aber vergebens kann es nützlich sein, wenn Hintergrundoperationen und Aufgaben ausgeführt werden. Das Thema Synchronisationsprimitive ist groß und gut verstanden, insbesondere empfehle ich, es in dem Buch "Core Python Applications Programming" von Wesley J. Chun zu lesen.
Und da wir uns bereits ein Beispiel angesehen haben, bei dem die GIL uns bei der Arbeit mit Threads nicht geholfen hat, werden wir das einfachste Beispiel betrachten, wie wir uns vor einem solchen Problem schützen können.
import threading
lock = threading.Lock()
a = 0
def x():
global a
lock.acquire()
try:
for i in range(100000):
a += 1
finally:
lock.release()
threads = []
for j in range(10):
thread = threading.Thread(target=x)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
assert a == 1000000
Wiederholen Sie den Vorgang am ganzen Kopf
Sie können sich nie darauf verlassen, dass die Infrastruktur immer stabil funktioniert. In Interviews werden wir häufig gebeten, einen einfachen Mikrodienst zu entwerfen, der mit anderen interagiert (z. B. über HTTP). Das Problem der Servicestabilität verwirrt die Kandidaten manchmal. Ich möchte auf einige Probleme hinweisen, die Kandidaten übersehen, wenn sie einen erneuten Versuch über HTTP vorschlagen.
Das erste Problem: Der Dienst funktioniert möglicherweise einfach lange nicht mehr. Wiederholte Anfragen in Echtzeit sind bedeutungslos.
Grob durchgeführte Wiederholungsversuche können einen Dienst beenden, der unter Last langsamer wird. Das Mindeste, was er braucht, ist eine Erhöhung der Last, die aufgrund wiederholter Anfragen erheblich zunehmen kann. Wir sind immer daran interessiert, Methoden zum Speichern des Status und zum Implementieren des Versands zu diskutieren, nachdem der Service normal funktioniert.
Alternativ können Sie versuchen, das Protokoll von HTTP auf etwas mit garantierter Zustellung (AMQP usw.) zu ändern.
Das Service-Mesh kann auch die Wiederholungsaufgabe übernehmen. Sie können mehr in diesem Artikel lesen .
Insgesamt gibt es hier, wie gesagt, keine Überraschungen, aber dieser Artikel kann Ihnen helfen, herauszufinden, welche Themen angesprochen werden sollen. Nicht nur für Interviews, sondern auch für ein tieferes Verständnis der Essenz der laufenden Prozesse.