Dr. Erhard Henkes (e.henkes@gmx.net) - C++ und MFC - Stand: 04.08.2002

zurück zum Inhaltsverzeichnis

zurück zum vorherigen Kapitel
 

Threads:

Was ist ein Thread? Dieses englische Wort bedeutet "Faden". Wenn eine Anwendung also mehrere Threads besitzt, dann laufen mehrere Programm-"Fäden" zeitlich parallel. Man spricht dann von Multithreading im Gegensatz zu Singlethreading. Die Fragen, die sich stellen, sind:

Beginnen wir mit der ersten Frage.

Wenn man im Betriebssystem MS Windows eine Anwendung startet, dann wird ein Prozess gestartet. Dieser Prozess beginnt mit einem Thread. Dieser Thread kann seit den 32-Bit-Versionen des Betriebssystems (also seit Windows 95) weitere Threads hervorbringen. Das Betriebssystem muß dafür sorgen, dass die Rechenzeit ständig in kleinen Zeitscheiben zwischen all den Threads aufgeteilt wird. Bei Multithreading laufen die Threads sozusagen "quasi-parallel", oder genauer gesagt: es können nur soviele Threads "echt" parallel laufen, wie der Computer Prozessoren hat.

Geeignet sind weitere Threads für die Erledigung rechenintensiver Hintergrundaufgaben. Der Haupt-Thread kümmert sich während seiner Zeitscheiben um den "mausklickenden" Benutzer, der rasche Reaktionen verlangt, während weitere Threads im Hintergrund in ihren Zeitscheiben Haupt- oder Nebenarbeiten erledigen. Hier liegt der entscheidende Vorteil dieser Methode.

Multithreading-Anwendungen sind einfach und kompliziert zugleich. Einfach, weil sie leicht zu programmieren sind, kompliziert, weil Threads sich zeitlich unkoordiniert - also völlig asynchron - verhalten. Man muß sich daher neben der eigentlichen Programmieraufgabe verstärkt um die Regeln für das Miteinander - also die Synchronisierung der Threads - kümmern. Dafür kann sich das Antwortverhalten einer Anwendung signifikant erhöhen. Dies ist der Anreiz für die Mühe. Eine Geschwindigkeitserhöhung des gesamten Prozesses ist bei einer Maschine mit einem einzigen Prozessor nicht zu erwarten, eher das Gegenteil aufgrund des Multithreading-Überbaus.

Als Analogie stellt man sich z.B. ein Ladengeschäft mit angrenzender Werkstatt vor. Ist nur eine Person anwesend, muß diese, wenn ein Kunde den Laden betritt, die Arbeit in der Werkstatt unterbrechen und in den Laden eilen. Sind mehrere Personen anwesend, kann man die Bedienung der Kunden und die Werkstattarbeit personell sinnvoll aufteilen. Das Multithreading-Konzept hat sein Vorbild also in der arbeitsteiligen Gesellschaft mit den damit verbundenen Koordinationsaufgaben.
 

Nun zur zweiten Frage.

Wie sieht eigentlich ein einfaches MFC-Multithreading-Programm aus?

Wir erzeugen zunächst eine dialogbasierende MFC-Anwendung namens "Thread001" mit zwei Schaltflächen und zwei damit verbundenen Funktionen (OnButtonStart und OnButtonStop). Der Klick auf den einen Button ( IDC_BUTTONSTART, Aufschrift: "Thread starten" ) soll einen zusätzlichen Hintergrund-Thread starten, während der Klick auf den zweiten Button ( IDC_BUTTONSTOP, Aufschrift: "Thread stoppen" ) diesen Thread stoppt.

In der Klassendefinition der Dialogklasse (CThread001Dlg) werden wir zwei Member (z.B. mit Assistent) hinzufügen:

Thread-Funktionen müssen global oder statische Member-Funktionen sein, da diese vom Betriebssystem aufgerufen werden.

Wir fügen nun folgenden Start- und Stopp-Code und den Code der Thread-Funktion hinzu:
 
 class CThread001Dlg : public CDialog
 {
 // Konstruktion
 public:
 static UINT thrFunction (LPVOID pParam);
 CThread001Dlg(CWnd* pParent = NULL); // Standard-Konstruktor
  ...
  ...
 private:
 int m_Flag;
 }

 //... 

 void CThread001Dlg::OnButtonStart() 
 {
   m_Flag = 1;
   CWinThread* pThread = AfxBeginThread (thrFunction, &m_Flag);
 }

 void CThread001Dlg::OnButtonStop() 
 {
   m_Flag = 0;
 }

 UINT CThread001Dlg::thrFunction(LPVOID pParam)
 {
   int* pFlag = (int*) pParam;
   while (*pFlag) 
   {
     Sleep(1000);
     MessageBeep(0);
   }
   return 0;
 }

Nach dem Kompilieren sollte das Multithreading bereits funktionieren. Wie können wir uns nun vergewissern, dass neben dem Hauptthread wirklich ein zweiter Thread erzeugt wird? Erstens "hören" wir den Thread, da jede Sekunde ein Sound durch MessageBeep(0) ausgelöst wird. Zweitens können wir vor und nach dem Thread-Start den Prozess-Viewer, ein Tool, das den MS VC++ 6 begleitet, einsetzen, um die Anzahl der Threads je Prozess zu prüfen:

Vor dem Start des zweiten Threads:

Nach dem Start des zweiten Threads:

Zunächst analysieren wir den Code, damit das Zusammenspiel zwischen den Threads klarer wird:

Die entscheidende Zeile für den Start des zusätzlichen Arbeitsthreads ist:

CWinThread* pThread = AfxBeginThread (thrFunction, &m_Flag);

Wir starten hier einen sogenannten Arbeitsthread ("Workerthread", ohne Fenster, ohne Nachrichtenschleife).
Die vollständige Syntax dieser Funktion lautet:
 
CWinThread* AfxBeginThread

  AFX_THREADPROC        pfnThreadProc, 
  LPVOID                pParam, 
  int                   nPriority       = THREAD_PRIORITY_NORMAL, 
  UINT                  nStackSize      = 0, 
  DWORD                 dwCreateFlags   = 0, 
  LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL
);

Der Rückgabewert der Funktion ist ein Zeiger auf das erstellte Thread-Objekt der MFC-Klasse CWinThread.

Der erste Parameter ist ein Zeiger auf die eigentlich Threadfunktion. Diese Funktion muß wie folgt deklariert werden:

UINT ThreadFunction( LPVOID );

Der Funktionsname kann natürlich frei gewählt werden.

Der zweite Parameter ist ein Zeiger auf den dieser Threadfunktion zu übergebenden Parameter.

Wir übergeben in unserem Beispiel die Integerzahl 1 an diese Funktion. Der Parameter ist vom Zeiger-Typ LPVOID. Daher erfolgt in der Threadfunktion die Zurückumwandlung von void* auf int*. Den Inhalt dieser Adresse erhalten wir dann mit *pFlag. *pFlag ist in diesem Fall also identisch mit m_Flag. Daher können wir hier den Thread durch einfaches Nullsetzen von m_Flag beenden.

Der dritte Parameter legt die Priorität dieses Threads fest. Übergeben wir hier den Wert 0, dann hat der zusätzliche Thread die gleiche Priorität wie der erzeugende Thread.

Werte geordnet nach abnehmender Priorität sind:
 
 THREAD_PRIORITY_TIME_CRITICAL
 THREAD_PRIORITY_HIGHEST
 THREAD_PRIORITY_ABOVE_NORMAL
 THREAD_PRIORITY_NORMAL
 THREAD_PRIORITY_BELOW_NORMAL
 THREAD_PRIORITY_LOWEST
 THREAD_PRIORITY_IDLE

Im Standardfall ist dieser Parameter automatisch auf THREAD_PRIORITY_NORMAL gesetzt. Das reicht für normale Anwendungen völlig aus.

Der vierte Parameter legt die Größe des Stacks in Bytes für den neuen Thread fest. Ein Wert von 0 ergibt die gleiche Stack-Größe wie beim erzeugenden Thread (normal sind max. 1 MB).

Der fünfte Parameter spezifiziert die Erzeugung des Threads, hier gibt es zwei Möglichkeiten:
 
CREATE_SUSPENDED Der Thread wird von außen durch ResumeThread() gestartet.
0: Der Thread wird sofort gestartet.

Der sechste Parameter ist ein Zeiger auf eine SECURITY_ATTRIBUTES Struktur.
Wenn dieser Zeiger gleich NULL ist, dann gelten die gleichen Sicherheitsattribute wie beim erzeugenden Thread.

"MFC under the hood":
Wen es interessiert, die Implementierung dieser MFC-Funktion findet sich in THRDCORE.CPP :
 
CWinThread* AFXAPI AfxBeginThread(AFX_THREADPROC pfnThreadProc, LPVOID pParam,
 int nPriority, UINT nStackSize, DWORD dwCreateFlags,
 LPSECURITY_ATTRIBUTES lpSecurityAttrs)
{
 //...

 ASSERT(pfnThreadProc != NULL);

 CWinThread* pThread = DEBUG_NEW CWinThread(pfnThreadProc, pParam);
 ASSERT_VALID(pThread);

 if (!pThread->CreateThread(dwCreateFlags|CREATE_SUSPENDED, nStackSize, lpSecurityAttrs))
 {
  pThread->Delete();
  return NULL;
 }
 VERIFY(pThread->SetThreadPriority(nPriority));
 if (!(dwCreateFlags & CREATE_SUSPENDED))  VERIFY(pThread->ResumeThread() != (DWORD)-1);

 return pThread;
}

Sie sehen, der erste Einstieg in die Thread-Programmierung ist mittels MFC nicht allzu schwierig.

Damit Sie Tests mit Benutzereingaben durchführen können, fügen wir folgende Funktion für die Nachricht WM_MOUSEMOVE hinzu:
 
void CThread001Dlg::OnMouseMove(UINT nFlags, CPoint point) 
{
 CClientDC dc(this);
 dc.SetPixel(point, RGB(0,0,0));

 CDialog::OnMouseMove(nFlags, point);
}

Nun können Sie durch Ziehen mit der Maus über das Dialogfeld ein Gefühl für das Antwortverhalten Ihrer Anwendung mit und ohne laufenden zweiten Thread erhalten. Sie sollten im konkreten Fall keinen Unterschied sehen.

Wir haben die Member-Variable m_Flag auf dem Umweg über mehrere Zeiger zur Verwendung in unserer Threadfunktion weitergereicht. Hier gibt es eine interessante Alternative, mit der wir den Zwang einer statischen Funktion quasi aushebeln:

Im ersten Schritt fügen wir eine zusätzliche nicht-statische Member-Funktion void CThread001Dlg::thrRun() hinzu.
 
