Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Image browsing program - Why does it randomly crash after making it threaded?

Long story short, I'm very far from a skilled programmer, in fact, my most complicated programs so far were either plain ASCII string manipulating, simple maths and array searching/sorting either in Free Pascal or later, Delphi 7 and Java. This was some years ago, when I learnt programming in high school faculty (that was plain Pascal). Later I went on to become a programmer (meeting with D7 and Java, and some C++), but I had quit my programming studies due to personal reasons, and since then, I didn't wrote a single line of code.

Erm, sorry for the long introduction, so... Recently I decided to revive programming as my hobby, mainly because I didn't found a suitable program for some tasks I would like to accomplish since long. In spite of my faint understanding of such fairly basic things as explicit parameters, pointers, objects, classes, constructors and threads, with the help of a programming book, Delphi help files and the internet, I managed to write a simple program in Delphi 7 that can load and display certain image file formats in a given directory using external libraries, make it possible to arbitrarily switch among them (using the GUI), and log some information (mainly for debug purposes) in text files.

However, I encountered a problem at the current version of the code when I tried to make the image loading and displaying function threaded. First, for better understanding, I'll explain the logic of my program.

First of all, in the main form's FormCreate event, the program looks for supported image files in the current directory (where the exe is). If there is no image files, the current directory is set at the one at the upper level (with the standard Windows file system symbol "..") and is checked for images. Supported image files are determined by file extensions. Anyway, this explorer function stores the found images' filename and a file type identifier (which is a byte) in a dynamic array. Then, using this array as a reference, the first supported image file is loaded by the correct library and displayed in the form. The GUI has buttons and a combobox to change between images, with each control's OnClick or OnSelect (combobox) event setting variables about the supposedly current image file and calling the image loader and displayer function which uses the reference array.

The problem is that some images are so huge that the loading takes noticeable time, so the GUI can't respond until the image is fully loaded and displayed. I tried to make this program threaded by initializing each image loader function as a thread. While the GUI is more responsive now, there are certainly two new bugs with the program.

The first is that the program sometimes randomly crashes when changing images, with appearing messages either referring to "JPEG Error #58" (supposedly meaning "invalid file structure" in Delphi's in-built jpeg library), "EAccessViolation" exception, "EOSError" exception (including "System Error, Code 5"), "unknown software exception", "Runtime error 216", and error messages about memory locations and failed read operations. Before using threads, none of these error messages appeared, but I certainly want to (and must) use threads in the program.

The other, minor bug is that when the interface buttons are clicked in a fast succession, it seems like all loading and displaying takes place, although in a laggy-then-quickly manner. I don't really have an idea on how to "kill" a thread and initiate it "again" to load the now-current file instead of the obsolete one it tried to load a few hundred milliseconds ago.

I start a thread in the following manner:

LoaderThread := CreateThread(nil, 0, Addr(LoadPicture), nil, 0, LoaderThreadID);
CloseHandle(LoaderThread);

I use this two times in the main form's FormCreate event (although only one of them executes at any start), and in the GUI controls' OnClick or OnSelect event to faciliate the desired function of the control (for example skip to the last image).

Any suggestions? Thank you in advance! :)

UPDATE: Here is some (well, almost all) of my source code:

procedure TMainForm.FormCreate(Sender: TObject);
begin
  MainForm.DoubleBuffered := true;
  MainJPEG := TJPEGImage.Create;
  MainJPEG.ProgressiveDisplay := true;
  MainJPEG.Smoothing := true;
  MainJPEG.Performance := jpBestQuality;
  MainPNG := TPNGObject.Create;
  MainGIF := TGIFImage.Create;
  AssignFile(Log, '_NyanLog.txt');
  CurrentDir := GetCurrentDir;
  ExploreCurrentDir;
  if CurrentDirHasImages = false then
  begin
    SetCurrentDir('..');
    CurrentDir := GetCurrentDir;
    ExploreCurrentDir;
  end;
  if CurrentDirHasImages = true then
    begin
      CurrentFilename := ImagesOfCurrentDir[CurrentPos].Filename;
      CurrentFiletype := ImagesOfCurrentDir[CurrentPos].Filetype;
      LoaderThread := BeginThread(nil, 0, Addr(LoadImage), nil, 0, LoaderThreadID);
      CloseHandle(LoaderThread);
      if Length(ImagesOfCurrentDir) > 1 then
      begin
        MainForm.NextButton.Enabled := true;
        MainForm.EndButton.Enabled := true;
        MainForm.SlideshowButton.Enabled := true;
        MainForm.SlideshowIntervalUpDown.Enabled := true;
      end;
      UpdateTitleBar;
    end
  else UpdateTitleBar;
