Wie bereits erwähnt, verfügt jeder Thread über seine eigenen Ressourcen, der Codebereich wird jedoch gemeinsam genutzt, dh jeder Thread kann dieselbe Funktion ausführen. Das Problem, das dies verursachen kann, besteht darin, dass mehrere Threads gleichzeitig eine Funktion ausführen, was zu Datenverwirrung und unvorhersehbaren Ergebnissen führt. Daher müssen wir diese Situation vermeiden.
C# bietet eine Schlüsselwortsperre, mit der ein Codeabschnitt als sich gegenseitig ausschließender Abschnitt (kritischer Abschnitt) definiert werden kann, sodass jeweils nur ein Thread ausgeführt werden kann, während andere Threads warten müssen. In C# ist die Schlüsselwortsperre wie folgt definiert:
lock(expression) Statement_block
Ausdruck stellt das Objekt dar, das Sie verfolgen möchten, normalerweise eine Objektreferenz.
Der Statement_Block ist der Code des sich gegenseitig ausschließenden Abschnitts. Dieser Code kann jeweils nur von einem Thread ausgeführt werden.
Im Folgenden finden Sie ein typisches Beispiel für die Verwendung des Schlüsselworts lock. Die Verwendung und der Zweck des Schlüsselworts lock werden in den Kommentaren erläutert.
Beispiele sind wie folgt:
Code
Verwenden des Systems;
Verwenden von System.Threading;
Namensraum ThreadSimple
{
internes Klassenkonto
{
int Balance;
Zufällig r = new Random();
internes Konto (int initial)
{
Saldo = anfänglich;
}
intern int Abheben (int Betrag)
{
wenn (Saldo < 0)
{
//Wenn der Kontostand kleiner als 0 ist, wird eine Ausnahme ausgelöst
throw new Exception("Negative Balance");
}
//Der folgende Code wird garantiert abgeschlossen, bevor der aktuelle Thread den Wert von Balance ändert.
//Kein anderer Thread wird diesen Code ausführen, um den Wert von Balance zu ändern
//Daher darf der Wert von balance nicht kleiner als 0 sein
sperren(dies)
{
Console.WriteLine("Aktueller Thread:"+Thread.CurrentThread.Name);
// Wenn das Schlüsselwort lock nicht geschützt ist, kann dies nach der Ausführung der if-bedingten Beurteilung der Fall sein.
//Ein anderer Thread hat balance=balance-amount ausgeführt und den Wert von balance geändert.
//Diese Änderung ist für diesen Thread unsichtbar und kann daher dazu führen, dass die if-Bedingung nicht mehr gilt.
//Dieser Thread führt jedoch weiterhin balance=balance-amount aus, daher kann der Kontostand kleiner als 0 sein
if (Saldo >= Betrag)
{
Thread.Sleep(5);
Saldo = Saldo - Betrag;
Rückgabebetrag;
}
anders
{
return 0; // Transaktion abgelehnt
}
}
}
interne Leere DoTransactions()
{
for (int i = 0; i < 100; i++)
Zurückziehen(r.Next(-50, 100));
}
}
Interner Klassentest
{
static internal Thread[] threads = new Thread[10];
öffentliches statisches void Main()
{
Kontokonto = neues Konto (0);
for (int i = 0; i < 10; i++)
{
Thread t = new Thread(new ThreadStart(acc.DoTransactions));
threads[i] = t;
}
for (int i = 0; i < 10; i++)
threads[i].Name=i.ToString();
for (int i = 0; i < 10; i++)
threads[i].Start();
Console.ReadLine();
}
}
}Monitor-Klasse sperrt ein Objekt
Wenn mehrere Threads ein Objekt gemeinsam nutzen, treten ähnliche Probleme wie bei allgemeinem Code auf. Das Schlüsselwort lock sollte hier nicht verwendet werden. , Monitor bietet eine Lösung für Threads zur gemeinsamen Nutzung von Ressourcen.
Die Monitor-Klasse kann ein Objekt sperren und ein Thread kann das Objekt nur bearbeiten, wenn er die Sperre erhält. Der Objektsperrmechanismus stellt sicher, dass in Situationen, die zu Chaos führen können, jeweils nur ein Thread auf dieses Objekt zugreifen kann. Monitor muss einem bestimmten Objekt zugeordnet sein. Da es sich jedoch um eine statische Klasse handelt, kann er nicht zum Definieren von Objekten verwendet werden. Alle seine Methoden sind statisch und können nicht mithilfe von Objekten referenziert werden. Der folgende Code veranschaulicht die Verwendung von Monitor zum Sperren eines Objekts:
...
Warteschlange oQueue=new Queue();
...
Monitor.Enter(oQueue);
......//Jetzt kann das oQueue-Objekt nur noch vom aktuellen Thread manipuliert werden
Monitor.Exit(oQueue);//Sperre freigeben
Wenn ein Thread, wie oben gezeigt, die Methode Monitor.Enter() aufruft, um ein Objekt zu sperren, gehört das Objekt ihm. Wenn andere Threads auf dieses Objekt zugreifen möchten, können sie nur darauf warten, dass es die Methode Monitor.Exit() verwendet. Methode zum Aufheben der Sperre. Um sicherzustellen, dass der Thread die Sperre irgendwann aufhebt, können Sie die Methode Monitor.Exit() in den Final-Codeblock in der Struktur try-catch-finally schreiben.
Für jedes von Monitor gesperrte Objekt werden einige damit verbundene Informationen im Speicher gespeichert:
Einer ist ein Verweis auf den Thread, der derzeit die Sperre hält;
Die zweite ist eine Vorbereitungswarteschlange, in der Threads gespeichert werden, die zum Erwerb von Sperren bereit sind.
Die dritte ist eine Warteschlange, die einen Verweis auf die Warteschlange enthält, die derzeit auf eine Statusänderung des Objekts wartet.
Wenn der Thread, der die Objektsperre besitzt, bereit ist, die Sperre aufzuheben, verwendet er die Methode Monitor.Pulse(), um den ersten Thread in der Warteschlange zu benachrichtigen, sodass der Thread in die Vorbereitungswarteschlange übertragen wird, wenn die Objektsperre aufgehoben wird , in der Vorbereitungswarteschlange Der Thread kann die Objektsperre sofort erwerben.
Das Folgende ist ein Beispiel, das zeigt, wie das Schlüsselwort lock und die Monitor-Klasse zum Implementieren der Thread-Synchronisierung und -Kommunikation verwendet werden. Es handelt sich ebenfalls um ein typisches Produzenten- und Verbraucherproblem.
In dieser Routine wechseln sich der Producer-Thread und der Consumer-Thread ab. Der Producer schreibt eine Zahl, und der Consumer liest sie sofort und zeigt sie an (die Essenz des Programms wird in den Kommentaren vorgestellt).
Die verwendeten System-Namespaces sind wie folgt:
Verwenden des Systems;
Verwenden von System.Threading;
Definieren Sie zunächst eine Klassenzelle des Objekts, an dem gearbeitet wird. In dieser Klasse gibt es zwei Methoden: ReadFromCell() und WriteToCell. Der Consumer-Thread ruft ReadFromCell() auf, um den Inhalt von cellContents zu lesen und anzuzeigen, und der Producer-Prozess ruft die Methode WriteToCell() auf, um Daten in cellContents zu schreiben.
Beispiele sind wie folgt:
Code
öffentliche Klasse Zelle
{
int cellContents; //Inhalt im Cell-Objekt
bool readerFlag = false; // Status-Flag, wenn es wahr ist, kann es gelesen werden, wenn es falsch ist, wird es geschrieben
public int ReadFromCell( )
{
lock(this) // Was garantiert das Schlüsselwort Lock? Bitte lesen Sie die vorherige Einführung zu lock.
{
if (!readerFlag)//Wenn es jetzt nicht lesbar ist
{
versuchen
{
//Warten Sie, bis die Monitor.Pulse()-Methode in der WriteToCell-Methode aufgerufen wird
Monitor.Wait(this);
}
Catch (SynchronizationLockException e)
{
Console.WriteLine(e);
}
Catch (ThreadInterruptedException e)
{
Console.WriteLine(e);
}
}
Console.WriteLine("Consume: {0}",cellContents);
ReaderFlag = false;
// Setzen Sie das ReaderFlag-Flag zurück, um anzuzeigen, dass das Verbrauchsverhalten abgeschlossen ist
Monitor.Pulse(this);
//Benachrichtigen Sie die WriteToCell()-Methode (diese Methode wird in einem anderen Thread ausgeführt und wartet)
}
return cellContents;
}
public void WriteToCell(int n)
{
sperren(dies)
{
if(readerFlag)
{
versuchen
{
Monitor.Wait(this);
}
Catch (SynchronizationLockException e)
{
//Wenn synchronisierte Methoden (bezogen auf Methoden der Monitor-Klasse außer Enter) in asynchronen Codebereichen aufgerufen werden
Console.WriteLine(e);
}
Catch (ThreadInterruptedException e)
{
//Abbruch, wenn sich der Thread im Wartezustand befindet
Console.WriteLine(e);
}
}
cellContents = n;
Console.WriteLine("Produce: {0}",cellContents);
ReaderFlag = true;
Monitor.Pulse(this);
//Benachrichtigen Sie die wartende ReadFromCell()-Methode in einem anderen Thread
}
}
}
Die Producer-Klasse CellProd und die Consumer-Klasse CellCons sind unten definiert. Sie haben beide nur eine Methode ThreadRun(), sodass das ThreadStart-Proxy-Objekt, das dem Thread in der Main()-Funktion bereitgestellt wird, als Eingang zum Thread dient.
öffentliche Klasse CellProd
{
Cell cell; // Zellobjekt, das bearbeitet wird
int amount = 1; // Die Häufigkeit, mit der der Produzent produziert, initialisiert auf 1
public CellProd(Cell box, int request)
{
//Konstrukteur
Zelle = Box;
Menge = Anfrage;
}
public void ThreadRun( )
{
for(int looper=1; looper<=quantity; looper++)
cell.WriteToCell(looper); //Der Produzent schreibt Informationen in das Operationsobjekt
}
}
öffentliche Klasse CellCons
{
Zellzelle;
int Menge = 1;
public CellCons(Cell box, int request)
{
//Konstrukteur
Zelle = Box;
Menge = Anfrage;
}
public void ThreadRun( )
{
int valReturned;
for(int looper=1; looper<=quantity; looper++)
valReturned=cell.ReadFromCell();//Consumer liest Informationen aus dem Operationsobjekt
}
}
Dann müssen wir in der Main()-Funktion der MonitorSample-Klasse unten zwei Threads als Produzenten und Konsumenten erstellen und die Methoden CellProd.ThreadRun() und CellCons.ThreadRun() verwenden, um Operationen an denselben auszuführen Zellobjekt.
Code
öffentliche Klasse MonitorSample
{
public static void Main(String[] args)
{
int result = 0; //Ein Flag-Bit bedeutet, dass kein Fehler im Programm vorliegt.
Zelle Zelle = neue Zelle( );
// Im Folgenden wird cell zum Initialisieren der Klassen CellProd und CellCons verwendet. Die Produktions- und Verbrauchszeiten betragen jeweils das 20-fache.
CellProd prod = new CellProd(cell, 20);
CellCons cons = new CellCons(cell, 20);
Thread-Produzent = new Thread(new ThreadStart(prod.ThreadRun));
Thread-Consumer = new Thread(new ThreadStart(cons.ThreadRun));
//Sowohl der Producer-Thread als auch der Consumer-Thread wurden erstellt, aber die Ausführung wurde noch nicht gestartet.
versuchen
{
Produzent.Start( );
Consumer.Start( );
Produzent.Join( );
Consumer.Join( );
Console.ReadLine();
}
Catch (ThreadStateException e)
{
//Wenn der Thread aufgrund seines Status die angeforderte Operation nicht ausführen kann
Console.WriteLine(e);
Ergebnis = 1;
}
Catch (ThreadInterruptedException e)
{
//Abbruch, wenn sich der Thread im Wartezustand befindet
Console.WriteLine(e);
Ergebnis = 1;
}
//Obwohl die Funktion Main() keinen Wert zurückgibt, kann die folgende Anweisung das Ausführungsergebnis an den übergeordneten Prozess zurückgeben
Environment.ExitCode = Ergebnis;
}
}
In der obigen Routine erfolgt die Synchronisierung durch Warten auf Monitor.Pulse(). Zunächst produziert der Produzent einen Wert, und gleichzeitig befindet sich der Konsument in einem Wartezustand, bis er vom Produzenten einen „Impuls“ erhält, der ihm mitteilt, dass die Produktion abgeschlossen ist. Danach tritt der Konsument in den Konsumzustand ein. und der Produzent beginnt zu warten, bis der Verbraucher den Vorgang abgeschlossen hat. Der von Monitor.Pulese() ausgegebene „Impuls“ wird nach der Operation aufgerufen.
Das Ausführungsergebnis ist einfach:
Produzieren: 1
Verbrauchen: 1
Produzieren: 2
Verbrauchen: 2
Produzieren: 3
Verbrauchen: 3
...
...
Produzieren: 20
Verbrauch: 20
Tatsächlich hat uns dieses einfache Beispiel dabei geholfen, große Probleme zu lösen, die in Multithread-Anwendungen auftreten können. Solange Sie die grundlegenden Methoden zur Lösung von Konflikten zwischen Threads verstehen, ist es einfach, es auf komplexere Programme anzuwenden.
-