class CThread001Dlg : public CDialog
{
// Konstruktion
public:
 void thrRun();
 static UINT thrFunction (LPVOID pParam);
 CThread001Dlg(CWnd* pParent = NULL); // Standard-Konstruktor
...
...

Im zweiten Schritt wechseln wir nach OnButtonStart() und übergeben dort als zweiten Parameter in AfxBeginThread(...) keinen Zeiger auf m_Flag, sondern den this -Zeiger, den wir nachfolgend in der statischen Funktion in einen Zeiger auf unsere Dialogklasse CThread001Dlg* umwandeln. Mit diesem Zeiger können wir dann die eigentliche nicht-statische Funktion starten. Die Member-Variable kann dort direkt verwendet werden.
 
void CThread001Dlg::OnButtonStart() 
{
  m_Flag = 1;
  CWinThread* pThread = AfxBeginThread (thrFunction, this);
}

void CThread001Dlg::OnButtonStop() 
{
  m_Flag = 0;
}

UINT CThread001Dlg::thrFunction(LPVOID pParam)
{
  CThread001Dlg* pDlg = (CThread001Dlg*) pParam;
  pDlg->thrRun();
  return 0;
}

void CThread001Dlg::OnMouseMove(UINT nFlags, CPoint point) 
{
  CClientDC dc(this);
  dc.SetPixel(point, RGB(255,0,0));

  CDialog::OnMouseMove(nFlags, point);
}

void CThread001Dlg::thrRun()
{
  while (m_Flag) 
  {
     Sleep(1000);
     MessageBeep(0);
  }
}

Nach diesem Schema lassen sich auch leicht zwei oder mehr zusätzliche Threads erzeugen. Ändern Sie einfach den Sourcecode ohne Einsatz des Assistenten wie folgt ab:
 
class CThread001Dlg : public CDialog
{
// Konstruktion
public:
 void thrRun1();
 void thrRun2();
 static UINT thrFunction1 (LPVOID pParam);
 static UINT thrFunction2 (LPVOID pParam);
 CThread001Dlg(CWnd* pParent = NULL); // Standard-Konstruktor
//...
//...
private:
 int m_Flag;
};

void CThread001Dlg::OnButtonStart() 
{
  m_Flag = 1;
  CWinThread* pThread1 = AfxBeginThread (thrFunction1, this);
  CWinThread* pThread2 = AfxBeginThread (thrFunction2, this);
}

void CThread001Dlg::OnButtonStop() 
{
  m_Flag = 0;
}

UINT CThread001Dlg::thrFunction1(LPVOID pParam)
{
  CThread001Dlg* pDlg = (CThread001Dlg*) pParam;
  pDlg->thrRun1();
  return 0;
}

UINT CThread001Dlg::thrFunction2(LPVOID pParam)
{
  CThread001Dlg* pDlg = (CThread001Dlg*) pParam;
  pDlg->thrRun2();
  return 0;
}

void CThread001Dlg::OnMouseMove(UINT nFlags, CPoint point) 
{
  CClientDC dc(this);
  dc.SetPixel(point, RGB(255,0,0));

  CDialog::OnMouseMove(nFlags, point);
}

void CThread001Dlg::thrRun1()
{
  while (m_Flag) 
  {
    Sleep(1000);
    MessageBeep(0);
  }

}

void CThread001Dlg::thrRun2()
{
  while (m_Flag) 
  {
  Sleep(1000);
   WinExec("notepad.exe",SW_SHOWMINIMIZED);
  }
}
 

Dass diese Anwendung wirklich 3 Threads umfaßt, zeigt uns erneut der Process Viewer:

Mit zwei zusätzlichen Arbeitsthreads kann man experimentell zwei gegensätzliche Aktionen testen. Hier ein kleines Beispiel mit einem Checkbutton (vergrößern Sie bitte das Dialogfeld, fügen Sie ein Optionsfeld hinzu und erzeugen Sie die Member-Variable m_Checkbutton vom Kontroll-Typ CButton.):
 
void CThread001Dlg::thrRun1()
{
  while (m_Flag) 
  {
    Sleep(99);
    m_Checkbutton.SetCheck(TRUE);
  }
}

void CThread001Dlg::thrRun2()
{
  while (m_Flag) 
  {
    Sleep(101);
    m_Checkbutton.SetCheck(FALSE);
  }
}

Das ergibt ein ständiges Hin und Her mit dem Setzen/Zurücksetzen des Optionsfeldes mit zeitlich bedingten Effekten.
Was passiert eigentlich, wenn man beide Threads direkt gegeneinander loshetzt, also ohne Sleep(...)?
 
void CThread001Dlg::thrRun1()
{
  while (m_Flag) 
  {
    m_Checkbutton.SetCheck(TRUE);
  }
}

void CThread001Dlg::thrRun2()
{
  while (m_Flag) 
  {
    m_Checkbutton.SetCheck(FALSE);
  }
}

Es ergibt sich - wie oben - ein ständiges Hin und Her, ohne daß sich ein Thread durchsetzen kann. Das ist ein interessanter Punkt. Hier können wir die Threadprioritäten ins Spiel bringen. Nutzen Sie folgenden Code, um die Prioritäten der beiden Threads unterschiedlich zu gestalten:
 
void CThread001Dlg::OnButtonStart() 
{
 m_Flag = 1;
 CWinThread* pThread1 = AfxBeginThread (thrFunction1, this);
 CWinThread* pThread2 = AfxBeginThread (thrFunction2, this);

 pThread1->SetThreadPriority(THREAD_PRIORITY_HIGHEST);
 pThread2->SetThreadPriority(THREAD_PRIORITY_LOWEST);
}

Das führt nun zu einer klaren - optisch gut nachvollziehbaren - Vorherrschaft des einen Threads gegenüber dem anderen.
Wenn Sie den Hauptthread ( Zeiger pThread0 ) in diese Experimente einbeziehen wollen, benutzen Sie AfxGetThread():

"MFC under the hood":
Hier die einfache Implementierung von AfxGetThread() in THRDCORE.CPP:
 
CWinThread* AFXAPI AfxGetThread()
{
 // check for current thread in module thread state
 AFX_MODULE_THREAD_STATE* pState = AfxGetModuleThreadState();
 CWinThread* pThread = pState->m_pCurrentWinThread;

 // if no CWinThread for the module, then use the global app
 if (pThread == NULL)  pThread = AfxGetApp();

 return pThread;
}

Hier unsere Implementierung in OnButtonStart:
 
void CThread001Dlg::OnButtonStart() 
{
 m_Flag = 1;

 CWinThread* pThread0 = AfxGetThread( );
 CWinThread* pThread1 = AfxBeginThread (thrFunction1, this);
 CWinThread* pThread2 = AfxBeginThread (thrFunction2, this);

 pThread0->SetThreadPriority(THREAD_PRIORITY_ABOVE_NORMAL);
 pThread1->SetThreadPriority(THREAD_PRIORITY_BELOW_NORMAL);
 pThread2->SetThreadPriority(THREAD_PRIORITY_NORMAL);
}

Wenn die Priorität des Hauptthreads zu niedrig wird, reagiert die Anwendung schlicht und einfach nicht mehr auf Benutzereingaben. Dann müssen Sie die Anwendung mit dem Hammer (z.B. Taskmanager) schließen.

Nach diesen einfachen Experimenten haben Sie nun ein praktisches Verständnis, wie man einen oder mehrere Arbeitsthreads in MFC startet und entsprechende Thread-Funktionen erstellt.
 
 

Threaderzeugung mit WinAPI - "back to the roots"

Lassen Sie uns zur Vertiefung noch einen Kurzausflug zur WinAPI-Programmierung (ohne MFC) machen. Dort erfolgt Multithreading sehr ähnlich. Hier ein einfaches Beispiel zur Veranschaulichung und zum Ausprobieren:
 
#include <windows.h>
#include <process.h>  /* _beginthread, _endthread ... */

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;

int Flag = 1;

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
     static TCHAR szAppName[] = TEXT ("Thread Test") ;
     MSG          msg ;
     WNDCLASS     wndclass ;

     wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
     wndclass.lpfnWndProc   = WndProc ;
     wndclass.cbClsExtra    = 0 ;
     wndclass.cbWndExtra    = 0 ;
     wndclass.hInstance     = hInstance ;
     wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
     wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
     wndclass.lpszMenuName  = NULL ;
     wndclass.lpszClassName = szAppName ;

     RegisterClass (&wndclass);
     HWND hwnd = CreateWindow (szAppName, TEXT ("Thread Test"),
                          WS_OVERLAPPEDWINDOW, 100, 100, 300, 100,
                          NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, iCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
     {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
     }
     return msg.wParam ;
}

VOID Thread (PVOID pvoid)
{
  while(Flag)
  {
    Sleep(1000);
    MessageBeep(0);
  }
}

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
     switch (message)
     {
     case WM_LBUTTONDOWN: /* startet Thread */
          Flag = 1;
          _beginthread (Thread, 0, NULL) ;
          return 0 ;

     case WM_RBUTTONDOWN: /* stoppt Thread */
          Flag = 0;
          return 0 ;

    case WM_DESTROY:
          PostQuitMessage (0) ;
          return 0 ;
     }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
}
 

Für _beginthread(...) benötigt man process.h. In diesem Header findet man folgende Deklarationen:

/* function prototypes */
#ifdef  _MT
_CRTIMP unsigned long  __cdecl _beginthread (void (__cdecl *) (void *),
        unsigned, void *);
_CRTIMP void __cdecl _endthread(void);
_CRTIMP unsigned long __cdecl _beginthreadex(void *, unsigned,
        unsigned (__stdcall *) (void *), void *, unsigned, unsigned *);
_CRTIMP void __cdecl _endthreadex(unsigned);
#endif

Bei den Projekteinstellungen für C/C++ müssen Sie unter Code Generation die Laufzeit-Bibliothek auf Multithreaded umstellen, damit das Compiler Flag /MT gesetzt wird, ansonsten gilt _beginthread(...) als nicht deklariert:


 

Wie Sie sehen, nimmt die MFC-Programmierung Ihnen diesbezüglich einige Kleinarbeiten ab.

Auch bei Konsolen kann man die Thread-Programmierung, sinnvoll z.B. für Server, realisieren. Darauf wollen wir hier jedoch nicht eingehen, da dies keine neuen Aspekte bringt.
 
 

Thread-Synchronisierung

Das zentrale Thema bei Multithreadinganwendungen ist die sogenannte Synchronisierung. Das ist die zeitliche Steuerung der Threads. Warum braucht man diese Technik? Stellen Sie sich einfach die Arbeitsteilung im normalen Leben vor und lassen Sie vor Ihrem geistigen Auge folgende Vorgänge gleichzeitig ablaufen:

An einer Kreuzung fahren alle Fahrzeuge gleichzeitig los.
In einer erregten Diskussion sprechen alle zur gleichen Zeit.
Sie essen vom Teller, während der Kellner darauf serviert.
Sie trinken aus dem Glas, in das der Kellner gerade eingießt.
Sie schließen eine Tür, während jemand durchgehen will.
Zwei Personen bedienen gleichzeitig unkoordiniert einen Computer.
Es gibt nur drei gleiche Werkzeuge (z.B. Bohrmaschine), aber zehn Handwerker, die diese gleichzeitig benützen wollen.

In einem Programm sieht dieses Problem z.B. so aus:
Thread1 und Thread 2 schreiben in eine String-Variable, während Thread3 diese liest und ausgibt. Was wird der Benutzer sehen? Den String von Thread 1 oder von Thread 2? Beides ist möglich. Vielleicht kommt es auch zu einem Durcheinander oder gar zu einem Programmabsturz.
Probieren wir es aus:

Erzeugen Sie ein Dialog-Programm mit einem Button für Start ("Thread starten") und einen Button für Stop ("Thread stoppen") und zwei Edit-Feldern. Den Edit-Feldern ordnen Sie bitte mittels Klassenassistent zwei Member-Variablen vom Typ CString zu:
m_strEdit1 und  m_strEdit2. Thread 1 und Thread 2 werden beide in m_strEdit1 schreiben, das dem obere Edit-Feld zugeordnet ist.
Thread 3 liest m_strEdit1 aus, weist es m_strEdit2 zu, das im unteren Edit-Feld erscheint.

Die Header-Datei sollte bitte wie folgt aussehen:
Wir finden dort statische und nicht-statische Member-Funktionen für die drei Arbeitsthreads, die Member-Variablen für die Edit-Felder, die Funktionen OnButtonStart() und OnButtonStop() sowie unsere Variable m_Flag.
 
class CThread001Dlg : public CDialog
{
// Konstruktion
public:
  void thrRun1();
  void thrRun2();
  void thrRun3();
  static UINT thrFunction1 (LPVOID pParam);
  static UINT thrFunction2 (LPVOID pParam);
  static UINT thrFunction3 (LPVOID pParam);

  CThread001Dlg(CWnd* pParent = NULL); // Standard-Konstruktor

// Dialogfelddaten
 //{{AFX_DATA(CThread001Dlg)
  enum { IDD = IDD_THREAD001_DIALOG };
  CString m_strEdit1;
  CString m_strEdit2;
 //}}AFX_DATA

 // Vom Klassenassistenten generierte Überladungen virtueller Funktionen
 //{{AFX_VIRTUAL(CThread001Dlg)
protected:
  virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV-Unterstützung
 //}}AFX_VIRTUAL

// Implementierung
protected:
  HICON m_hIcon;

 // Generierte Message-Map-Funktionen
 //{{AFX_MSG(CThread001Dlg)
  virtual BOOL OnInitDialog();
  afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
  afx_msg void OnPaint();
  afx_msg void OnButtonStart();
  afx_msg void OnButtonStop();
 //}}AFX_MSG
  DECLARE_MESSAGE_MAP()

private:
  int m_Flag;
};