end;

procedure ExploreCurrentDir;
var
  Over: boolean;
begin
  CurrentPos := 0;
  Over := false;
  ReWrite(Log);
  Write(Log, 'blablabla');
  if FindFirst(CurrentDir+'\*.*', faAnyFile-faDirectory, Find) = 0 then
    begin
      CurrentFilename := Find.Name;
      DetermineFiletype;
      if CurrentFiletype <> UNSUPPORTED then
      begin
        SetLength(ImagesOfCurrentDir, CurrentPos+1);
        ImagesOfCurrentDir[CurrentPos].Filename := CurrentFilename;
        ImagesOfCurrentDir[CurrentPos].Filetype := CurrentFiletype;
        MainForm.ImagelistComboBox.AddItem(CurrentFilename, nil);
        Write(Log, 'blablabla');
        CurrentPos := Succ(CurrentPos);
      end;
      while Over = false do
      begin
        if FindNext(Find) = 0 then
          begin
            CurrentFilename := Find.Name;
            DetermineFiletype;
            if CurrentFiletype <> UNSUPPORTED then
            begin
              SetLength(ImagesOfCurrentDir, CurrentPos+1);
              ImagesOfCurrentDir[CurrentPos].Filename := CurrentFilename;
              ImagesOfCurrentDir[CurrentPos].Filetype := CurrentFiletype;
              MainForm.ImagelistComboBox.AddItem(CurrentFilename, nil);
              Write(Log, 'blablabla');
              CurrentPos := Succ(CurrentPos);
            end;
          end
        else
          begin
            FindClose(Find);
            Over := true;
          end;
      end;
      CurrentDirImageCount := Length(ImagesOfCurrentDir);
      CurrentDirHasImages := true;
      Write(Log, 'blablabla');
    end;
  if CurrentDirHasImages = false then Write(Log, 'blablabla');
  CloseFile(Log);
  CurrentPos := 0;
end;

procedure LoadImage; //procedure #1 which should be used in a thread
begin
  if CurrentFiletype = BMP then
    begin
      MainForm.MainImage.Picture := nil;
      MainForm.MainImage.Picture.LoadFromFile(CurrentFilename)
    end
  else
    if CurrentFiletype = JPEG then
      begin
        MainForm.MainImage.Picture := nil;
        MainJPEG.LoadFromFile(CurrentFilename);
        MainForm.MainImage.Picture.Assign(MainJPEG);
      end
    else
      if CurrentFiletype = PNG then
        begin
          MainForm.MainImage.Picture := nil;
          MainPNG.LoadFromFile(CurrentFilename);
          MainForm.MainImage.Picture.Assign(MainPNG);
        end
      else
        if CurrentFiletype = GIF then
          begin
            MainForm.MainImage.Picture := nil;
            MainGIF.LoadFromFile(CurrentFilename);
            MainForm.MainImage.Picture.Assign(MainGIF);
          end;
end;

procedure NextImage; //the "NextButton" button from the GUI calls this
begin
  if CurrentPos < Length(ImagesOfCurrentDir)-1 then
  begin
    CurrentPos := Succ(CurrentPos);
    CurrentFilename := ImagesOfCurrentDir[CurrentPos].Filename;
    CurrentFiletype := ImagesOfCurrentDir[CurrentPos].Filetype;
    UpdateTitleBar;
    LoaderThread := BeginThread(nil, 0, Addr(LoadImage), nil, 0, LoaderThreadID);
    CloseHandle(LoaderThread);
    while MainImageIsEmpty = true do
    begin
      if CurrentPos < Length(ImagesOfCurrentDir)-1 then
      begin
        CurrentPos := Succ(CurrentPos);
        CurrentFilename := ImagesOfCurrentDir[CurrentPos].Filename;
        CurrentFiletype := ImagesOfCurrentDir[CurrentPos].Filetype;
        UpdateTitleBar;
        LoaderThread := BeginThread(nil, 0, Addr(LoadImage), nil, 0, LoaderThreadID);
        CloseHandle(LoaderThread);
      end;
      if CurrentPos = CurrentDirImageCount-1 then Break;
    end;
  end;
  if CurrentPos = CurrentDirImageCount-1 then
  begin
    MainForm.NextButton.Enabled := false;
    MainForm.EndButton.Enabled := false;
    MainForm.SlideshowButton.Enabled := false;
    MainForm.SlideshowIntervalUpDown.Enabled := false;
  end;
  MainForm.PrevButton.Enabled := true;
  MainForm.StartButton.Enabled := true;
