Was sind die Vorteile eines nicht präemptiven Betriebssystems für eine Bared-Metal-MCU im Vergleich zum hausgemachten Code mit Hintergrundschleife plus Timer-Interrupt-Architektur? Welche dieser Vorteile sind attraktiv genug für ein Projekt, um ein nicht-präemptives Betriebssystem einzuführen, anstatt hausgemachten Code mit Hintergrundschleifenarchitektur zu verwenden?
.
Ich schätze wirklich alle, die auf meine Frage geantwortet haben. Ich habe das Gefühl, die Antwort war fast da. Ich füge diese Erklärung meiner Frage hier hinzu, die meine eigene Überlegung zeigt und helfen kann, die Frage einzugrenzen oder zu präzisieren.
Was ich versuche, ist zu verstehen, wie man das am besten geeignete RTOS für ein Projekt im Allgemeinen auswählt.
Um dies zu erreichen, hilft ein besseres Verständnis der grundlegenden Konzepte und der attraktivsten Vorteile verschiedener Arten von RTOS und des entsprechenden Preises, da es nicht das beste RTOS für alle Anwendungen gibt.
Ich habe vor ein paar Jahren Bücher über OS gelesen, aber ich habe sie nicht mehr bei mir. Ich habe im Internet gesucht, bevor ich meine Frage hier gepostet habe, und fand, dass diese Informationen am hilfreichsten waren: http://www.ustudy.in/node/5456 .
Es gibt viele andere hilfreiche Informationen wie die Einführungen auf der Website verschiedener RTOS, Artikel, die präemptives Scheduling und nicht-präemptives Scheduling vergleichen, und so weiter.
Aber ich habe kein Thema gefunden, das erwähnt wurde, wann man ein nicht präemptives RTOS wählt und wann es besser ist, einfach seinen eigenen Code mit Timer-Interrupt und Hintergrundschleife zu schreiben.
Ich habe gewisse eigene Antworten, aber ich bin nicht zufrieden genug mit ihnen.
Ich würde wirklich gerne die Antwort oder Ansicht von erfahreneren Leuten wissen, insbesondere aus der Industriepraxis.
Mein bisheriges Verständnis ist:
Unabhängig davon, ob Sie ein Betriebssystem verwenden oder nicht, sind bestimmte Arten von Planungscodes immer erforderlich, auch in Form von Code wie:
in the timer interrupt which occurs every 10ms
if(it's 10ms)
{
call function A / execute task A;
}
if(it's 50ms)
{
call function B / execute task B;
}
Vorteil 1:
Ein nicht präemptives Betriebssystem legt den Weg / Programmierstil für den Planungscode fest, sodass Ingenieure dieselbe Ansicht teilen können, auch wenn sie zuvor nicht im selben Projekt waren. Dann können Ingenieure mit der gleichen Ansicht über Konzeptaufgaben an verschiedenen Aufgaben arbeiten und sie testen, sie so weit wie möglich unabhängig voneinander profilieren.
Aber wie viel können wir wirklich davon profitieren? Wenn Ingenieure am selben Projekt arbeiten, können sie die gleiche Ansicht gut teilen, ohne ein nicht präemptives Betriebssystem zu verwenden.
Wenn ein Ingenieur aus einem anderen Projekt oder Unternehmen stammt, profitiert er davon, wenn er das Betriebssystem vorher kannte. Aber wenn nicht, dann scheint es für ihn keinen großen Unterschied zu machen, ein neues Betriebssystem oder einen neuen Code zu lernen.
Vorteil 2:
Wenn der OS-Code gut getestet wurde, spart das Zeit beim Debuggen. Das ist wirklich ein guter Vorteil.
Aber wenn die Anwendung nur etwa 5 Aufgaben hat, denke ich, dass es nicht wirklich chaotisch ist, Ihren eigenen Code mit Timer-Interrupt und Hintergrundschleife zu schreiben.
Ein nicht präemptives Betriebssystem wird hier auf ein kommerzielles / kostenloses / Legacy-Betriebssystem mit einem nicht präemptiven Scheduler bezogen.
Wenn ich diese Frage poste, denke ich hauptsächlich an bestimmte Betriebssysteme wie:
(1) KISS Kernel (ein kleines nichtpräemptives RTOS - behauptet von seiner Website)
http://www.frontiernet.net/~rhode/kisskern.html
(2) uSmartX (leichtes RTOS – wird von seiner Website behauptet)
(3) FreeRTOS (es ist ein präemptives RTOS, aber soweit ich weiß, kann es auch als nicht präemptives RTOS konfiguriert werden)
(4) uC/OS (ähnlich wie FreeRTOS)
(5 ) Legacy OS / Scheduler-Code in einigen Unternehmen (normalerweise vom Unternehmen intern erstellt und gepflegt)
(Kann keine weiteren Links hinzufügen, da die Einschränkung durch das neue StackOverflow-Konto erfolgt)
Soweit ich weiß, ist ein nicht präemptives Betriebssystem eine Sammlung dieser Codes:
(1) ein Scheduler, der eine nicht präemptive Strategie verwendet.
(2) Einrichtungen für Kommunikation zwischen Tasks, Mutex, Synchronisation und Zeitsteuerung.
(3) Speicherverwaltung.
(4) andere hilfreiche Einrichtungen / Bibliotheken wie Dateisystem, Netzwerkstapel, GUI usw. (FreeRTOS und uC/OS bieten diese, aber ich bin mir nicht sicher, ob sie noch funktionieren, wenn der Scheduler als nicht präemptiv konfiguriert ist)
Einige davon sie sind nicht immer da. Aber der Planer ist ein Muss.
Das riecht etwas off-topic, aber ich werde versuchen, es wieder auf den richtigen Weg zu bringen.
Preemptives Multitasking bedeutet, dass das Betriebssystem oder der Kernel den aktuell ausgeführten Thread aussetzen und auf der Grundlage der vorhandenen Scheduling-Heuristik zu einem anderen wechseln kann. Meistens haben die laufenden Threads keine Ahnung, dass auf dem System andere Dinge vor sich gehen, und das bedeutet für Ihren Code, dass Sie darauf achten müssen, ihn so zu gestalten, dass, wenn der Kernel entscheidet, einen Thread mitten in a Mehrschrittbetrieb (z. B. Ändern eines PWM-Ausgangs, Auswählen eines neuen ADC-Kanals, Lesen des Status von einem I2C-Peripheriegerät usw.) und einen anderen Thread für eine Weile laufen lassen, damit sich diese beiden Threads nicht gegenseitig stören.
Ein beliebiges Beispiel: Nehmen wir an, Sie sind neu bei eingebetteten Multithread-Systemen und haben ein kleines System mit einem I2C-ADC, einem SPI-LCD und einem I2C-EEPROM. Sie haben entschieden, dass es eine gute Idee wäre, zwei Threads zu haben: einen, der vom ADC liest und Samples in das EEPROM schreibt, und einen, der die letzten 10 Samples liest, sie mittelt und auf dem SPI-LCD anzeigt. Das unerfahrene Design würde in etwa so aussehen (grob vereinfacht):
char i2c_read(int i2c_address, char databyte)
{
turn_on_i2c_peripheral();
wait_for_clock_to_stabilize();
i2c_generate_start();
i2c_set_data(i2c_address | I2C_READ);
i2c_go();
wait_for_ack();
i2c_set_data(databyte);
i2c_go();
wait_for_ack();
i2c_generate_start();
i2c_get_byte();
i2c_generate_nak();
i2c_stop();
turn_off_i2c_peripheral();
}
char i2c_write(int i2c_address, char databyte)
{
turn_on_i2c_peripheral();
wait_for_clock_to_stabilize();
i2c_generate_start();
i2c_set_data(i2c_address | I2C_WRITE);
i2c_go();
wait_for_ack();
i2c_set_data(databyte);
i2c_go();
wait_for_ack();
i2c_generate_start();
i2c_get_byte();
i2c_generate_nak();
i2c_stop();
turn_off_i2c_peripheral();
}
adc_thread()
{
int value, sample_number;
sample_number = 0;
while (1) {
value = i2c_read(ADC_ADDR);
i2c_write(EE_ADDR, EE_ADDR_REG, sample_number);
i2c_write(EE_ADDR, EE_DATA_REG, value);
if (sample_number < 10) {
++sample_number;
} else {
sample_number = 0;
}
};
}
lcd_thread()
{
int i, avg, sample, hundreds, tens, ones;
while (1) {
avg = 0;
for (i=0; i<10; i++) {
i2c_write(EE_ADDR, EE_ADDR_REG, i);
sample = i2c_read(EE_ADDR, EE_DATA_REG);
avg += sample;
}
/* calculate average */
avg /= 10;
/* convert to numeric digits for display */
hundreds = avg / 100;
tens = (avg % 100) / 10;
ones = (avg % 10);
spi_write(CS_LCD, LCD_CLEAR);
spi_write(CS_LCD, '0' + hundreds);
spi_write(CS_LCD, '0' + tens);
spi_write(CS_LCD, '0' + ones);
}
}
Dies ist ein sehr grobes und schnelles Beispiel. Codieren Sie nicht so!
Denken Sie jetzt daran, dass das präventive Multitasking-Betriebssystem einen dieser Threads in jeder Zeile im Code (eigentlich in jeder Assembler-Anweisung) aussetzen und dem anderen Thread Zeit zum Ausführen geben kann.
Denk darüber nach. Stellen Sie sich vor, was passieren würde, wenn das Betriebssystem entscheiden würde, adc_thread()
zwischen dem Setzen der EE-Adresse zum Schreiben und dem Schreiben der eigentlichen Daten zu unterbrechen. lcd_thread()
würde laufen, mit dem I2C-Peripheriegerät herumspielen, um die benötigten Daten zu lesen, und wenn adc_thread()
es wieder an der Reihe wäre, wäre das EEPROM nicht in demselben Zustand, in dem es verlassen wurde. Die Dinge würden überhaupt nicht sehr gut funktionieren. Schlimmer noch, es könnte sogar die meiste Zeit funktionieren, aber nicht immer, und Sie würden verrückt werden, wenn Sie versuchen würden, herauszufinden, warum Ihr Code nicht funktioniert, wenn er so AUSSIEHT, wie er sollte!
Das ist ein Best-Case-Beispiel; Das Betriebssystem entscheidet sich möglicherweise dafür, den Kontext i2c_write()
von adc_thread()
zu verlassen und es erneut aus lcd_thread()
dem Kontext von auszuführen! Die Dinge können sehr schnell sehr chaotisch werden.
Wenn Sie Code schreiben, der in einer präventiven Multitasking-Umgebung funktioniert, müssen Sie Sperrmechanismen verwenden , um sicherzustellen, dass nicht die Hölle losbricht, wenn Ihr Code zu einem ungünstigen Zeitpunkt ausgesetzt wird.
Kooperatives Multitasking hingegen bedeutet, dass jeder Thread die Kontrolle darüber hat, wann er seine Ausführungszeit aufgibt. Die Codierung ist einfacher, aber der Code muss sorgfältig entworfen werden, um sicherzustellen, dass alle Threads genügend Zeit zum Ausführen erhalten. Noch ein erfundenes Beispiel:
char getch()
{
while (! (*uart_status & DATA_AVAILABLE)) {
/* do nothing */
}
return *uart_data_reg;
}
void putch(char data)
{
while (! (*uart_status & SHIFT_REG_EMPTY)) {
/* do nothing */
}
*uart_data_reg = data;
}
void echo_thread()
{
char data;
while (1) {
data = getch();
putch(data);
yield_cpu();
}
}
void seconds_counter()
{
int count = 0;
while (1) {
++count;
sleep_ms(1000);
yield_cpu();
}
}
Dieser Code wird nicht so funktionieren, wie Sie denken, oder selbst wenn er zu funktionieren scheint, wird er nicht funktionieren, wenn die Datenrate des Echo-Threads zunimmt. Nehmen wir uns noch einmal eine Minute Zeit, um es uns anzusehen.
echo_thread()
wartet darauf, dass ein Byte an einem UART erscheint und bekommt es dann und wartet, bis es Platz zum Schreiben gibt, dann schreibt es. Nachdem dies erledigt ist, können andere Threads ausgeführt werden. seconds_counter()
erhöht einen Zähler, wartet 1000 ms und gibt dann den anderen Threads die Möglichkeit, ausgeführt zu werden. Wenn während dieser Zeit zwei Bytes in den UART sleep()
eingehen, könnten Sie sie übersehen, da unser hypothetischer UART kein FIFO zum Speichern von Zeichen hat, während die CPU mit anderen Dingen beschäftigt ist.
Der richtige Weg, dieses sehr schlechte Beispiel zu implementieren, wäre, dort zu platzieren, yield_cpu()
wo immer Sie eine Besetztschleife haben. Dies hilft, die Dinge voranzutreiben, kann aber andere Probleme verursachen. Wenn zB das Timing kritisch ist und Sie die CPU einem anderen Thread überlassen, der länger dauert als erwartet, könnte Ihr Timing abgeworfen werden. Ein preemptives Multitasking-Betriebssystem hätte dieses Problem nicht, da es Threads zwangsweise anhält, um sicherzustellen, dass alle Threads korrekt geplant sind.
Was hat das nun mit einem Timer und einer Hintergrundschleife zu tun? Der Timer und die Hintergrundschleife sind dem obigen kooperativen Multitasking-Beispiel sehr ähnlich:
void timer_isr(void)
{
++ticks;
if ((ticks % 10)) == 0) {
ten_ms_flag = TRUE;
}
if ((ticks % 100) == 0) {
onehundred_ms_flag = TRUE;
}
if ((ticks % 1000) == 0) {
one_second_flag = TRUE;
}
}
void main(void)
{
/* initialization of timer ISR, etc. */
while (1) {
if (ten_ms_flag) {
if (kbhit()) {
putch(getch());
}
ten_ms_flag = FALSE;
}
if (onehundred_ms_flag) {
get_adc_data();
onehundred_ms_flag = FALSE;
}
if (one_second_flag) {
++count;
update_lcd();
one_second_flag = FALSE;
}
};
}
Dies sieht dem kooperativen Threading-Beispiel ziemlich ähnlich; Sie haben einen Timer, der Ereignisse einrichtet, und eine Hauptschleife, die nach ihnen sucht und auf atomare Weise darauf reagiert. Sie müssen sich keine Sorgen machen, dass sich die ADC- und LCD-„Threads“ gegenseitig stören, da einer den anderen niemals unterbricht. Sie müssen sich immer noch Sorgen machen, dass ein "Thread" zu lange dauert; zB was passiert wenn get_adc_data()
dauert 30ms? Sie werden drei Gelegenheiten verpassen, nach einem Zeichen zu suchen und es wiederzugeben.
Die Loop+Timer-Implementierung ist oft viel einfacher zu implementieren als ein kooperativer Multitasking-Mikrokernel, da Ihr Code spezifischer für die jeweilige Aufgabe entworfen werden kann. Sie betreiben weniger Multitasking als vielmehr das Entwerfen eines festen Systems, bei dem Sie jedem Subsystem etwas Zeit geben, um seine Aufgaben auf sehr spezifische und vorhersehbare Weise zu erledigen. Sogar ein kooperatives Multitasking-System muss eine generische Aufgabenstruktur für jeden Thread haben, und der nächste auszuführende Thread wird durch eine Scheduling-Funktion bestimmt, die recht komplex werden kann.
Die Verriegelungsmechanismen für alle drei Systeme sind die gleichen, aber der für jedes erforderliche Overhead ist ziemlich unterschiedlich.
Persönlich programmiere ich fast immer nach diesem letzten Standard, der Loop+Timer-Implementierung. Ich finde Threading ist etwas, das sehr sparsam verwendet werden sollte. Es ist nicht nur komplexer zu schreiben und zu debuggen, sondern erfordert auch mehr Overhead (ein preemptiver Multitasking-Mikrokernel wird immer größer sein als ein dumm einfacher Timer und Event-Follower der Hauptschleife).
Es gibt auch ein Sprichwort, das jeder, der an Threads arbeitet, zu schätzen wissen wird:
if you have a problem and use threads to solve it, yoeu ndup man with y pemro.bls
:-)
poll()
kommt mir sofort in den Sinn).Multitasking kann in vielen Mikrocontroller-Projekten eine nützliche Abstraktion sein, obwohl ein echter präemptiver Scheduler in den meisten Fällen zu schwerfällig und unnötig wäre. Ich habe weit über 100 Mikrocontroller-Projekte durchgeführt. Ich habe einige Male kooperatives Tasking verwendet, aber ein präventiver Task-Wechsel mit dem damit verbundenen Ballast war bisher nicht angemessen.
Die Probleme beim präemptiven Tasking im Gegensatz zum kooperativen Tasking sind:
Im Allgemeinen ist es sinnvoll, eine Aufgabe einem bestimmten Job zuzuweisen, wenn die CPU dies unterstützen kann und der Job mit genügend verlaufsabhängigen Operationen so kompliziert ist, dass es umständlich wäre, ihn in einige wenige separate Einzelereignisse aufzuteilen. Dies ist im Allgemeinen der Fall, wenn ein Kommunikationseingangsstrom verarbeitet wird. Solche Dinge sind normalerweise stark zustandsgesteuert, abhängig von früheren Eingaben. Beispielsweise können Opcode-Bytes gefolgt von Datenbytes vorhanden sein, die für jeden Opcode eindeutig sind. Dann gibt es das Problem, dass diese Bytes auf Sie zukommen, wenn etwas anderes Sie senden möchte. Mit einer separaten Aufgabe, die den Eingabestrom verarbeitet, können Sie ihn im Aufgabencode so erscheinen lassen, als würden Sie hinausgehen und das nächste Byte abrufen.
Insgesamt sind Aufgaben nützlich, wenn viel Zustandskontext vorhanden ist. Tasks sind im Grunde Zustandsmaschinen, wobei der PC die Zustandsvariable ist.
Viele Dinge, die ein Mikro tun muss, können als Reaktion auf eine Reihe von Ereignissen ausgedrückt werden. Infolgedessen habe ich normalerweise eine Hauptereignisschleife. Dies überprüft jedes mögliche Ereignis nacheinander, springt dann zurück an die Spitze und wiederholt alles. Wenn die Behandlung eines Ereignisses mehr als nur ein paar Zyklen dauert, springe ich nach der Behandlung des Ereignisses normalerweise zum Anfang der Ereignisschleife zurück. Dies bedeutet praktisch, dass Ereignisse eine implizite Priorität haben, je nachdem, wo sie in der Liste markiert sind. Auf vielen einfachen Systemen ist dies gut genug.
Manchmal bekommt man etwas kompliziertere Aufgaben. Diese können oft in eine Abfolge einer kleinen Anzahl separater Aufgaben unterteilt werden. In diesen Fällen können Sie interne Flags als Ereignisse verwenden. Ich habe so etwas oft bei Low-End-PICs gemacht.
Wenn Sie die grundlegende Ereignisstruktur wie oben haben, aber beispielsweise auch auf einen Befehlsstrom über den UART antworten müssen, dann ist es nützlich, eine separate Aufgabe zu haben, die den empfangenen UART-Strom verarbeitet. Einige Mikrocontroller haben begrenzte Hardware-Ressourcen für Multitasking, wie ein PIC 16, der seinen eigenen Call-Stack nicht lesen oder schreiben kann. In solchen Fällen verwende ich eine, wie ich sie nenne, Pseudo-Task für den UART-Befehlsprozessor. Die Hauptereignisschleife behandelt immer noch alles andere, aber eines ihrer zu behandelnden Ereignisse ist, dass ein neues Byte vom UART empfangen wurde. In diesem Fall springt es zu einer Routine, die diese Pseudo-Aufgabe ausführt. Das UART-Befehlsmodul enthält den Aufgabencode, und die Ausführungsadresse und einige Registerwerte der Aufgabe werden im RAM in diesem Modul gespeichert. Der Code, zu dem die Ereignisschleife gesprungen ist, speichert die aktuellen Register, lädt die gespeicherten Task-Register, und springt zur Task-Restart-Adresse. Der Aufgabencode ruft ein YIELD-Makro auf, das das Gegenteil tut, das dann schließlich zum Anfang der Hauptereignisschleife zurückspringt. In einigen Fällen führt die Hauptereignisschleife die Pseudo-Aufgabe einmal pro Durchgang aus, normalerweise ganz unten, um sie zu einem Ereignis mit niedriger Priorität zu machen.
Auf einem PIC 18 und höher verwende ich ein echtes kooperatives Tasking-System, da der Aufrufstapel von der Firmware gelesen und beschrieben werden kann. Auf diesen Systemen werden die Neustartadresse, einige andere Zustandselemente und der Datenstapelzeiger für jede Aufgabe in einem Speicherpuffer gehalten. Um alle anderen Aufgaben einmal laufen zu lassen, ruft eine Aufgabe TASK_YIELD auf. Dies speichert den aktuellen Aufgabenstatus, durchsucht die Liste nach der nächsten verfügbaren Aufgabe, lädt ihren Status und führt sie dann aus.
In dieser Architektur ist die Hauptereignisschleife nur eine weitere Aufgabe mit einem Aufruf von TASK_YIELD am Anfang der Schleife.
Mein gesamter Multitasking-Code für PICs ist kostenlos verfügbar. Um es anzuzeigen, installieren Sie die PIC Development Tools- Version unter http://www.embedinc.com/pic/dload.htm . Suchen Sie nach Dateien mit „task“ in ihrem Namen im Verzeichnis SOURCE > PIC für die 8-Bit-PICs und im Verzeichnis SOURCE > DSPIC für die 16-Bit-PICs.
the most significant advantage of non-preemptive systems when it comes to mutexes is that they are only required either when interacting directly with interrupts...
Ich bin mir ziemlich sicher, dass Sie nicht implizieren wollen, dass ein System (präventiv oder nicht) Mutexe (im Gegensatz zu kritischen Abschnitten) verwenden sollte, um Daten zu schützen, die zwischen einem Thread und einem Interrupt geteilt werden. Auf einen Mutex in einem (asynchronen) Interrupt warten - keine so gute Idee. Obwohl ich sicher bin, dass Sie das wissen, bin ich mir bei der OP nicht so sicher.yield()
bis das Flag gelöscht ist ). Nicht ganz das normale Muster für einen Mutex oder einen kritischen Abschnitt; Ich bin mir nicht sicher, wie es heißen soll.Bearbeiten: (Ich werde meinen früheren Beitrag unten lassen; vielleicht hilft es jemandem eines Tages.)
Multitasking-Betriebssysteme jeglicher Art und Interrupt-Service-Routinen sind keine konkurrierenden Systemarchitekturen – oder sollten es nicht sein. Sie sind für verschiedene Jobs auf verschiedenen Ebenen des Systems gedacht. Interrupts sind wirklich für kurze Codesequenzen gedacht, um unmittelbare Aufgaben wie das Neustarten eines Geräts, möglicherweise das Abfragen von nicht unterbrechenden Geräten, Zeitmessung in Software usw. zu erledigen. Es wird normalerweise angenommen, dass der Hintergrund jede weitere Verarbeitung übernimmt, die danach nicht mehr zeitkritisch ist Sofortige Bedürfnisse wurden erfüllt. Wenn Sie lediglich einen Timer neu starten und eine LED umschalten oder ein anderes Gerät pulsieren müssen, kann der ISR dies normalerweise sicher im Vordergrund erledigen. Andernfalls muss es den Hintergrund informieren (indem es ein Flag setzt oder eine Nachricht in die Warteschlange stellt), dass etwas getan werden muss, und den Prozessor freigeben.
Ich habe sehr einfache Programmstrukturen gesehen, deren Hintergrundschleife nur eine Leerlaufschleife ist: for(;;){ ; }
. Die gesamte Arbeit wurde in der Timer-ISR erledigt. Dies kann funktionieren, wenn das Programm einen konstanten Vorgang wiederholen muss, der garantiert in weniger als einer Timer-Periode abgeschlossen ist. bestimmte begrenzte Arten der Signalverarbeitung kommen mir in den Sinn.
Persönlich schreibe ich ISRs, die aufräumen und herauskommen, und lasse den Hintergrund alles andere übernehmen, was getan werden muss, selbst wenn das so einfach ist wie eine Multiplikation und Addition, die in einem Bruchteil einer Timer-Periode erledigt werden könnte. Warum? Eines Tages komme ich auf die brillante Idee, meinem Programm eine weitere „einfache“ Funktion hinzuzufügen, und „verdammt, es braucht nur eine kurze ISR, um es zu tun“, und plötzlich wächst meine zuvor einfache Architektur mit einigen Interaktionen, die ich nicht geplant hatte an und passieren inkonsistent. Diese zu debuggen macht nicht viel Spaß.
(Zuvor geposteter Vergleich zweier Arten von Multitasking)
Aufgabenumschaltung: Preemptive MT übernimmt die Aufgabenumschaltung für Sie, einschließlich der Sicherstellung, dass kein Thread CPU-hungrig wird und dass Threads mit hoher Priorität ausgeführt werden, sobald sie bereit sind. Cooperative MT erfordert, dass der Programmierer sicherstellt, dass kein Thread den Prozessor zu lange am Stück beschäftigt. Sie müssen auch entscheiden, wie lang zu lang ist. Das bedeutet auch, dass Sie bei jeder Änderung des Codes darauf achten müssen, ob ein Codesegment jetzt dieses Zeitquantum überschreitet.
Schutz nicht-atomarer Operationen: Bei einer PMT müssen Sie sicherstellen, dass Thread-Swaps nicht mitten in Operationen stattfinden, die nicht geteilt werden dürfen. Lesen/Schreiben bestimmter Geräteregisterpaare, die beispielsweise in einer bestimmten Reihenfolge oder innerhalb einer maximalen Zeit behandelt werden müssen. Mit CMT ist es ziemlich einfach - geben Sie den Prozessor nur nicht mitten in einer solchen Operation auf.
Debugging: Im Allgemeinen einfacher mit CMT, da Sie planen, wann/wo Threadwechsel stattfinden. Race-Conditions zwischen Threads und Bugs im Zusammenhang mit nicht Thread-sicheren Operationen mit einem PMT sind besonders schwer zu debuggen, da Thread-Änderungen probabilistisch und somit nicht wiederholbar sind.
Den Code verstehen: Threads, die für ein PMT geschrieben wurden, sind ziemlich genau so geschrieben, als ob sie alleine stehen könnten. Threads, die für ein CMT geschrieben wurden, werden als Segmente geschrieben und können je nach gewählter Programmstruktur für einen Leser schwieriger zu verstehen sein.
Verwenden von nicht Thread-sicherem Bibliothekscode: Sie müssen überprüfen, ob jede Bibliotheksfunktion, die Sie unter einem PMT-Thread-sicher aufrufen, sicher ist. printf() und scanf() und ihre Varianten sind fast immer nicht Thread-sicher. Bei einem CMT wissen Sie, dass keine Thread-Änderung auftritt, es sei denn, Sie geben den Prozessor ausdrücklich frei.
Ein von einem endlichen Zustandsautomaten gesteuertes System zur Steuerung eines mechanischen Geräts und/oder zur Verfolgung externer Ereignisse sind oft gute Kandidaten für CMT, da bei jedem Ereignis nicht viel zu tun ist – einen Motor starten oder stoppen, ein Flag setzen, den nächsten Zustand wählen usw. Somit sind Zustandsänderungsfunktionen von Natur aus kurz.
Ein hybrider Ansatz kann in solchen Systemen sehr gut funktionieren: CMT, um die Zustandsmaschine (und damit den größten Teil der Hardware) zu verwalten, die als ein Thread ausgeführt wird, und ein oder zwei weitere Threads, um länger laufende Berechnungen durchzuführen, die von einem Zustand ausgelöst werden Rückgeld.
Kohlschmied