Anwendungsübergreifendes Mutex

Antworten
Ekkehard
Beiträge: 96
Registriert: So 12. Feb 2023, 12:42
OS, Lazarus, FPC: Windows Lazarus 4.0, FPC 3.2.2
CPU-Target: 64-Bit
Wohnort: Hildesheim

Anwendungsübergreifendes Mutex

Beitrag von Ekkehard »

Hallo Ihr Lieben,
ich arbeite gerade an
- einer Konsolenanwendung,
- die unter Linux laufen wird und
- die in mehreren Instanzen gestartet werden kann und
- da auf gleichen Ordnern operieren kann.
Etwas konkretisiert gibt es Unterverzeichnisse für bestimmte "Objekte", in denen die Dateien nicht konkurrierend verändert werden dürfen, sondern nur von einer Instanz pro Zeit. Da es aber viele von diesen "Objekten" gibt und idR nur selten eine zweite Instanz benötigt wird (zum manuellen Korrigieren bzw. Aktualisieren) kommt eine Limitierung auf eine einzige laufende Instanz nicht in Frage.
Unter Windows würde ich mittels CreateMutexEx (Dort den Namen auf den Namen des entsprechenden Objektes setzen) und WaitForSingleObject den Zugriff auf bestimmte Ordner zeitweise reservieren bzw für die andere(n) Instanz(en) sperren.
Wie geht sowas unter Linux?
Ich habe leider nichts gefunden, bin aber da noch recht unbelesen :?
Vielen Dank!

Mathias
Beiträge: 7158
Registriert: Do 2. Jan 2014, 17:21
OS, Lazarus, FPC: Linux (die neusten Trunk)
CPU-Target: 64Bit
Wohnort: Schweiz

Re: Anwendungsübergreifendes Mutex

Beitrag von Mathias »

Wen du etwas externes nehmen willst, unter Linux gibt es einiges, was dies beherrscht.
Am besten mal googeln, was die am besten zusagt.

Ich habe auch schon damit versuche gemacht, hier findest du ein paar Beispiele: https://github.com/sechshelme/Lazarus-X ... -_Diverses
Die Beispiel verwendet alle FPC eigene Funktionen.

Eine Warnung vorweg, wen du für die Ausgabe Wirteln verenden willst, die ist nicht Multitasking fähig.
Da muss du vor der Ausgabe sperren und dann wieder entsperren.
Mit Lazarus sehe ich grün
Mit Java und C/C++ sehe ich rot

Ekkehard
Beiträge: 96
Registriert: So 12. Feb 2023, 12:42
OS, Lazarus, FPC: Windows Lazarus 4.0, FPC 3.2.2
CPU-Target: 64-Bit
Wohnort: Hildesheim

Re: Anwendungsübergreifendes Mutex

Beitrag von Ekkehard »

Mathias hat geschrieben: Sa 3. Jan 2026, 20:15 Ich habe auch schon damit versuche gemacht, hier findest du ein paar Beispiele: https://github.com/sechshelme/Lazarus-X ... -_Diverses
Die Beispiel verwendet alle FPC eigene Funktionen.
Hmm, das einzige was da ungefähr passt ist
https://github.com/sechshelme/Lazarus-X ... _Threading
aber die dort verwendete Funktion

Code: Alles auswählen

  pthread_mutex_lock(@mutex);
  Write(Thread.index, ' ');
  pthread_mutex_unlock(@mutex);
arbeitet aber nur innerhalb einer Anwendung und nicht systemweit.

In einer RedHat Dokumentation fand ich
Shared and private mutexes
Shared mutexes can be used between processes, however they can create a lot more overhead.
pthread_mutexattr_setpshared(&my_mutex_attr, PTHREAD_PROCESS_SHARED);

Diese Funktion ist in Lazarus/FreePascal in der Datei pthrlinux.inc definiert:
function pthread_mutexattr_setpshared(__attr:Ppthread_mutexattr_t; __pshared:cint):cint;cdecl; external libthreads;
Gibt es da auch ein Beispiel?

Benutzeravatar
theo
Beiträge: 11113
Registriert: Mo 11. Sep 2006, 19:01

Re: Anwendungsübergreifendes Mutex

Beitrag von theo »