end;

procedure PrevImage; //called by "PrevButton"
begin
  //some code, calls LoadImage
  //almost the same logic as above for a backward step among the images
end;

procedure FirstImage; //called by "StartButton"
begin
  //some code, calls LoadImage
end;

procedure LastImage; //called by "EndButton"
begin
  //some code, calls LoadImage
end;

procedure Slideshow; //procedure #2 which should be used in a thread
begin
  while SlideshowOn = true do
  begin
    SlideshowInterval := MainForm.SlideshowIntervalUpDown.Position*1000;
    Sleep(SlideshowInterval);
    NextImage; //NextImage calls LoadImage which should be a thread
    if CurrentPos = CurrentDirImageCount-1 then SlideshowOn := false;
  end;
end;

function MainImageIsEmpty;
begin
  if MainForm.MainImage.Picture = nil then MainImageIsEmpty := true
  else MainImageIsEmpty := false;
end;

procedure TMainForm.NextButtonClick(Sender: TObject);
begin
  NextImage;
end;

procedure TMainForm.PrevButtonClick(Sender: TObject);
begin
  PrevImage;
end;

procedure TMainForm.StartButtonClick(Sender: TObject);
begin
  FirstImage;
end;

procedure TMainForm.EndButtonClick(Sender: TObject);
begin
  LastImage;
end;

procedure TMainForm.SlideshowButtonClick(Sender: TObject);
begin;
  if SlideshowOn = false then
    begin
      SlideshowOn := true;
      SlideshowThread := BeginThread(nil, 0, Addr(Slideshow), nil, 0, SlideshowThreadID);
      SlideshowButton.Caption := '||';
      SlideshowButton.Hint := 'DIAVETÍTÉS LEÁLLÍTÁSA';
    end
  else
    begin
      SlideshowOn := false;
      CloseHandle(SlideshowThread);
      SlideshowButton.Caption := '|>';
      SlideshowButton.Hint := 'DIAVETÍTÉS INDÍTÁSA';
    end;
end;
like image 754
Rev Avatar asked Jul 30 '11 20:07

Rev


2 Answers

There's a lot of text here, and not much code. Your question would probably be better with more code and less text.

Anyway, I can offer some hints.

Firstly, calling CreateThread directly is a rather laborious way to do threading in Delphi. It's easier to use TThread which wraps up some of the low-level Windows API issues in a manner more native to typical Delphi code style. Of course, you could go further and use a threading library like OmniThreadLibrary, but for now it may be better just to stick to TThread and work out how to do it that way.

Now, that won't be your problem here. Almost certainly your problem will be cause by one of two common issues with threading:

  1. All VCL and GUI code should run in the main thread. Windows controls have affinity with the thread that creates them. Many parts of the VCL are not thread-safe. These issues strongly push you to putting all VCL/GUI code in the main thread.
  2. It's quite possible that you have a race condition due to lack of synchronisation.

The most common way to deal with issue 1 is to call TThread.Synchronize or TThread.Queue from the worker threads in order to force all the VCL/GUI code to run on the main thread. Of course you need to be sure that none of the time-consuming code in your worker thread uses VCL/GUI objects since that is doomed to failure.

Issue 2 can be dealt with by synchronisation objects like critical sections or lock-free methods using the InterlockedXXX family of functions.

Exactly what your problem is I can't say. If you want more detailed help then please post more code, most probably cut down from what you are currently running.

like image 67
David Heffernan Avatar answered Nov 18 '22 21:11

David Heffernan


  1. You create a thread and kill it right away without waiting for it to finish loading
  2. LoadImage is not VCL thread safe

Here simple seudo thread in VCL way. Codes is in simple form and you can study further and make enhancement

TYourThread.Create(image file name);

type
TYourThread = class(TThread)
protected
  FBitmap: TBitmap;
  FImageFileName: string;
  procedure BitmapToVCL;
  begin
    MainForm.MainImage.Picture := FBitmap;
  end;     

  procedure Execute; override;
  begin
    FBitmap := TBitmap.Create;
    try
      FBitmap.LoadFromFile(FImageFileName);
      Synchronize(BitmapToVCL);
    finally
      FreeAndNil(FBitmap);
    end;
  end;
public
  constructor Create(const AImageFileName: string);
  begin
    FImageFileName := AImageFileName;
    inherited Create(False);
    FreeOnTerminate := True;
  end; 
end;

Gook luck Cheer

like image 40
APZ28 Avatar answered Nov 18 '22 23:11

APZ28