Die Implementierung unserer drei Threads inclusive Start und Stop sollte wie folgt aussehen:
 
void CThread001Dlg::OnButtonStart() 
{
  m_Flag = 1;
  CWinThread* pThread1 = AfxBeginThread (thrFunction1, this);
  CWinThread* pThread2 = AfxBeginThread (thrFunction2, this);
  CWinThread* pThread3 = AfxBeginThread (thrFunction3, this);
}

void CThread001Dlg::OnButtonStop() 
{
  m_Flag = 0;
}

UINT CThread001Dlg::thrFunction1(LPVOID pParam)
{
  CThread001Dlg* pDlg = (CThread001Dlg*) pParam;
  pDlg->thrRun1();

  return 0;
}

UINT CThread001Dlg::thrFunction2(LPVOID pParam)
{
  CThread001Dlg* pDlg = (CThread001Dlg*) pParam;
  pDlg->thrRun2();

  return 0;
}

UINT CThread001Dlg::thrFunction3(LPVOID pParam)
{
  CThread001Dlg* pDlg = (CThread001Dlg*) pParam;
  pDlg->thrRun3();

  return 0;
}

void CThread001Dlg::thrRun1()
{
  while (m_Flag) 
  {
    m_strEdit1  = "Jetzt";
    m_strEdit1 += " schreibt";
    m_strEdit1 += " Arbeitsthread Nr. 1";
  }
}

void CThread001Dlg::thrRun2()
{
  while (m_Flag) 
  {
    m_strEdit1  = "Nun";
    m_strEdit1 += " diktiert";
    m_strEdit1 += " Arbeitsthread Nr. 2";
  }
}

void CThread001Dlg::thrRun3()
{
  while (m_Flag) 
  {
    m_strEdit2 = m_strEdit1;
    UpdateData(FALSE);
  }
}

Nun unternehmen wir folgendes Experiment:
Wir lassen die Threads mehrere Male abwechselnd starten und stoppen, um den von Arbeitsthread 3 ausgelesenen String zu bewundern:

Das sind die geordneten Zustände:

... und hier regiert das kreative Chaos:

Ich hoffe, das klappt bei Ihnen auch.

Die Aufgabe ist klar: Zuerst muß Thread1 oder Thread 2 sich zum Schreibvorgang anmelden, anschließend erhält nur einer der Threads die Freigabe zum Schreiben und zum Schluß meldet dieser sich wieder ordentlich ab. Erst jetzt erhält der wartende Thread 2 die Freigabe zum Schreiben oder der wartende Thread 3 die Freigabe zum Lesen. Entweder schreibt Thread 1 oder Thread 2 in den String, aber nicht beide gleichzeitig, und wenn geschrieben wird, wird nicht gelesen, und umgekehrt. So muß das ablaufen! Wir als Programmierer müssen die Oberhand über die "Taktsteuerung", die man bei Threads "Synchronisation" nennt, behalten. Ansonsten machen die Threads in unserem Programm, was sie wollen.

Wir benötigen einen Mechanismus analog dem Besuch einer Flugzeugtoilette. Die wartenden Personen sind die angemeldeten Threads, das Schloß bzw. Lichtzeichen (grün / rot) der Toilette ist der Verriegelungsmechanismus. Nur einer darf gleichzeitig rein, und dann wird sofort verriegelt.
 
 

Kritische Abschnitte und Mutexe

Beide Hilfsmittel dienen als Verriegelungs-/Freigabemechanismus für den Zugriff mehrerer Threads auf die gleichen Variablen bzw. allgemein Ressourcen. Kritische Abschnitte kann man nur prozessintern einsetzen, während Mutexe auch prozessübergreifend funktionieren. Dafür sind Kritische Abschnitte "schlanker" und "schneller".

Zur Realisierung der kritischen Abschnitte steht in MFC die Klasse CCriticalSection bereit. CCriticalSection kennt nur drei Methoden:
den Konstruktor, Lock() und Unlock().

Zunächst muß man folgenden Header einbinden: #include <afxmt.h>
Wir erzeugen dann einen kritischen Abschnitt, d.h. ein Objekt der MFC-Klasse CCriticalSection. Der Konstruktor benötigt keinen Parameter.
Innerhalb der Funktion von Thread1 erfolgt dann der Lock/Unlock-Mechanismus:

//z.B. Member der Klasse CXXX oder global
CCriticalSection cs;

void CXXX::thrRun1()
{
  cs.Lock();
  //Aktionen
  cs.Unlock();
}

Ein zweiter Thread kann erst dann erfolgreich cs.Lock() ausführen, wenn der erste Thread cs.Unlock() aufgerufen hat. Stellen Sie sich das vor wie bei dem Beispiel der Flugzeugtoilette. Das Türschloß ist cs. Mit cs.Lock() schließt man zu, und mit cs.Unlock() schließt man auf, und in der Zwischenzeit "erledigt man seine Geschäfte". Vor der Tür stehen die anderen Threads und warten, bis ihr eigenes cs.Lock() zum Zuge kommt.

Das realisieren wir sofort. Also was müssen wir machen? Zunächst einen kritischen Abschnitt einfügen. Man kann das als globale Variable oder in unserem Fall auch als private Member-Variable erledigen. Wichtig ist, dass alle Threads darauf zugreifen können:
 
#include <afxmt.h>

/////////////////////////////////////////////////////////////////////////////
// CThread001Dlg Dialogfeld

class CThread001Dlg : public CDialog
{
...
...
private:
 int m_Flag;
 CCriticalSection cs;
};

Jetzt haben wir einen "Schlüssel". Nun müssen wir nur zum richtigen Zeitpunkt öffnen und schließen.
Innerhalb der Thread-Funktionen wenden wir das wie folgt konkret an:
 
void CThread001Dlg::thrRun1()
{
  while (m_Flag) 
  {
    cs.Lock();
    m_strEdit1  = "Jetzt";
    m_strEdit1 += " schreibt";
    m_strEdit1 += " Arbeitsthread Nr. 1";
    cs.Unlock();
  }
}

void CThread001Dlg::thrRun2()
{
  while (m_Flag) 
  {
    cs.Lock();
    m_strEdit1  = "Nun";
    m_strEdit1 += " diktiert";
    m_strEdit1 += " Arbeitsthread Nr. 2";
    cs.Unlock();
  }
}

void CThread001Dlg::thrRun3()
{
  while (m_Flag) 
  {
    cs.Lock();
    m_strEdit2 = m_strEdit1;
    cs.Unlock();
    UpdateData(FALSE);
  }
}

Nun ist endlich Ordnung eingekehrt bei der "quasiparallelen" Verwendung der Variable m_strEdit1. Einfach durch "Einklammern" mit Lock() / Unlock() einen kritischen Abschnitt schaffen und nur einen Thread zur gleichen Zeit seine Arbeit bezüglich dieser Variable erledigen lassen.

Es gibt da noch eine wichtige Feinheit, auf die ich hinweisen möchte. Die Prozessorauslastung unserer Mini-Anwendung liegt bei stolzen 100%. Überprüfen Sie es selbst mit dem Systemmonitor im Zubehör von MS Windows.

Preisfrage: Woran liegt das? Wie können wir dies ändern?

Der entscheidende Punkt ist unsere while-Schleife. Solange m_Flag gesetzt ist, laufen unsere Threads sozusagen Amok. Also gönnen wir ihnen nach jeder Aktion eine Pause, damit die CPU sich auch noch um den Rest der Welt kümmern kann. Wie wäre es mit einer Millisekunde? Ist doch ausreichend Pause für einen Thread? Threads haben keinen Betriebsrat.
 
void CThread001Dlg::thrRun1()
{
  while (m_Flag) 
  {
    cs.Lock();
    //Aktion
    cs.Unlock();
    Sleep(1);
  }
}

void CThread001Dlg::thrRun2()
{
  while (m_Flag) 
  {
    cs.Lock();
    //Aktion
    cs.Unlock();
    Sleep(1);
  }
}

void CThread001Dlg::thrRun3()
{
  while (m_Flag) 
  {
    cs.Lock();
    //Aktion
    cs.Unlock();
    Sleep(1);
  }
}
 

Wichtig ist, dass alle Threads mal Pause machen. Ein arbeitswütiger Thread kann den Prozessor alleine bei Laune halten.

Nun stellen Sie hoffentlich zwei Dinge fest:
1) Die Anwendung läuft viel schneller ab!
2) Die Prozessorauslastung liegt bei ca. 20%.

Während Sleep(...) verbraucht ein Thread keine Rechenleistung,  sondern macht wirklich Pause.

Nachdem wir nun diese zwei wesentlichen Prinzipien untersucht haben, wenden wir uns einer komplexeren Anwendung zu. Da Multithreading sein Vorbild in unserer arbeitsteiligen Welt hat, verzichten wir auf weitere Mini-Beispiele und wenden uns sofort einer virtuellen Fertigungsstraße zu:

Das Modell unserer "virtuellen" Fertigungsstraße funktioniert wie folgt:

Der Rohstoffeinkauf beschafft 5 Einzelteile für die Produktion. Die Vormontage fertigt aus Teil 1 und Teil 2 das Zwischenprodukt Kombi A und aus Teil 3 und Teil 4 das Zwischenprodukt Kombi B. Die Endmontage produziert Endprodukt 1 aus Kombi A und Kombi B sowie Endprodukt 2 aus Kombi B und Teil 5. Der Vertrieb bringt die beiden Endprodukte zu den Abnehmern. Die Endprodukte werden uns aktuell aus den Händen gerissen. Das Simulationsmodell soll helfen, Kapazitätsengpässe ("bottle necks") zu finden und gleichzeitig die Vorräte zu optimieren.

Für das zu erstellende Programm werden die jeweiligen Einzeltätigkeiten von Rohstoffeinkauf, Vormontage, Endmontage und Vertrieb in einzelnen Arbeitsthreads abgebildet. Der Bestand der Ressourcen wird als Zahlenvariablen vom Typ UINT dargestellt, auf die die einzelnen Threads zugreifen. Wenn Thread_Vormontage_KombiA abläuft, wird Bestand_Teil1 und Bestand_Teil2 um eins erniedrigt, während sich der Bestand_KombiA um eins erhöht. Wir müssen dafür sorgen, dass z.B. nicht Threads der Vormontage und Endmontage gleichzeitig auf die Bestandsvariable der Kombi A zugreifen können (Vormontage will erhöhen und Endmontage will erniedrigen). Hier muß ein kritischer Abschnitt (sprich ein "Schloß") dafür sorgen, dass nur jeweils ein Thread den Bestand von Kombi A  verändert. Wir werden daher eine CCriticalSection cs_KombiA erzeugen, die hier als Wächter für den Zugriff von Thread_Vormontage_KombiA und Thread_Endmontage_Endprodukt1 auf den Bestand_KombiA fungiert.

Der Hauptthread hat die Aufgabe, ständig den aktuellen Gesamtzustand der Produktion zu visualisieren. Wir werden nur einen Start- und Stop-Button schaffen. Alles andere soll automatisch ablaufen.

Nun noch einige Details unseres Produktionsmodells:
Die Beschaffung von Teilen erfolgt im 100er Pack und benötigt 60 Sekunden.
Die Vormontage von Kombi A benötigt 3,0 Sekunden.
Die Vormontage von Kombi B benötigt 2,0 Sekunden.
Die Endmontage von Endprodukt 1 benötigt 4,0 Sekunden.
Die Endmontage von Endprodukt 2 benötigt 5,0 Sekunden.
Der Vertrieb erfolgt im 10er Pack und benötigt zur Bestandserniedrigung im Endprodukte-Lager jeweils 15 Sekunden.
In den Lagern darf nur jeweils ein Zu- oder Abgang zur gleichen Zeit erfolgen.

Diese Fertigunszeiten bilden wir innerhalb des Threads auf einfache Weise mittels Sleep( Millisekunden ) ab. In dieser Zeit verbraucht der Thread keine Rechenzeit.

Verfolgen wir die Fertigung einer KombiA aus Teil 1 und Teil 2 im Programm:
 