Vielleicht könntest du die Technik von uniqueinstance übernehmen?
https://github.com/blikblum/luipack/tre ... ueinstance

Oder einfach ein Lockfile? Kommt halt drauf an, wie kritisch das Ganze ist.
Auch das kann man anspruchsvoller machen:
https://www.baeldung.com/linux/file-locking

K.A. ob's hilft.

Ekkehard
Beiträge: 96
Registriert: So 12. Feb 2023, 12:42
OS, Lazarus, FPC: Windows Lazarus 4.0, FPC 3.2.2
CPU-Target: 64-Bit
Wohnort: Hildesheim

Re: Anwendungsübergreifendes Mutex

Beitrag von Ekkehard »

theo hat geschrieben: Sa 3. Jan 2026, 21:49 Vielleicht könntest du die Technik von uniqueinstance übernehmen?

Oder einfach ein Lockfile? Kommt halt drauf an, wie kritisch das Ganze ist.
Vielen Dank für die Hinweise, ich hatte schon beides probiert zunächst mit durchwachsenem Erfolg.
Zu uniqueinstance.
Hier erscheint mir der Overhead recht groß. Gleichwohl würde es damit funktionieren, obwohl es bei gleichzeitigem Zugriff zu einer gegenseitigen Blockade kommt und man sorgfältig mit zufälligen Timeouts muss, damit sich diese Gleichzeitigkeit durchbrechen lässt.

Zum Lockfile.
Das war mein erster Gedanke, aber ich scheiterte überraschender Weise und landete in völlig zufälligem Verhalten.
Ich nahm einen TFileStream als File, mit fmShareExclusive als Mode, allerdings schloss ich die Datei wieder und prüfte andernorts nur auf deren Existenz. Das ging völlig daneben.
Ich nahm aber Deinen Beitrag und Link nochmal zum Anlass elementarer vorzugehen und hatte Erfolg, aber leider bisher nur unter Windows!

Code: Alles auswählen

const
  LockFolderName = 'My/Path/To/An/Existing/Folder/';
  LockFileName = 'dir.LOCK';
  LockTestFileName = 'test.txt';

function UnlockFolder(const AHandle : THandle) : Boolean;
begin
  Result := (AHandle <> THandle(-1));
  if Result then
    FileClose(AHandle);
end;

function LockFolder(out AHandle : THandle; const MaxWait : Integer = -1) : Boolean;
var
  t0, t1 : Int64;
begin
  t0 := GetTickCount64;
  AHandle := THandle(-1);
  Result := False;
  if not DirectoryExists(LockFolderName) then Exit;
  repeat
    AHandle := FileCreate(LockFolderName+LockFileName,fmShareExclusive);
    Result := (AHandle <> THandle(-1));
    if not Result then
      Sleep(0);
    if MaxWait >= 0 then
    begin
      if MaxWait = 0 then
        Break;
      t1 := GetTickCount64;
      if t1-t0 > MaxWait then
        Break;
    end;
  until Result;
end;              
Das Ganze lässt sich so verwenden, hier als Thread innerhalb einer normalen Formularanwendung

Code: Alles auswählen

procedure TLockFolderThread.Execute;
var
  lock : THandle;
  sl : TStringList;
begin
  Synchronize(@SynchMessage);
  try
    while not Terminated do
    begin
      if LockFolder(lock) then
      try
        sl := TStringList.Create;
        try
          if FileExists(LockFolderName+LockTestFileName) then
            sl.LoadFromFile(LockFolderName+LockTestFileName);
          while sl.Count > 10000 do
            sl.Delete(0);
          sl.Add(Format('This is Thread %8.8x (%d)',[ThreadID, ThreadID]));
          sl.SaveToFile(LockFolderName+LockTestFileName);
        finally
          if Assigned(sl) then
            sl.Free;
        end;
        Inc(FLineCount);
        Synchronize(@SynchMessage);
      finally
        UnLockFolder(lock);
      end;
      // Sleep(Random(250));
      Sleep(0);
    end;
  finally
    if not Terminated then
      Terminate;
  end;
end;         
 


Unter Linux wird die Datei genau einmal erzeugt und lässt sich anschließen nicht mehr "erzeugen", aber auch nicht öffnen. Und löschen kann sie auch jeder Prozess. Da muss ich nochmal intensiver forschen.