class CThread001Dlg : public CDialog
{
// Konstruktion
public:
 void thr_KombiA();
 //...
 static UINT thrFunction_KombiA (LPVOID pParam);
 //...
 UINT m_Bestand_Teil1;  //Klassenassistent
 UINT m_Bestand_Teil2;  //Klassenassistent
 //...
 UINT m_Bestand_KombiA; //Klassenassistent
 //...

private:
 int m_Flag;

 CCriticalSection  m_cs_Teil1, m_cs_Teil2, m_cs_Teil3, m_cs_Teil4, m_cs_Teil5,
            m_cs_KombiA, m_cs_KombiB,
                   m_cs_Endprodukt1, m_cs_Endprodukt2;
 //...
 CWinThread* pThread_Vormontage_KombiA
 //...
};

void CThread001Dlg::OnButtonStart()
{
 SetTimer(1,500,NULL); //Timer für die Visualisierung
 m_Flag = 1;
 //...
 pThread_Vormontage_KombiA = AfxBeginThread (thrFunction_KombiA, this);
 //...
}

void CThread001Dlg::OnButtonStop()
{
 KillTimer(1);
 m_Flag = 0;
}

void CThread001Dlg::OnTimer(UINT nIDEvent)
{
 UpdateData(FALSE); //Daten -> Felder

 CDialog::OnTimer(nIDEvent);
}

UINT CThread001Dlg::thrFunction_KombiA (LPVOID pParam)
{
  CThread001Dlg* pDlg = (CThread001Dlg*) pParam;
  pDlg->thr_KombiA();

  return 0;
}

void CThread001Dlg::thr_KombiA()
{
  while (m_Flag)
  {
    Warteschleife();//ansonsten benötigt das Programm 100% Prozessorlast

    while ( (m_Bestand_Teil1 > 0) && (m_Bestand_Teil2 > 0) )
    {
     m_Checkbutton_Vormontage_KombiA.SetCheck(TRUE); //Arbeitsvorgang startet

     m_cs_Teil1.Lock(); //Teil 1 wird aus dem Vorrat genommen
     m_Bestand_Teil1--;
     m_cs_Teil1.Unlock();

     m_cs_Teil2.Lock(); //Teil 2 wird aus dem Vorrat genommen
     m_Bestand_Teil2--;
  m_cs_Teil2.Unlock();

     Sleep(3000); //Fertigungszeit

     m_cs_KombiA.Lock(); //Kombi A wird in den Vorrat genommen
     m_Bestand_KombiA++;
  m_cs_KombiA.Unlock();

     m_Checkbutton_Vormontage_KombiA.SetCheck(FALSE); //Arbeitsvorgang beendet
    }
  }
}
 

void CThread001Dlg::Warteschleife()
{
  Sleep(1);
}

Wir benutzen für den Zugriff auf die Bestandsdaten individuelle CCriticalSections. Damit ist sicher gestellt, dass nicht mehrere Threads simultan auf die gleiche Variable zugreifen. Jeder Zugriff wird in jedem Thread durch eine Lock()-Unlock()-Klammer eingeschlossen. Das ist alles.

Hinweis:
Wichtig ist auch unsere kleine Member-Funktion "Warteschleife" innerhalb der äußeren while-Schleife, die dem "Rest des Computers" zumindest eine Millisekunde gewährt. Ohne diese Warteschleifen innerhalb der Threadfunktionen sind diese ständig damit beschäftigt, die Bestandsdaten abzufragen. Ein wichtiges Detail. Probieren Sie es ohne Warteschleife aus und beachten Sie die 100%-Prozessorauslastung. Mit diesen Warteschleifen liegt der CPU-Bedarf des Programms bei ca. 5 %. Das können Sie selbst mit dem "Systemmonitor" (MS Windows-Zubehör) überprüfen und optimieren.

Unsere kleine Wirtschaftssimulation sieht optisch in der Basisversion wie folgt aus:

Die Produktion ist angelaufen.
 

Der Rohstoffeinkauf hat neue Teile besorgt, der Versand verschickt die ersten Endprodukte.
 

Der Process-Viewer zeigt uns an, dass wirklich 12 Threads gleichzeitig ablaufen. Diese sind:


Nachfolgend finden Sie zum vertieften Studium der Thread-Synchronisierung das ganze Programm:
 
//StdAfx.h

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

#define VC_EXTRALEAN  // Selten verwendete Teile der Windows-Header nicht einbinden

#include <afxwin.h>         // MFC-Kern- und -Standardkomponenten
#include <afxext.h>         // MFC-Erweiterungen
#include <afxdisp.h>        // MFC Automatisierungsklassen
#include <afxdtctl.h>  // MFC-Unterstützung für allgemeine Steuerelemente von Internet Explorer 4
#ifndef _AFX_NO_AFXCMN_SUPPORT
#include <afxcmn.h>   // MFC-Unterstützung für gängige Windows-Steuerelemente
#endif // _AFX_NO_AFXCMN_SUPPORT

#include <Afxmt.h>

//{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ fügt unmittelbar vor der vorhergehenden Zeile zusätzliche Deklarationen ein.

#endif // !defined(AFX_STDAFX_H__65E8E946_9137_11D6_A393_004033E1CE3C__INCLUDED_)
 
 

//Resource.h

//{{NO_DEPENDENCIES}}
// Microsoft Developer Studio generated include file.
// Used by Thread001.rc
//
#define IDM_ABOUTBOX                    0x0010
#define IDD_ABOUTBOX                    100
#define IDS_ABOUTBOX                    101
#define IDD_THREAD001_DIALOG            102
#define IDR_MAINFRAME                   128
#define IDC_BUTTONSTART                 1000
#define IDC_BUTTONSTOP                  1001
#define IDC_CHECK1                      1002
#define IDC_CHECK2                      1003
#define IDC_CHECK3                      1004
#define IDC_CHECK4                      1005
#define IDC_CHECK5                      1006
#define IDC_EDIT1                       1007
#define IDC_EDIT2                       1008
#define IDC_EDIT3                       1009
#define IDC_EDIT4                       1010
#define IDC_EDIT5                       1011
#define IDC_CHECK6                      1012
#define IDC_CHECK7                      1013
#define IDC_EDIT6                       1014
#define IDC_EDIT7                       1015
#define IDC_CHECK8                      1016
#define IDC_CHECK9                      1017
#define IDC_EDIT8                       1018
#define IDC_EDIT9                       1019
#define IDC_CHECK10                     1020
#define IDC_CHECK11                     1021

// Next default values for new objects
// 
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE        129
#define _APS_NEXT_COMMAND_VALUE         40001
#define _APS_NEXT_CONTROL_VALUE         1015
#define _APS_NEXT_SYMED_VALUE           101
#endif
#endif
 
 

//Thread001.h : Haupt-Header-Datei für die Anwendung THREAD001

#if !defined(AFX_THREAD001_H__65E8E942_9137_11D6_A393_004033E1CE3C__INCLUDED_)
#define AFX_THREAD001_H__65E8E942_9137_11D6_A393_004033E1CE3C__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

#ifndef __AFXWIN_H__
 #error include 'stdafx.h' before including this file for PCH
#endif

#include "resource.h"  // Hauptsymbole

/////////////////////////////////////////////////////////////////////////////
// CThread001App:
// Siehe Thread001.cpp für die Implementierung dieser Klasse
//

class CThread001App : public CWinApp
{
public:
 CThread001App();

// Überladungen
 // Vom Klassenassistenten generierte Überladungen virtueller Funktionen
 //{{AFX_VIRTUAL(CThread001App)
 public:
 virtual BOOL InitInstance();
 //}}AFX_VIRTUAL

// Implementierung

 //{{AFX_MSG(CThread001App)
  // HINWEIS - An dieser Stelle werden Member-Funktionen vom Klassen-Assistenten eingefügt und entfernt.
  //    Innerhalb dieser generierten Quelltextabschnitte NICHTS VERÄNDERN!
 //}}AFX_MSG
 DECLARE_MESSAGE_MAP()
};

/////////////////////////////////////////////////////////////////////////////

//{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ fügt unmittelbar vor der vorhergehenden Zeile zusätzliche Deklarationen ein.

#endif // !defined(AFX_THREAD001_H__65E8E942_9137_11D6_A393_004033E1CE3C__INCLUDED_)
 
 

// Thread001Dlg.h : Header-Datei

#if !defined(AFX_THREAD001DLG_H__65E8E944_9137_11D6_A393_004033E1CE3C__INCLUDED_)
#define AFX_THREAD001DLG_H__65E8E944_9137_11D6_A393_004033E1CE3C__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

/////////////////////////////////////////////////////////////////////////////
// CThread001Dlg Dialogfeld

class CThread001Dlg : public CDialog
{
// Konstruktion
public:
 void Warteschleife(); 
 void thr_Teil1();
 static UINT thrFunction_Teil1 (LPVOID pParam);
 void thr_Teil2();
 static UINT thrFunction_Teil2 (LPVOID pParam);
 void thr_Teil3();
 static UINT thrFunction_Teil3 (LPVOID pParam);
 void thr_Teil4();
 static UINT thrFunction_Teil4 (LPVOID pParam);
 void thr_Teil5();
 static UINT thrFunction_Teil5 (LPVOID pParam);

 void thr_KombiA();
 static UINT thrFunction_KombiA (LPVOID pParam);
 void thr_KombiB();
 static UINT thrFunction_KombiB (LPVOID pParam);

 void thr_Endprodukt1();
 static UINT thrFunction_Endprodukt1 (LPVOID pParam);
 void thr_Endprodukt2();
 static UINT thrFunction_Endprodukt2 (LPVOID pParam);

 void thr_VertriebEndprodukt1();
 static UINT thrFunction_VertriebEndprodukt1 (LPVOID pParam);
 void thr_VertriebEndprodukt2();
 static UINT thrFunction_VertriebEndprodukt2 (LPVOID pParam);

 CThread001Dlg(CWnd* pParent = NULL); // Standard-Konstruktor

// Dialogfelddaten
 //{{AFX_DATA(CThread001Dlg)
 enum { IDD = IDD_THREAD001_DIALOG };
 CButton m_ButtonStop;
 CButton m_ButtonStart;
 CButton m_Checkbutton_Vertrieb_Endprodukt2;
 CButton m_Checkbutton_Vertrieb_Endprodukt1;
 CButton m_Checkbutton_Endmontage_Endprodukt2;
 CButton m_Checkbutton_Endmontage_Endprodukt1;
 CButton m_Checkbutton_Vormontage_KombiB;
 CButton m_Checkbutton_Vormontage_KombiA;
 CButton m_Checkbutton_Beschaffung_Teil5;
 CButton m_Checkbutton_Beschaffung_Teil4;
 CButton m_Checkbutton_Beschaffung_Teil3;
 CButton m_Checkbutton_Beschaffung_Teil2;
 CButton m_Checkbutton_Beschaffung_Teil1;
 UINT m_Bestand_Teil1;
 UINT m_Bestand_Teil2;
 UINT m_Bestand_Teil3;
 UINT m_Bestand_Teil4;
 UINT m_Bestand_Teil5;
 UINT m_Bestand_KombiA;
 UINT m_Bestand_KombiB;
 UINT m_Bestand_Endprodukt1;
 UINT m_Bestand_Endprodukt2;
 //}}AFX_DATA

 // Vom Klassenassistenten generierte Überladungen virtueller Funktionen
 //{{AFX_VIRTUAL(CThread001Dlg)
 protected:
 virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV-Unterstützung
 //}}AFX_VIRTUAL

// Implementierung
protected:
 HICON m_hIcon;

 // Generierte Message-Map-Funktionen
 //{{AFX_MSG(CThread001Dlg)
 virtual BOOL OnInitDialog();
 afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
 afx_msg void OnPaint();
 afx_msg void OnButtonStart();
 afx_msg void OnButtonStop();
 afx_msg void OnTimer(UINT nIDEvent);
 //}}AFX_MSG
 DECLARE_MESSAGE_MAP()
private:
 int m_Flag;

 CCriticalSection m_cs_Teil1, m_cs_Teil2, m_cs_Teil3, m_cs_Teil4, m_cs_Teil5,
                  m_cs_KombiA, m_cs_KombiB, 
                  m_cs_Endprodukt1, m_cs_Endprodukt2;

 CWinThread* pThread_Beschaffung_Teil1;
 CWinThread* pThread_Beschaffung_Teil2;
 CWinThread* pThread_Beschaffung_Teil3;
 CWinThread* pThread_Beschaffung_Teil4;
 CWinThread* pThread_Beschaffung_Teil5;

 CWinThread* pThread_Vormontage_KombiA;
 CWinThread* pThread_Vormontage_KombiB;

 CWinThread* pThread_Endmontage_Endprodukt1;
 CWinThread* pThread_Endmontage_Endprodukt2;

 CWinThread* pThread_Vertrieb_Endprodukt1;
 CWinThread* pThread_Vertrieb_Endprodukt2;
};

//{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ fügt unmittelbar vor der vorhergehenden Zeile zusätzliche Deklarationen ein.

#endif // !defined(AFX_THREAD001DLG_H__65E8E944_9137_11D6_A393_004033E1CE3C__INCLUDED_)
 
 

// stdafx.cpp : Quelltextdatei, die nur die Standard-Includes einbindet
// Thread001.pch ist die vorcompilierte Header-Datei
// stdafx.obj enthält die vorcompilierte Typinformation

#include "stdafx.h"
 
 

// Thread001.cpp : Legt das Klassenverhalten für die Anwendung fest.

#include "stdafx.h"
#include "Thread001.h"
#include "Thread001Dlg.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

/////////////////////////////////////////////////////////////////////////////
// CThread001App

BEGIN_MESSAGE_MAP(CThread001App, CWinApp)
 //{{AFX_MSG_MAP(CThread001App)
  // HINWEIS - Hier werden Mapping-Makros vom Klassen-Assistenten eingefügt und entfernt.
  //    Innerhalb dieser generierten Quelltextabschnitte NICHTS VERÄNDERN!
 //}}AFX_MSG
 ON_COMMAND(ID_HELP, CWinApp::OnHelp)
END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////
// CThread001App Konstruktion

CThread001App::CThread001App()
{
 // ZU ERLEDIGEN: Hier Code zur Konstruktion einfügen
 // Alle wichtigen Initialisierungen in InitInstance platzieren
}

/////////////////////////////////////////////////////////////////////////////
// Das einzige CThread001App-Objekt

CThread001App theApp;
 

/////////////////////////////////////////////////////////////////////////////
// CThread001App Initialisierung

BOOL CThread001App::InitInstance()
{
 AfxEnableControlContainer();

 // Standardinitialisierung
 // Wenn Sie diese Funktionen nicht nutzen und die Größe Ihrer fertigen 
 //  ausführbaren Datei reduzieren wollen, sollten Sie die nachfolgenden
 //  spezifischen Initialisierungsroutinen, die Sie nicht benötigen, entfernen.

#ifdef _AFXDLL
 Enable3dControls();   // Diese Funktion bei Verwendung von MFC in gemeinsam genutzten DLLs aufrufen
#else
 Enable3dControlsStatic(); // Diese Funktion bei statischen MFC-Anbindungen aufrufen
#endif

 CThread001Dlg dlg;
 m_pMainWnd = &dlg;
 int nResponse = dlg.DoModal();
 if (nResponse == IDOK)
 {
  // ZU ERLEDIGEN: Fügen Sie hier Code ein, um ein Schließen des
  //  Dialogfelds über OK zu steuern
 }
 else if (nResponse == IDCANCEL)
 {
  // ZU ERLEDIGEN: Fügen Sie hier Code ein, um ein Schließen des
  //  Dialogfelds über "Abbrechen" zu steuern
 }

 // Da das Dialogfeld geschlossen wurde, FALSE zurückliefern, so dass wir die
 //  Anwendung verlassen, anstatt das Nachrichtensystem der Anwendung zu starten.
 return FALSE;
}
 
 
 

// Thread001Dlg.cpp : Implementierungsdatei
//

#include "stdafx.h"
#include "Thread001.h"
#include "Thread001Dlg.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

/////////////////////////////////////////////////////////////////////////////
// CAboutDlg-Dialogfeld für Anwendungsbefehl "Info"

class CAboutDlg : public CDialog
{
public:
 CAboutDlg();

// Dialogfelddaten
 //{{AFX_DATA(CAboutDlg)
 enum { IDD = IDD_ABOUTBOX };
 //}}AFX_DATA

 // Vom Klassenassistenten generierte Überladungen virtueller Funktionen
 //{{AFX_VIRTUAL(CAboutDlg)
 protected:
 virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV-Unterstützung
 //}}AFX_VIRTUAL

// Implementierung
protected:
 //{{AFX_MSG(CAboutDlg)
 //}}AFX_MSG
 DECLARE_MESSAGE_MAP()
};

CAboutDlg::CAboutDlg() : CDialog(CAboutDlg::IDD)
{
 //{{AFX_DATA_INIT(CAboutDlg)
 //}}AFX_DATA_INIT
}

void CAboutDlg::DoDataExchange(CDataExchange* pDX)
{
 CDialog::DoDataExchange(pDX);
 //{{AFX_DATA_MAP(CAboutDlg)
 //}}AFX_DATA_MAP
}

BEGIN_MESSAGE_MAP(CAboutDlg, CDialog)
 //{{AFX_MSG_MAP(CAboutDlg)
  // Keine Nachrichten-Handler
 //}}AFX_MSG_MAP
END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////
// CThread001Dlg Dialogfeld

CThread001Dlg::CThread001Dlg(CWnd* pParent /*=NULL*/)
 : CDialog(CThread001Dlg::IDD, pParent)
{
 //{{AFX_DATA_INIT(CThread001Dlg)
 m_Bestand_Teil1 = 0;
 m_Bestand_Teil2 = 0;
 m_Bestand_Teil3 = 0;
 m_Bestand_Teil4 = 0;
 m_Bestand_Teil5 = 0;
 m_Bestand_KombiA = 0;
 m_Bestand_KombiB = 0;
 m_Bestand_Endprodukt1 = 0;
 m_Bestand_Endprodukt2 = 0;
 //}}AFX_DATA_INIT
 // Beachten Sie, dass LoadIcon unter Win32 keinen nachfolgenden DestroyIcon-Aufruf benötigt
 m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}

void CThread001Dlg::DoDataExchange(CDataExchange* pDX)
{
 CDialog::DoDataExchange(pDX);
 //{{AFX_DATA_MAP(CThread001Dlg)
 DDX_Control(pDX, IDC_BUTTONSTOP, m_ButtonStop);
 DDX_Control(pDX, IDC_BUTTONSTART, m_ButtonStart);
 DDX_Control(pDX, IDC_CHECK11, m_Checkbutton_Vertrieb_Endprodukt2);
 DDX_Control(pDX, IDC_CHECK10, m_Checkbutton_Vertrieb_Endprodukt1);
 DDX_Control(pDX, IDC_CHECK9, m_Checkbutton_Endmontage_Endprodukt2);
 DDX_Control(pDX, IDC_CHECK8, m_Checkbutton_Endmontage_Endprodukt1);
 DDX_Control(pDX, IDC_CHECK7, m_Checkbutton_Vormontage_KombiB);
 DDX_Control(pDX, IDC_CHECK6, m_Checkbutton_Vormontage_KombiA);
 DDX_Control(pDX, IDC_CHECK5, m_Checkbutton_Beschaffung_Teil5);
 DDX_Control(pDX, IDC_CHECK4, m_Checkbutton_Beschaffung_Teil4);
 DDX_Control(pDX, IDC_CHECK3, m_Checkbutton_Beschaffung_Teil3);
 DDX_Control(pDX, IDC_CHECK2, m_Checkbutton_Beschaffung_Teil2);
 DDX_Control(pDX, IDC_CHECK1, m_Checkbutton_Beschaffung_Teil1);
 DDX_Text(pDX, IDC_EDIT1, m_Bestand_Teil1);
 DDX_Text(pDX, IDC_EDIT2, m_Bestand_Teil2);
 DDX_Text(pDX, IDC_EDIT3, m_Bestand_Teil3);
 DDX_Text(pDX, IDC_EDIT4, m_Bestand_Teil4);
 DDX_Text(pDX, IDC_EDIT5, m_Bestand_Teil5);
 DDX_Text(pDX, IDC_EDIT6, m_Bestand_KombiA);
 DDX_Text(pDX, IDC_EDIT7, m_Bestand_KombiB);
 DDX_Text(pDX, IDC_EDIT8, m_Bestand_Endprodukt1);
 DDX_Text(pDX, IDC_EDIT9, m_Bestand_Endprodukt2);
 //}}AFX_DATA_MAP
}

BEGIN_MESSAGE_MAP(CThread001Dlg, CDialog)
 //{{AFX_MSG_MAP(CThread001Dlg)
 ON_WM_SYSCOMMAND()
 ON_WM_PAINT()
 ON_BN_CLICKED(IDC_BUTTONSTART, OnButtonStart)
 ON_BN_CLICKED(IDC_BUTTONSTOP, OnButtonStop)
 ON_WM_TIMER()
 //}}AFX_MSG_MAP
END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////
// CThread001Dlg Nachrichten-Handler

BOOL CThread001Dlg::OnInitDialog()
{
 CDialog::OnInitDialog();

 // Hinzufügen des Menübefehls "Info..." zum Systemmenü.

 // IDM_ABOUTBOX muss sich im Bereich der Systembefehle befinden.
 ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
 ASSERT(IDM_ABOUTBOX < 0xF000);

 CMenu* pSysMenu = GetSystemMenu(FALSE);
 if (pSysMenu != NULL)
 {
  CString strAboutMenu;
  strAboutMenu.LoadString(IDS_ABOUTBOX);
  if (!strAboutMenu.IsEmpty())
  { 
   pSysMenu->AppendMenu(MF_SEPARATOR);
   pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
  }
 }

 SetIcon(m_hIcon, TRUE);   // Großes Symbol verwenden
 SetIcon(m_hIcon, FALSE);  // Kleines Symbol verwenden

 //Produktions-Hauptschalter ( 0 = off, 1 = on )
 m_Flag = 0;

 //Bestandsmengen (hier: Stückzahl)
 m_Bestand_Teil1       = 50;
 m_Bestand_Teil2       = 50;
 m_Bestand_Teil3       = 50;
 m_Bestand_Teil4       = 50;
 m_Bestand_Teil5       = 50;
 m_Bestand_KombiA      =  0;
 m_Bestand_KombiB      =  0;
 m_Bestand_Endprodukt1 =  0;
 m_Bestand_Endprodukt2 =  0;

 UpdateData(FALSE); //Daten -> Felder

 return TRUE;  // Geben Sie TRUE zurück, außer ein Steuerelement soll den Fokus erhalten
}

void CThread001Dlg::OnSysCommand(UINT nID, LPARAM lParam)
{
 if ((nID & 0xFFF0) == IDM_ABOUTBOX)
 {
  CAboutDlg dlgAbout;
  dlgAbout.DoModal();
 }
 else
 {
  CDialog::OnSysCommand(nID, lParam);
 }
}

void CThread001Dlg::OnPaint() 
{
  CDialog::OnPaint();
}

void CThread001Dlg::OnButtonStart() 
{
 SetTimer(1,500,NULL); //Timer für die Visualisierung
 m_Flag = 1;
 m_ButtonStart.EnableWindow(FALSE);
 m_ButtonStop.EnableWindow(TRUE);

 pThread_Beschaffung_Teil1 = AfxBeginThread (thrFunction_Teil1, this);
 pThread_Beschaffung_Teil2 = AfxBeginThread (thrFunction_Teil2, this);
 pThread_Beschaffung_Teil3 = AfxBeginThread (thrFunction_Teil3, this);
 pThread_Beschaffung_Teil4 = AfxBeginThread (thrFunction_Teil4, this);
 pThread_Beschaffung_Teil5 = AfxBeginThread (thrFunction_Teil5, this);

 pThread_Vormontage_KombiA = AfxBeginThread (thrFunction_KombiA, this);
 pThread_Vormontage_KombiB = AfxBeginThread (thrFunction_KombiB, this);

 pThread_Endmontage_Endprodukt1 = AfxBeginThread (thrFunction_Endprodukt1, this);
 pThread_Endmontage_Endprodukt2 = AfxBeginThread (thrFunction_Endprodukt2, this);

 pThread_Vertrieb_Endprodukt1 = AfxBeginThread (thrFunction_VertriebEndprodukt1, this);
 pThread_Vertrieb_Endprodukt2 = AfxBeginThread (thrFunction_VertriebEndprodukt2, this);

}