Ekkehard
Beiträge: 96
Registriert: So 12. Feb 2023, 12:42
OS, Lazarus, FPC: Windows Lazarus 4.0, FPC 3.2.2
CPU-Target: 64-Bit
Wohnort: Hildesheim

Re: Anwendungsübergreifendes Mutex (Gelöst via LockFile)

Beitrag von Ekkehard »

Nach etwas Gesuche habe ich eine Lösung gefunden, die sowohl unter Windows als auch unter Linux funktioniert.

Um das nachfolgende Beispiel zum Laufen zu bekommen erzeugt man eine neue Anwendung und setzt einen Button und zwei Memos auf das Formular.
Anschließen kopiert man den Quelltext in die Unit1.

Code: Alles auswählen

unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls;

type

  { TLockFolderThread }

  TLockFolderThread = class(TThread)
  private
    FThreadNum : Integer;
    FLineCount : Integer;
    procedure SynchMessage;
  protected
    procedure Execute;override;
  public
    constructor Create(const AThreadNum : Integer);
  end;

  { TForm1 }

  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    Memo2: TMemo;
    procedure Button1Click(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    FLockFolderThreads : array of TLockFolderThread;
  public
    procedure CreateThreads;
    procedure DestroyThreads;
  end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

const
  LockFileSubFolder = 'Test';
  LockFileName = 'test.LOCK';
  LockTestFileName = 'test.txt';

function UnlockFolder(const ALockFolderName : String; const AHandle : THandle) : Boolean;
var
  b : Boolean;
begin
  Result := (AHandle <> THandle(-1));
  if Result then
  begin
    b := DeleteFile(IncludeTrailingPathDelimiter(ALockFolderName)+LockFileName);
    FileClose(AHandle);
  end;
end;

function LockFolder(const ALockFolderName : String; out AHandle : THandle; const MaxWait : Integer = -1) : Boolean;
var
  t0, t1 : Int64;
begin
  t0 := GetTickCount64;
  AHandle := THandle(-1);
  Result := False;
  if not DirectoryExists(ALockFolderName) then Exit;
  repeat
    AHandle := FileCreate(IncludeTrailingPathDelimiter(ALockFolderName)+LockFileName,fmShareExclusive,$000);
    Result := (AHandle <> THandle(-1));
    if Result then Break;
    Sleep(0);
    if MaxWait >= 0 then
    begin
      if MaxWait = 0 then
        Break;
      t1 := GetTickCount64;
      if t1-t0 > MaxWait then
        Break;
    end;
  until Result;
end;

{ TLockFolderThread }

procedure TLockFolderThread.SynchMessage;
begin
  if FLineCount < 0 then
  begin
    Form1.Memo1.Lines.Add(Format('%d %8.8x (%d)',[FThreadNum,ThreadID,ThreadID]));
    Inc(FLineCount);
  end;
  while Form1.Memo2.Lines.Count <= FThreadNum do
    Form1.Memo2.Lines.Add(IntToStr(Form1.Memo2.Lines.Count));
  Form1.Memo2.Lines[FThreadNum] := Format('%d %d',[FThreadNum,FLineCount]);
end;

procedure TLockFolderThread.Execute;
var
  lockfn : String;
  lock : THandle;
  sl : TStringList;
begin
  try
    lockfn := GetUserDir()+IncludeTrailingPathDelimiter('Documents')+IncludeTrailingPathDelimiter(LockFileSubFolder);
    Synchronize(@SynchMessage);
    while not Terminated do
    begin
      if LockFolder(lockfn,lock) then
      try
        sl := TStringList.Create;
        try
          try
            if FileExists(lockfn+LockTestFileName) then
              sl.LoadFromFile(lockfn+LockTestFileName);
            while sl.Count > 10000 do
              sl.Delete(0);
            sl.Add(Format('This is Thread %8.8x (%d)',[ThreadID, ThreadID]));
            sl.SaveToFile(lockfn+LockTestFileName);
          except
          end;
        finally
          if Assigned(sl) then
            sl.Free;
        end;
        Inc(FLineCount);
        Synchronize(@SynchMessage);
      finally
        UnLockFolder(lockfn,lock);
      end;
//      Sleep(Random(250));
      Sleep(0);
    end;
  finally
    if not Terminated then
      Terminate;
  end;
end;

constructor TLockFolderThread.Create(const AThreadNum: Integer);
begin
  inherited Create(True);
//  Priority := tpHighest;
  FThreadNum := AThreadNum;
  FLineCount := -1;
  Suspended := False;
end;

{ TForm1 }

procedure TForm1.Button1Click(Sender: TObject);
begin
  if Length(FLockFolderThreads) <= 0 then
  begin
    Button1.Caption := 'Stop';
    Memo1.Clear;
    Memo2.Clear;
    CreateThreads;
  end
  else
  begin
    Button1.Caption := 'Start';
    DestroyThreads;
  end;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  DestroyThreads;
end;

procedure TForm1.CreateThreads;
var
  i : Integer;
  lockfn : String;
begin
  DestroyThreads;
  lockfn := GetUserDir()+IncludeTrailingPathDelimiter('Documents')+IncludeTrailingPathDelimiter(LockFileSubFolder);
  if not DirectoryExists(lockfn) then
    ForceDirectories(lockfn);
  if not DirectoryExists(lockfn) then Exit;
  SetLength(FLockFolderThreads,5);
  for i := 0 to High(FLockFolderThreads) do
    FLockFolderThreads[i] := TLockFolderThread.Create(i);
end;

procedure TForm1.DestroyThreads;
var
  i : Integer;
  lockfn : String;
begin
  for i := 0 to High(FLockFolderThreads) do
  begin
    if Assigned(FLockFolderThreads[i]) then
      FLockFolderThreads[i].Terminate;
  end;
  for i := 0 to High(FLockFolderThreads) do
  begin
    if Assigned(FLockFolderThreads[i]) then
      FLockFolderThreads[i].WaitFor;
  end;
  for i := 0 to High(FLockFolderThreads) do
  begin
    if Assigned(FLockFolderThreads[i]) then
      FreeAndNil(FLockFolderThreads[i]);
  end;
  SetLength(FLockFolderThreads,0);
  lockfn := GetUserDir()+IncludeTrailingPathDelimiter('Documents')+IncludeTrailingPathDelimiter(LockFileSubFolder);
  DeleteFile(lockfn+LockFileName);
end;
end.

Der Schlüssel zur Funktion in Linux war
DeleteFile(IncludeTrailingPathDelimiter(ALockFolderName)+LockFileName);
vor
FileClose(AHandle);
in der Funktion UnlockFolder.
Unter Windows schlägt das Löschen fehl (Datei ist ja geöffnet) unter Linux nicht, vielmehr ist es dort zwingend erforderlich.
In
FileCreate(IncludeTrailingPathDelimiter(ALockFolderName)+LockFileName,fmShareExclusive,$000);
is ein fmOpenWrite entbehrlich.
Unter Windows wird mit FileCreate auch eine existierende Datei geöffnet.
Unter Linux schlägt das fehl. Deshalb muss diese auch in der Freigabe gelöscht werden. Dies wiederum muss zwingend geschehen solange die Datei geöffnet und somit gegen den Zugriff Dritter geschützt ist.

Hier ergibt sich aber ein wirklich blödes Problem mit mehreren Anwendungen unter Linux: Sollte eine Anwendung sterben, während die Datei geöffnet und existent ist, bleibt das System für immer gesperrt. Ich kümmere mich später darum.

Ekkehard
Beiträge: 96
Registriert: So 12. Feb 2023, 12:42
OS, Lazarus, FPC: Windows Lazarus 4.0, FPC 3.2.2
CPU-Target: 64-Bit
Wohnort: Hildesheim

Re: Anwendungsübergreifendes Mutex (Gelöst via LockFile)

Beitrag von Ekkehard »

Ekkehard hat geschrieben: So 4. Jan 2026, 12:28 Hier ergibt sich aber ein wirklich blödes Problem mit mehreren Anwendungen unter Linux: Sollte eine Anwendung sterben, während die Datei geöffnet und existent ist, bleibt das System für immer gesperrt. Ich kümmere mich später darum.

Code: Alles auswählen

function LockFolder(const ALockFolderName : String; out AHandle : THandle; const MaxWait : Integer = -1) : Boolean;
var
  t0, t1 : Int64;
begin
  t0 := GetTickCount64;
  AHandle := THandle(-1);
  Result := False;
  if not DirectoryExists(ALockFolderName) then Exit;
  repeat
    AHandle := FileCreate(IncludeTrailingPathDelimiter(ALockFolderName)+LockFileName,fmShareExclusive,$000);
    Result := (AHandle <> THandle(-1));
    if Result then Break;
    AHandle := FileOpen(IncludeTrailingPathDelimiter(ALockFolderName)+LockFileName,fmOpenWrite or fmShareExclusive);
    Result := (AHandle <> THandle(-1));
    if Result then Break;
    Sleep(0);
    if MaxWait >= 0 then
    begin
      if MaxWait = 0 then
        Break;
      t1 := GetTickCount64;
      if t1-t0 > MaxWait then
        Break;
    end;
  until Result;
end;
So scheint es zu funktionieren:
Wenn das FileCreate fehlschlägt, kommt man mit einem FileOpen zum Ziel, falls die Datei existiert und von niemandem der noch existiert geöffnet gehalten ist.

Benutzeravatar
theo
Beiträge: 11113
Registriert: Mo 11. Sep 2006, 19:01

Re: Anwendungsübergreifendes Mutex

Beitrag von theo »

An fmShareExclusive hatte ich auch als erstes gedacht.
Da du aber geschrieben hast; "da auf gleichen Ordnern operieren kann", habe ich das nicht erwähnt.
Ordner kann man so wohl nicht locken.

Warf
Beiträge: 2248
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: Anwendungsübergreifendes Mutex

Beitrag von Warf »

Wenn es unbedingt ein Mutex sein muss, dann kannst du einfach einen pthread Mutex in einer shared memory region erstellen.
Das was du vorhast klingt aber viel eher nach einem fall für einen lockfile wie ihn unteranderem die Linux paket manager oder git verwenden.

Im grunde ganz einfach: wenn du auf den ordner zugreifen willst erstellst du die Datei. Wenn du fertig bist löschst du sie wieder. Da da die Dateisystem zugriffe vom OS serialisiert werden kanns niemals passieren das zwei Prozesse gleichzeitig den lockfile erstellen können.

Ekkehard
Beiträge: 96
Registriert: So 12. Feb 2023, 12:42
OS, Lazarus, FPC: Windows Lazarus 4.0, FPC 3.2.2
CPU-Target: 64-Bit
Wohnort: Hildesheim

Re: Anwendungsübergreifendes Mutex

Beitrag von Ekkehard »

theo hat geschrieben: So 4. Jan 2026, 13:24 An fmShareExclusive hatte ich auch als erstes gedacht.
Da du aber geschrieben hast; "da auf gleichen Ordnern operieren kann", habe ich das nicht erwähnt.
Ordner kann man so wohl nicht locken.
Vielleicht hatte ich mich unglücklich ausgedrückt. Es geht um ein "privates" Lock zwischen zwei Prozessen der gleichen Anwendungen. Da die dafür in Frage kommenden Daten in jeweils einem Ordner liegen, sprach ich von "gleichen Ordnern".
Der Zugriff Dritter (etwas das Löschen von Dateien durch den Benutzer) soll und kann nicht blockiert werden.

Ekkehard
Beiträge: 96
Registriert: So 12. Feb 2023, 12:42
OS, Lazarus, FPC: Windows Lazarus 4.0, FPC 3.2.2
CPU-Target: 64-Bit
Wohnort: Hildesheim

Re: Anwendungsübergreifendes Mutex

Beitrag von Ekkehard »

Warf hat geschrieben: So 4. Jan 2026, 15:12 Im grunde ganz einfach: wenn du auf den ordner zugreifen willst erstellst du die Datei. Wenn du fertig bist löschst du sie wieder. Da da die Dateisystem zugriffe vom OS serialisiert werden kanns niemals passieren das zwei Prozesse gleichzeitig den lockfile erstellen können.
Das genau war ja mein erster Ansatz, der aber leider nicht so wie gewünscht funktionierte.
Wie zuvor ausgeführt, habe ich diese Hürde jetzt genommen und werde in diese Richtung weiter gehen.
Die Idee mit dem Mutex kam bei mir auf, weil dies eine verallgemeinerte Methodik ist um derartige Probleme zu lösen und ich mich fragte wie das unter Linux geht.

Antworten