void CThread001Dlg::OnButtonStop() 
{
 m_ButtonStart.EnableWindow(TRUE);
 m_ButtonStop.EnableWindow(FALSE);
 m_Flag = 0;
 KillTimer(1);
}

void CThread001Dlg::OnTimer(UINT nIDEvent) 
{
 UpdateData(FALSE); //Daten -> Felder

 CDialog::OnTimer(nIDEvent);
}

////////////////////////////////////////
//Statische Thread-Funktionen

UINT CThread001Dlg::thrFunction_Teil1 (LPVOID pParam)
{
 CThread001Dlg* pDlg = (CThread001Dlg*) pParam;
    pDlg->thr_Teil1();
    return 0;
}
 

UINT CThread001Dlg::thrFunction_Teil2 (LPVOID pParam)
{
 CThread001Dlg* pDlg = (CThread001Dlg*) pParam;
    pDlg->thr_Teil2();
    return 0;
}
 

UINT CThread001Dlg::thrFunction_Teil3 (LPVOID pParam)
{
 CThread001Dlg* pDlg = (CThread001Dlg*) pParam;
    pDlg->thr_Teil3();
    return 0;
}

UINT CThread001Dlg::thrFunction_Teil4 (LPVOID pParam)
{
 CThread001Dlg* pDlg = (CThread001Dlg*) pParam;
    pDlg->thr_Teil4();
    return 0;
}

UINT CThread001Dlg::thrFunction_Teil5 (LPVOID pParam)
{
 CThread001Dlg* pDlg = (CThread001Dlg*) pParam;
    pDlg->thr_Teil5();
    return 0;
}

UINT CThread001Dlg::thrFunction_KombiA (LPVOID pParam)
{
 CThread001Dlg* pDlg = (CThread001Dlg*) pParam;
    pDlg->thr_KombiA();
    return 0;
}

UINT CThread001Dlg::thrFunction_KombiB (LPVOID pParam)
{
 CThread001Dlg* pDlg = (CThread001Dlg*) pParam;
    pDlg->thr_KombiB();
    return 0;
}

UINT CThread001Dlg::thrFunction_Endprodukt1 (LPVOID pParam)
{
 CThread001Dlg* pDlg = (CThread001Dlg*) pParam;
    pDlg->thr_Endprodukt1();
    return 0;
}

UINT CThread001Dlg::thrFunction_Endprodukt2 (LPVOID pParam)
{
 CThread001Dlg* pDlg = (CThread001Dlg*) pParam;
    pDlg->thr_Endprodukt2();
    return 0;
}

UINT CThread001Dlg::thrFunction_VertriebEndprodukt1 (LPVOID pParam)
{
 CThread001Dlg* pDlg = (CThread001Dlg*) pParam;
    pDlg->thr_VertriebEndprodukt1();
    return 0;
}

UINT CThread001Dlg::thrFunction_VertriebEndprodukt2 (LPVOID pParam)
{
 CThread001Dlg* pDlg = (CThread001Dlg*) pParam;
    pDlg->thr_VertriebEndprodukt2();
    return 0;
}

////////////////////////////////////////
//Nicht-Statische Thread-Funktionen

void CThread001Dlg::thr_Teil1()
{
  while (m_Flag) 
  {
      Warteschleife();
   while (m_Bestand_Teil1<40)
   {
   m_Checkbutton_Beschaffung_Teil1.SetCheck(TRUE); //Beschaffungssvorgang startet
   Sleep(60000); //Beschaffungszeit
   m_cs_Teil1.Lock(); //Teil 1 wird in den Vorrat genommen
   m_Bestand_Teil1 = m_Bestand_Teil1+100;
   m_cs_Teil1.Unlock();
   m_Checkbutton_Beschaffung_Teil1.SetCheck(FALSE); //Arbeitsvorgang beendet
   }
  }
}

void CThread001Dlg::thr_Teil2()
{
  while (m_Flag)
  {
      Warteschleife();
   while (m_Bestand_Teil2<40)
   {
   m_Checkbutton_Beschaffung_Teil2.SetCheck(TRUE); //Beschaffungssvorgang startet
   Sleep(60000); //Beschaffungszeit
   m_cs_Teil2.Lock(); //Teil 2 wird in den Vorrat genommen
   m_Bestand_Teil2 = m_Bestand_Teil2+100;
   m_cs_Teil2.Unlock();
   m_Checkbutton_Beschaffung_Teil2.SetCheck(FALSE); //Arbeitsvorgang beendet
   }
  }
}

void CThread001Dlg::thr_Teil3()
{
  while (m_Flag)
  {
      Warteschleife();
   while (m_Bestand_Teil3<40)
   {
   m_Checkbutton_Beschaffung_Teil3.SetCheck(TRUE); //Beschaffungssvorgang startet
   Sleep(60000); //Beschaffungszeit
   m_cs_Teil3.Lock(); //Teil 3 wird in den Vorrat genommen
   m_Bestand_Teil3 = m_Bestand_Teil3+100;
   m_cs_Teil3.Unlock();
   m_Checkbutton_Beschaffung_Teil3.SetCheck(FALSE); //Arbeitsvorgang beendet
   }
  }
}

void CThread001Dlg::thr_Teil4()
{
  while (m_Flag) 
  {
      Warteschleife();
   while (m_Bestand_Teil4<40)
   {
   m_Checkbutton_Beschaffung_Teil4.SetCheck(TRUE); //Beschaffungssvorgang startet
   Sleep(60000); //Beschaffungszeit
   m_cs_Teil4.Lock(); //Teil 4 wird in den Vorrat genommen
   m_Bestand_Teil4 = m_Bestand_Teil4+100;
   m_cs_Teil4.Unlock();
   m_Checkbutton_Beschaffung_Teil4.SetCheck(FALSE); //Arbeitsvorgang beendet
   }
  }
}

void CThread001Dlg::thr_Teil5()
{
  while (m_Flag)
  {
      Warteschleife();
   while (m_Bestand_Teil5<40)
   {
   m_Checkbutton_Beschaffung_Teil5.SetCheck(TRUE); //Beschaffungssvorgang startet
   Sleep(60000); //Beschaffungszeit
   m_cs_Teil5.Lock(); //Teil 5 wird in den Vorrat genommen
   m_Bestand_Teil5 = m_Bestand_Teil5+100;
   m_cs_Teil5.Unlock();
   m_Checkbutton_Beschaffung_Teil5.SetCheck(FALSE); //Arbeitsvorgang beendet
   }
  }
}

void CThread001Dlg::thr_KombiA()
{
  while (m_Flag)
  {
      Warteschleife();
   while ( (m_Bestand_Teil1>0) && (m_Bestand_Teil2>0) )
   {
   m_Checkbutton_Vormontage_KombiA.SetCheck(TRUE); //Arbeitsvorgang startet
   m_cs_Teil1.Lock(); //Teil 1 wird aus dem Vorrat genommen
   m_Bestand_Teil1--;
   m_cs_Teil1.Unlock();
   m_cs_Teil2.Lock(); //Teil 2 wird aus dem Vorrat genommen
   m_Bestand_Teil2--;
   m_cs_Teil2.Unlock();
   Sleep(3000); //Fertigungszeit
   m_cs_KombiA.Lock(); //Kombi A wird in den Vorrat genommen
   m_Bestand_KombiA++;
   m_cs_KombiA.Unlock();
   m_Checkbutton_Vormontage_KombiA.SetCheck(FALSE); //Arbeitsvorgang beendet
   }
  }
}

void CThread001Dlg::thr_KombiB()
{
  while (m_Flag)
  {
      Warteschleife();
   while ( (m_Bestand_Teil3>0) && (m_Bestand_Teil4>0) )
   {
   m_Checkbutton_Vormontage_KombiB.SetCheck(TRUE); //Arbeitsvorgang startet
   m_cs_Teil3.Lock(); //Teil 3 wird aus dem Vorrat genommen
   m_Bestand_Teil3--;
   m_cs_Teil3.Unlock();
   m_cs_Teil4.Lock(); //Teil 4 wird aus dem Vorrat genommen
   m_Bestand_Teil4--;
   m_cs_Teil4.Unlock();
   Sleep(2000); //Fertigungszeit
   m_cs_KombiB.Lock(); //Kombi B wird in den Vorrat genommen
   m_Bestand_KombiB++;
   m_cs_KombiB.Unlock();
   m_Checkbutton_Vormontage_KombiB.SetCheck(FALSE); //Arbeitsvorgang beendet
   }
  }
}

void CThread001Dlg::thr_Endprodukt1()
{
  while (m_Flag)
  {
      Warteschleife();
   while ( (m_Bestand_KombiA>3) && (m_Bestand_KombiB>3) )
   {
   m_Checkbutton_Endmontage_Endprodukt1.SetCheck(TRUE); //Arbeitsvorgang startet
   m_cs_KombiA.Lock(); //Kombi A wird aus dem Vorrat genommen
   m_Bestand_KombiA--;
   m_cs_KombiA.Unlock();
   m_cs_KombiB.Lock(); //Kombi B wird aus dem Vorrat genommen
   m_Bestand_KombiB--;
   m_cs_KombiB.Unlock();
   Sleep(4000); //Fertigungszeit
   m_cs_Endprodukt1.Lock(); //Endprodukt 1 wird in den Vorrat genommen
   m_Bestand_Endprodukt1++;
   m_cs_Endprodukt1.Unlock();
   m_Checkbutton_Endmontage_Endprodukt1.SetCheck(FALSE); //Arbeitsvorgang beendet
   }
  }
}

void CThread001Dlg::thr_Endprodukt2()
{
  while (m_Flag)
  {
      Warteschleife();
   while ( (m_Bestand_KombiB>3) && (m_Bestand_Teil5>3) )
   {
   m_Checkbutton_Endmontage_Endprodukt2.SetCheck(TRUE); //Arbeitsvorgang startet
   m_cs_KombiB.Lock(); //Kombi B wird aus dem Vorrat genommen
   m_Bestand_KombiB--;
   m_cs_KombiB.Unlock();
   m_cs_Teil5.Lock(); //Teil 5 wird aus dem Vorrat genommen
   m_Bestand_Teil5--;
   m_cs_Teil5.Unlock();
   Sleep(5000); //Fertigungszeit
   m_cs_Endprodukt2.Lock(); //Endprodukt 2 wird in den Vorrat genommen
   m_Bestand_Endprodukt2++;
   m_cs_Endprodukt2.Unlock();
   m_Checkbutton_Endmontage_Endprodukt2.SetCheck(FALSE); //Arbeitsvorgang beendet
   }
  }
}

void CThread001Dlg::thr_VertriebEndprodukt1()
{
  while (m_Flag)
  {
      Warteschleife();
   while (m_Bestand_Endprodukt1>15)
   {
   m_Checkbutton_Vertrieb_Endprodukt1.SetCheck(TRUE); //Vertriebsvorgang startet
   Sleep(15000); //Lagerentnahmezeit
   m_cs_Endprodukt1.Lock(); //Endprodukt 1 wird aus dem Vorrat genommen
   m_Bestand_Endprodukt1 = m_Bestand_Endprodukt1 - 10;
   m_cs_Endprodukt1.Unlock();
   m_Checkbutton_Vertrieb_Endprodukt1.SetCheck(FALSE); //Arbeitsvorgang beendet
   }
  }
}

void CThread001Dlg::thr_VertriebEndprodukt2()
{
  while (m_Flag)
  {
      Warteschleife();
   while (m_Bestand_Endprodukt2>15)
   {
   m_Checkbutton_Vertrieb_Endprodukt2.SetCheck(TRUE); //Vertriebsvorgang startet
   Sleep(15000); //Lagerentnahmezeit
   m_cs_Endprodukt2.Lock(); //Endprodukt 2 wird aus dem Vorrat genommen
   m_Bestand_Endprodukt2 = m_Bestand_Endprodukt2 - 10;
   m_cs_Endprodukt2.Unlock();
   m_Checkbutton_Vertrieb_Endprodukt2.SetCheck(FALSE); //Arbeitsvorgang beendet
   }
  }
}

void CThread001Dlg::Warteschleife() //wichtig für niedrige Prozessorauslastung
{
  Sleep(1);
}

Dieses Programm können Sie weiterentwicklen oder auf eigene Abläufe umstricken. Entscheidend ist, dass Sie die Threaderzeugung und -steuerung klar erkennen. Multithreading unterstützt in unserem Fall ideal die Prozessorientierung.

Anmerkung:
Die Idee mit der Flugzeugtoilette als im Zugriff begrenzte Ressource, die ich hier gerne als Vergleich darstelle, hatte ich in der Tat unabhängig von Jeffrey Richter, aber beim späteren Durchblättern seines Buches "Programming Applications for MS Windows" fand ich in der 4. Auflage das gleiche Analogon, daher zitiere ich hier das Original:
"What are the key points to remember? When you have a resource that is accessed by multiple threads, you should create a CRITICAL_SECTION structure. Since I'm writing this on an airplane flight, let me draw the following analogy. A CRITICAL_SECTION structure is like an airplane's lavatory, and the toilet is the data that you want protected. Since the lavatory is small, only one person (thread) at a time can be inside the lavatory (critical section) using the toilet (protected resource)."
Jeffrey Richter hat genau so wenig wie ich im ersten Moment an Mütter mit kleinen Kindern gedacht. Die machen nicht einmal vor einem kritischen Abschnitt halt. Aber wir werden diesen Mehrfachzugriff auch noch mit Semaphoren abbilden (siehe unten).
 
 

Mutexe

Mutex ist ein zusammengesetzter Begriff, der von mutually exclusive (gegenseitig ausschließen) herrührt. Man verwendet einen Mutex analog zu kritischen Abschnitten mit dem Unterschied, dass Mutexe prozessübergreifend einsetzbar sind.  Zuständig ist die MFC-Klasse CMutex. Verfügbare Funktionen sind: Konstruktor, Lock() und Unlock().

Der Konstruktor hat folgende Parameter:

CMutex
(
  BOOL bInitiallyOwn = FALSE,
  LPCTSTR lpszName = NULL,
  LPSECURITY_ATTRIBUTES lpsaAttribute = NULL
);

Der Parameter bInitiallyOwn legt fest, ob das Objekt CMutex sofort gesperrt werden soll (TRUE).
Der zweite Parameter gibt dem Objekt einen Namen für die Kommunikation zwischen mehreren Prozessen.

Die Funktion Lock(...) kann bei CMutex einen Parameter aufnehmen, der die maximale Wartezeit in Millisekunden angibt. Dann wird automatisch "entriegelt". Dies ist ein entscheidender Unterschied zum kritischen Abschnitt. Bei der Interprozesskommunikation muß man eben härtere Bandagen anlegen, ansonsten könnte ein Prozess den anderen mit ins Verderben reißen. Dieser Parameter ist bei CCriticalSection::Lock(...) formal auch möglich. Dort wird der Parameter jedoch ignoriert!

Eine schnell erstellte Anwendung ist einfach der Ersatz von CCriticalSection durch CMutex in einer Anwendung. Probieren Sie es an einem unserer einfachen Beispiele aus:
 
class CThread001Dlg : public CDialog
{
// Konstruktion
public:
 void thrRun1();
...
 static UINT thrFunction1 (LPVOID pParam);
...
...
private:
 int m_Flag;
 CMutex cs; //anstelle CCriticalSection cs;
};

Das zeigt, dass die Funktion als einfacher "Schlüssel" (ohne Namen) identisch ist. Da es sich hier jedoch um eine Anwendung handelt, die nicht prozessübergreifend arbeitet, ist der "Überbau" von CMutex nicht notwendig. CMutex arbeitet langsamer als CCriticalSection.

Also schauen wir uns für eine Anwendung von CMutex nach einem einfachen prozessübergreifenden Beispiel um:

Sie erstellen eine einfache Dialoganwendung. Ergänzen Sie Folgendes:
Zunächst fügen Sie ein Edit-Feld hinzu. Als ID belassen Sie es bitte auf IDC_EDIT1. Zusätzlich benötigen wir noch eine Funktion für den rechten Mausklick, mit dem wir den Arbeitsthread starten wollen. In die Datei xxxDlg.cpp fügen Sie folgenden Sourcecode hinzu:
 
// Thread_mit_MutexDlg.cpp : Implementierungsdatei
//

#include "stdafx.h"
#include "Thread_mit_Mutex.h"
#include "Thread_mit_MutexDlg.h"
...

#include "afxmt.h"

CMutex key( FALSE, "MeinSchluessel" );

UINT thrFunction1(LPVOID pParam)
{
   ::SetDlgItemText(HWND(pParam),IDC_EDIT1,"Gestartet");

   key.Lock();
   ::SetDlgItemText(HWND(pParam),IDC_EDIT1,"Gesperrt");
   ::Sleep(5000);
   key.Unlock();

   ::SetDlgItemText(HWND(pParam),IDC_EDIT1,"Wieder offen");

   return 0;
}

/////////////////////////////////////////////////////////////////////////////
// CAboutDlg-Dialogfeld für Anwendungsbefehl "Info"

class CAboutDlg : public CDialog
{

...
...
 

void CThread_mit_MutexDlg::OnRButtonDown(UINT nFlags, CPoint point) 
{
 HWND hwnd = GetSafeHwnd();
 CWinThread* pThread1 = AfxBeginThread (thrFunction1, hwnd);
 CDialog::OnRButtonDown(nFlags, point);
}

Wir definieren also in diesem Fall einen globalen Mutex und eine globale Thread-Funktion thrFunction1(...). Entscheidend ist, dass der Mutex einen Namen (hier: "MeinSchluessel") erhält. Damit ist er systemweit bekannt. Wir haben nun einen prozessübergreifenden Schlüssel.
Der Thread wird durch den rechten Mausklick gestartet.

Nun sind wir bereit für unser Experiment. Stellen Sie sich einfach vier wartende Personen (wir benutzen Threads an ihrer Stelle) vor einer Flugzeugtoilette vor. Jeder benötigt 5 Sekunden. Also wie machen wir das? Ganz einfach: Starten Sie die Anwendung vier Mal und verteilen Sie die Fenster in der gestarteten Reihenfolge gleichmäßig auf dem Bildschirm: Zuerst links oben, dann rechts oben, dann links unten und zum Schluß rechts unten . Dann klicken Sie schnell hintereinander alle Dialogfenster mit der rechten Maustaste an, wieder die gleiche Folge: Zuerst links oben, dann rechts oben, dann links unten und zum Schluß rechts unten.

Dann sehen Sie folgendes Bild:
Alle Arbeitsthreads sind (getriggert durch den Rechtsklick) in Richtung "Toilette" gestartet, aber nur der erste (links oben, den Sie zuerst angeklickt haben) Arbeitsthread hat es geschafft, er hat natürlich sofort das Schloß verriegelt:

Nachdem er fünf Sekunden sein "Geschäft" (hier ::Sleep(5000) ) verrichtet hat, gibt er die "Toilette" wieder frei. Was passiert nun?

(Versuch mit Betriebssystem Windows 98 SE)

Das hängt davon ab, welches Betriebssystem Sie verwenden. Als Besitzer des altehrwürdigen Windows 98 stellen Sie folgendes fest:
Da hat sich einer vorgedrängt! Der Arbeitsthread, den Sie zuletzt gestartet haben, ist nun als Zweiter an der Reihe. Dann kommt der links unten und zuletzt rechts oben. Beschwerden bitte an die Stewardess (äh, an das Betriebssystem). Es gilt bei MS Windows 98 offenbar das Prinzip: "Wer zuletzt kommt, darf als Erster."

Als Besitzer des neueren Windows XP entgeht Ihnen dieses merkwürdige Erlebnis. Alle Threads gehen geordnet nach ihrer Wartezeit auf die "Toilette".

Ich hoffe, die Analogie mit der Flugzeugtoilette hilft Ihnen bei der geistigen Visualisierung des Vorgangs.
In unserem Beispiel machen wir nichts besonderes:

   key.Lock();
   ::SetDlgItemText(HWND(pParam),IDC_EDIT1,"Gesperrt");
   ::Sleep(5000);
   key.Unlock();

Wir drehen den Schlüssel um und schlafen 5 Sekunden. Nicht sonderlich aufregend.

Stellen Sie sich nun mehrere verschiedene Anwendungen vor, die z.B. über eine Datei oder die Registry kommunizieren bzw. eine andere Ressource quasiparallel manipulieren. Da wird das schon spannender. Sie wissen nun wie es geht.
 

Wenn man verhindern will, dass eine Anwendung mehr als einmal gestartet wird, gibt es zunächst den klassischen Weg mit
HWND FindWindow(  LPCTSTR lpClassName,   LPCTSTR lpWindowName  ). Nachfolgend ein einfaches WinAPI-Beispiel:
 
if( ::FindWindow("GesuchterKlassenName","GesuchterFensterTitel") ) 
{
   ::PostQuitMessage(0); 
   return 0;
}

Mit einem Mutex kann man nun eine interessante Alternative anwenden. Sie finden hier zur besseren Übersicht eine MFC-Anwendung, die ohne Assistent und in einer einzigen Datei programmiert ist :
 
#include <afxwin.h>

class CFenster : public CFrameWnd
{
 public:
 CFenster() 
 { 
  Create( NULL, "MFC-Programm mit Mehrfachstartverhinderung", WS_OVERLAPPEDWINDOW, CRect(0,0,400,100) );
 }
};

class CMeinProgramm : public CWinApp
{
 public:
 virtual BOOL InitInstance();
};

BOOL CMeinProgramm :: InitInstance()
{
 BOOL Flag = FALSE;
 HANDLE hMutex =  ::CreateMutex (NULL, TRUE, "Mutex_zur_Verhinderung_eines_Doppelstarts");
 if( GetLastError() == ERROR_ALREADY_EXISTS ) Flag = TRUE;
 if( Flag )
 {
  AfxMessageBox( "Ein Mehrfachstart wurde erfolgreich verhindert" );
  return FALSE;
 }

 m_pMainWnd = new CFenster();
 m_pMainWnd->ShowWindow(m_nCmdShow);
 m_pMainWnd->UpdateWindow();
 return TRUE;
}

CMeinProgramm test;

Zunächst erkennen Sie, dass wir in InitInstance() für den Mutex den direkten Weg über die WinAPI gewählt haben. Das erspart auch die Einbindung von afxmt.h (siehe unten).

Die WinAPI-Funktion zur Erzeugung eines Mutex lautet:

HANDLE CreateMutex
(
  LPSECURITY_ATTRIBUTES lpMutexAttributes, // pointer to security attributes
  BOOL bInitialOwner,                    // flag for initial ownership
  LPCTSTR lpName                           // pointer to mutex-object name
);

Der Trick liegt hier darin begründet, dass wir beim Erstellen des Mutex den Parameter bInitialOwner auf TRUE gesetzt haben. Damit geht der Mutex sofort in den Besitz dieses Threads über. Wird die Anwendung erneut gestartet, dann stellt genau diese Funktion auch fest, dass der Mutex bereits existiert und liefert einen Handle auf den bereits existierenden Mutex zurück. Die Funktion GetLastError() resultiert dann den Wert ERROR_ALREADY_EXISTS. Diesen Zusammenhang nutzen wir hier auf direkte Weise aus.

Die Hülle, die die MFC-Klasse CMutex um die WinAPI legt, ist sehr dünn. Die Klassendefinition findet man in afxmt.h und die Implementierung in mtex.cpp. Wagen wir einen Blick hinein:

"MFC under the hood": CMutex
 
//Klassendefinition in Datei afxmt.h
class CMutex : public CSyncObject
{
  DECLARE_DYNAMIC(CMutex)

public:
  CMutex(BOOL bInitiallyOwn = FALSE, LPCTSTR lpszName = NULL, LPSECURITY_ATTRIBUTES lpsaAttribute = NULL);
  virtual ~CMutex();
  BOOL Unlock();
};

//Implementierung in Datei mtex.cpp

CMutex::CMutex(BOOL bInitiallyOwn, LPCTSTR pstrName, LPSECURITY_ATTRIBUTES lpsaAttribute /* = NULL */
       : CSyncObject(pstrName)
{
  m_hObject = ::CreateMutex(lpsaAttribute, bInitiallyOwn, pstrName);
  if (m_hObject == NULL) AfxThrowResourceException();
}

CMutex::~CMutex(){}

BOOL CMutex::Unlock(){ return ::ReleaseMutex(m_hObject);}


 

Semaphore

Ein Semaphor (engl. Signalmast) ist ein Synchronisationsobjekt, dass es erlaubt, dass eine begrenzte Zahl von Threads in einem oder mehreren Prozessen auf eine gemeinsame Ressource geordnet zugreifen. Die MFC-Klasse CSemaphore behält die Übersicht über die Zahl zugreifender Threads und begrenzt die Nutzerzahl einer Ressource.

Der Konstruktor hat folgende Parameter:

CSemaphore
(
  LONG lInitialCount                   = 1,
  LONG lMaxCount                       = 1,
  LPCTSTR pstrName                     = NULL,
  LPSECURITY_ATTRIBUTES lpsaAttributes = NULL
);

Der InitialCount muß größer/gleich 0 und kleiner/gleich MaxCount sein. Normalerweise setzt man diesen Wert auf MaxCount, da diese Zahl abwärts auf 0 gezählt wird. MaxCount ist die größte zulässige Zahl an parallelen Zugriffen. lpszName ist der optionale Name für die Interprozesskommunikation (analog CMutex).

Ansonsten gibt es die bekannten Funktionen Lock() und Unlock().

Wir betrachten unser "Flugzeugtoiletten-Programm" aus dem Bereich CMutex und verändern es so, dass zwei Threads gleichzeitig auf die Ressource zugreifen dürfen.

Anstelle
 
#include "afxmt.h"
CMutex key( FALSE, "MeinSchluessel" );

nimmt man einfach
 
#include "afxmt.h"
CSemaphore key( 2, 2, "MeinSchluessel" ); 

Das ist wirklich alles!

Nehmen Sie ein etwas kleineres Dialogfeld (ohne OK und Abbruch), damit diese Versuche auch mit mehr als vier Prozessen möglich sind.  Nachfolgend findet man ein Beispiel mit 8 gestarteten Prozessen. Die oberen beiden Arbeitsthreads sind bereits fertig mit ihrem Zugriff, die vier in der Mitte warten noch, und die zwei unteren (wieder die zuletzt gestarteten) sind gerade beim Zugriff, sozusagen Besitzer des Semaphors. Zumindest läuft das unter Windows 98 so ab. Bei Windows XP geht es neuerdings der Reihe nach. Das kennen Sie ja schon von dem Mutex-Beispiel.


 

Fassen wir an dieser Stelle kurz zusammen:

Zur Regelung des quasiparallelen Zugriffs von Threads auf gleiche Ressourcen benutzt man einen Verriegelungs-Mechanismus.
Kritische Abschnitte sind die richtige Lösung innerhalb eines Prozesses, während man zwischen mehreren Prozessen Mutexe verwendet.
Die Identifikation erfolgt bei Mutex und Semaphor über die Festlegung eines Namens.
Will man mehreren Threads gleichzeitig den Zugang zu einer Ressource erlauben,  ersetzt man Mutexe einfach durch Semaphoren.
MFC stellt die Funktionen Lock() und Unlock() für Sperrung und Freigabe bereit.
 
 

CEvent

Während bei CCriticalSection, CMutex und CSemaphore die Visualisierung eines Verriegelungsmechanismus der richtige Vergleich ist, kommen wir mit den Ereignissen (Events) zu einem anderen Mechanismus der Verständigung zwischen Threads.

Ereignisse (Events) sind ein Alarmsystem zwischen mehreren Threads. Ereignisse sind vom Betriebssystem gepflegte Flags. Man nennt sie auch "Threadzünder".

Es gibt hier neben dem Konstruktor

CEvent
(
  BOOL bInitiallyOwn                  = FALSE,
  BOOL bManualReset                   = FALSE,
  LPCTSTR lpszName                    = NULL,
  LPSECURITY_ATTRIBUTES lpsaAttribute = NULL
);

Parameter:
  bInitiallyOwn    FALSE: Signal gesetzt,   TRUE: Signal nicht gesetzt.
  bManualReset     FALSE: "Autoreset",      TRUE: manueller Reset.
  lpszName         Name für die Interprozesskommunikation (analog CMutex und CSemaphore)

noch die Funktionen:

SetEvent()
Hiermit wird das "Ereignis" gesetzt / sinalisiert. Dies gibt alle wartenden Threads frei. Bei Ereignissen unterscheidet man einen "manuellen" und einen automatischen Reset. Beim manuellen Reset bleibt das Ereignis signalisiert, bis ResetEvent() diesen Signalzustand beendet. Beim automatischen Reset wird der Signalzustand beendet, sobald der erste (!) Thread freigegeben wird. Weitere wartende Threads werden nicht berücksichtigt.

ResetEvent()
Hiermit wird das "Ereignis" zurückgesetzt / der Signalzustand beendet.

PulseEvent()
Hiermit wird das "Ereignis" gesetzt / sinalisiert. Alle (!) wartenden Threads werden freigegeben und anschließend der Signalzustand automatisch - also ohne ResetEvent() - zurückgesetzt. Zwei oder mehr wartende Threads lassen sich nur über diese Funktion gemeinsam "entsperren" und damit fortsetzen.

Lock()
Ein CEvent Objekt wird aktiviert und sperrt damit den aufrufenden Thread. Dieser wartet dann darauf, das ein anderer Thread dieses Ereignis mit SetEvent() oder PulseEvent() signalisiert.

Unlock()
Das CEvent Objekt wird freigegeben.
 

Klingt alles recht kompliziert. Da helfen nur praktische Beispiele.

Nehmen Sie zunächst unser kleines Übungsbeispiel von CMutex und CSemaphore mit folgender Veränderung:
 
#include "afxmt.h"
CEvent alarm( FALSE, FALSE, "MeinSignal" );

UINT thrFunction1(LPVOID pParam)
{
   ::SetDlgItemText(HWND(pParam),IDC_EDIT1,"Gestartet");
   ::Sleep(3000);

   alarm.SetEvent();
   ::SetDlgItemText(HWND(pParam),IDC_EDIT1,"Signal gesetzt");
   ::Sleep(3000);

   ::SetDlgItemText(HWND(pParam),IDC_EDIT1,"Gesperrt");
   alarm.Lock();
   ::SetDlgItemText(HWND(pParam),IDC_EDIT1,"Wieder offen");

   return 0;
}

Lassen Sie sich Zeit im Verständnis. Betrachten wir zunächst eine einzelne Anwendung:

Nach dem Rechtsklick startet der Arbeitsthread thrFunction1(...), und das Edit-Feld zeigt "Gestartet".

3 Sekunden später wird das (prozessübergreifende) Autoreset-Ereignis "MeinSignal" gesetzt/signalisiert.
Das Edit-Feld zeigt "Signal gesetzt".

Wieder 3 Sekunden später wird der Thread durch alarm.lock() angehalten.
Da das Ereignis noch signalisiert ist, erhält der Thread sofort die Freigabe.
Der Thread läuft weiter, und Sie sehen "Wieder offen".


 

Interessanter wird es mit zwei gestarteten Anwendungen.

Sie klicken zuerst die linke, dann die rechte Anwendung an. Hier schafft es nur der zuerst gestartete Arbeitsthread das alarm.lock() zu überwinden. Er setzt das (für beide Threads gültige) Ereignis zurück, und für den zu spät kommenden Thread gibt es nur die verschlossene Tür (das zurückgesetzte/nicht-signalisierte Ereignis).

Der Grund hierfür ist der im Konstruktor angegebene "Autoreset":

CEvent alarm( FALSE, FALSE, "MeinSignal" );

Wenn Sie diesen Parameter auf TRUE setzen, muß "manuell", d.h. im Programm, die Funktion ResetEvent() eingesetzt werden.
Probieren Sie es aus.

Ein anderer Versuch:
Ersetzen Sie SetEvent() durch PulseEvent(), um dann Experimente mit mehreren gestarteten Anwendungen durchzuführen.

Diese vielfältigen Möglichkeiten lassen sich hier nicht optimal visualisieren. Hier geht "Probieren über Studieren".

Entscheidend ist, dass Sie verstehen, dass man Threads mit Lock() an einem Ereignis anhält. Wenn das Ereignis signalisiert ist ("grüne Ampel"), dann geht die Fahrt weiter, falls nicht ("rote Ampel"), muß erst ein anderer Thread das Ereignis signalisieren ("Ampel auf grün stellen").
Mit SetEvent() gibt man einem wartenden Thread den Weg frei. Mehrere wartende Threads benötigen ein PulseEvent(). Bei PulseEvent() darf das Autoreset-Flag jedoch nicht gesetzt sein. Um das ResetEvent() kümmert sich PulseEvent() nach der Fortsetzung aller wartenden Threads selbst.
 
 

Deadlock

Stellen Sie sich vor, Sie erstellen ein Simulationsprogramm für eine Verkehrskreuzung mit Rechts-vor-Links-Vorfahrt. Solange das Verkehrsaufkommen niedrig ist, klappt das sicher gut. Wenn jedoch plötzlich bei ansteigendem Verkehr von jeder Seite der Kreuzung ein Fahrzeug kommt, entsteht die typische "Deadlock"-Situation. Kein Fahrzeug darf fahren, weil ein anderer ihm gegenüber die Vorfahrt hat.

Dies kann auch bei Multithreading-Programmen bei entsprechenden Rahmenbedingungen passieren. Kein Arbeitsthread kann mehr weiter. Das Programm steckt in diesem Fall sozusagen am toten Punkt (engl. deadlock) fest. Wenn diese Möglichkeit besteht, müssen Sie die "Regeln"  entsprechend verändern, dass diese Situation vermieden wird.

Weitere Beispiele für solche Situationen können Sie z.B. hier sehen:

Dining Philosophers:
http://www.hta-be.bfh.ch/~fischli/kurse/threads/
http://www.hta-be.bfh.ch/~fischli/kurse/threads/phil/index.html
http://www-dse.doc.ic.ac.uk/concurrency/

Producer/Consumer Problem:
http://www.hta-be.bfh.ch/~fischli/kurse/threads/prodcons/
http://www.cs.mtu.edu/~shene/NSF-3/e-Book/MONITOR/ProducerConsumer-1/MON-example-buffer-1.html
http://www.cs.mtu.edu/~shene/NSF-3/e-Book/SEMA/VISUAL/VISUAL-sema-buffer.html
http://cne.gmu.edu/modules/ipc/aqua/producer.html

Sleeping Barber Problem  / Barbershop Problem:
http://www.cis.ksu.edu/saves/eap/examples/barber/prbE.html
http://cs.millersville.edu/~webster/cs380/assignment4.html
http://www.math.grin.edu/~walker/courses/213.fa00/lab-barbershop.html

Reader/Writer Problem:
http://www.hta-be.bfh.ch/~fischli/kurse/threads/readwrit/index.html
http://cne.gmu.edu/modules/ipc/aqua/readers.html
http://cne.gmu.edu/modules/ipc/orange/readsem.html
http://www.mrs.umn.edu/~swl/cs3400/fall98/ipc.html#readerwriter

Cigarette Smoker Problem:
http://www.cs.umd.edu/~hollings/cs412/s96/synch/smokers.html
http://www.cs.cmu.edu/~emc/15-398/assignments/parnas_smokers.pdf
http://webster.cs.uga.edu/~maria/classes/4730-Spring-2002/project3/project3.txt

Monkey Rock Problem (und alle anderen):
http://i30www.ira.uka.de/teaching/coursedocuments/39/sysarch-7-thread051_sel.pdf
 
 (Links vom Stand Aug. 2002)

zurück zum Inhaltsverzeichnis

vor zum nächsten Kapitel