25. september 2002 - 14:05Der er
20 kommentarer og 3 løsninger
Der er noget galt med min pointer (tror jeg)!
Hejsa mine kære med-eksperter!
Jeg er ved at lave et program der kan resume downloads over et lokaltnetværk. Til det har jeg oprettet følgende klasser:
TFile (beskriver en enkelt fil) TFileDownload (arver fra en Thread, henter filen over netværket) TFileList (array af TFile's, holder styr på køer og TFileDownload's)
Brugeren ser status for filer, liste af filer osv., via et TListView. Property'en TListView.Items.Item.Data (Pointer) henviser til den TFile som ListView'ets Item bruger.
I TFile har jeg også en Pointer til en TListItem. Når TFile så ændrer sig (f.eks. der bliver downloadet nogle bytes), så er det meningen at det skal kunne ses i ListView'et.
Dog får jeg fejl hvis jeg skriver til TFile.ListItem^.Caption (f.eks.)...
Her er et eksempel: PFile = ^TFile lvFiles = TListView ListItem = ^TListItem
TFile = class(TObject) private vName: String; vSourceDir: String; vDestDir: String; vSize: Integer; vDownloaded: Integer; vID: Integer; vStatus: TFileStatus; function GetFullSource: String; function GetFullDestination: String; function IsNewFile: Boolean; procedure SetStatus(const Value: TFileStatus); procedure UpdateNewListItem; //Opdatere indholdet af TListItem der lige er tilføjet til ListView'et public ListItem: PListItem; //Krydsreferance mellem TFile og TListItem, så kan de holde styr på hinanden constructor CreateNew(ASourceFile, ADestDir: String; AID: Integer; Item: TListItem); constructor CreateFromSaved(AName, ASourceDir, ADestDir: String; ASize, ADownloaded, AID: Integer; Item: TListItem); constructor CreateResumeFile(SourceFile, DestDir: String; AID: Integer; Item: TListItem); //Bruges endnu ikke, men... destructor Destroy; override; published property Name: String read vName write vName; //Navn på filen uden mappe property SourceDir: String read vSourceDir write vSourceDir; //Kilde mappe, uden filnavn property DestDir: String read vDestDir write vDestDir; //Destinations mappe, uden filnavn property Size: Integer read vSize write vSize; //Filstørelse (bytes) property Downloaded: Integer read vDownloaded write vDownloaded; //Antal bytes hentet property ID: Integer read vID; //ID for fil (samme som i array'ets index Files[n].ID = n) property Status: TFileStatus read vStatus write SetStatus;//Status for filen, bruges endnu ikke property FullSource: String read GetFullSource; //Angiver hele filnavnet for kilden (mappe og filnavn) property FullDestination: String read GetFullDestination; //Angiver hele filnavnet for destinationen (mappe og filnavn) property NewFile: Boolean read IsNewFile; //Om der allerede er hentet noget af filen end;
PFile = ^TFile;
Her bruges f.eks. CreateNew:
constructor TFile.CreateNew(ASourceFile, ADestDir: String; AID: Integer; Item: TListItem); begin vName := ExtractFileName(ASourceFile); vSourceDir := ExcludeTrailingBackslash(ExtractFilePath(ASourceFile)); vDestDir := ExcludeTrailingBackslash(ADestDir); vSize := GetFileSize(ASourceFile); vDownloaded := 0; vID := AID; vStatus := fsNone; New(ListItem); //Jeg ved ikke helt om den skal oprettes... ListItem := @Item; UpdateNewListItem; end;
Den bliver created hvis jeg f.eks. henter en fil fra en OpenDialog:
var Dir: String; I: Integer; begin if openDlgFiles.Execute and SelectDirectory('Select destination directory', '', Dir) then begin lvFiles.AllocBy := lvFiles.Items.Count + openDlgFiles.Files.Count; for I := 0 to openDlgFiles.Files.Count -1 do FileList.AddNew(openDlgFiles.Files.Strings[I], Dir, lvFiles.Items.Add); end;
FileList = TFileList AddNew ser således ud:
procedure TFileList.AddNew(SourceFile, DestDir: String; ListItem: TListItem); var I: Integer; begin I := GetNewID; SetLength(vFiles, I+1); vFiles[I] := TFile.CreateNew(SourceFile, DestDir, I, ListItem); end;
GetNewID giver bare et tal der holder styr på index'et i array'et... Det behøver du ikke bekymre dig om! ;)
Håber i forstår meningen med det her : procedure TForm1.Button1Click(Sender: TObject); var LI : TListItem; //LI er en pointer, alle var. af typen TEtEllerAndet er pointers begin LI := ListView1.Items.Add; LI.Caption := 'TEST'; LI.Data := LI; TListItem(LI.Data).Caption := 'tester lige igen....'; end; //Klaus
Prøv at se her : Listitem.Data := @NewItem; ListItem.Data = pointer og NewItem = pointer hvis du skriver ListItem.Data := NewItem er det samme som : i := 1; a := 2; i := a; nu er i = 2 ikke? hvis du så skriver ListItem.Data := @NewItem det er det samme som : i := 1; variablen i ligger på adressen 123456789 a := 2; variablen a ligger på adressen 123456786 i := @a; nu er i = 123456786
Altså jeg ved at en klasse kun er en pointer til heap'en... Derfor har jeg lidt svært ved at se den helt store forskel mellem @ListItem og Pointer(ListItem)... Det er måske fordi Pointer(ListItem) giver pointeren til heap'en og @ListItem giver en pointer til pointeren til heap'en?
Jeg prøver det lige lidt senere... Har lidt travlt her til morgen! ;)
Jeg har ikke kigget din kode nærmere igennem, men kan det tænkes at problemet er at du har flere threads der tilgår samme TListView? Det kan nemlig give nogle "sjove" exceptions. Det er næsten dømt til at crashe før eller siden hvis der ikke er en eller anden form for synkronisering (TCriticalSection, mutex, Synchronize...).
Ok, vi kan tage et simpelt eksempel. Hvis du har en form med et memo, og laver f.ex. 10 threads der alle sammen kalder Form1.Memo1.Lines.Add('some text') så vil du få et crash før eller siden. VCL er ikke threadsafe, så hvis flere threads arbejder på samme komponent vil det næsten altid gå galt.
Hvordan får du fyldt dit TListView? Smider hver thread selv data i det, eller hvordan?
Programmet henter filer ind i ListView'et med en OpenDialog/SelectDirectory, og det er helt app-baseret... Det eneste sted der bruges Threads er når man henter en fil... Hvis jeg nu sætter et delay ind på f.eks. 500 ms for hver Thread der opdaterer i ListView'et, kan det så gå? Jeg kan godt se at det går galt hvis du hele tiden opdaterer noget fra flere Threads, men et delay på et par milisek. kan det ikke gå?
Prøv at fjerne al tilgang til ListView'et fra dine threads. Lav istedet en (eller flere) funktioner i form-klassen. Hvis vi tager mit memo som eksempel, så kan det se således ud:
var VCLSection : TCriticalSection // (syncObjs.pas)
procedure TForm1.AddMemoLines(Line: string); begin VCLSection.Enter; Memo1.Lines.Add(Line); VCLSection.Leave; end;
Hvis threads'ne alle kalder denne funktion, så kan de adde til memo'et lige så tosset de vil, uden crash.
Det eneste du skal være opmærksom på er at der også er en mainthread, dvs. den der kører når man sidder og klikker på formen. Den skal også benytte VCLSection hvis den arbejder på komponenten.
Det jeg synes er underligt er at jeg opdaterer indholdet af en ListItem når der smides en TFile i TFileList'en...
procedure TFile.UpdateNewListItem; begin with ListItem^ do begin Caption := vName; SubItems.Add(PrefixKB(vSize)); SubItems.Add(PrefixKB(vDownloaded)); SubItems.Add(GetFileStatus(vStatus)); SubItems.Add(vSourceDir); SubItems.Add(vDestDir); Data := @Self; end; end;
Den ses i slutningen af CreateNew constructor'en, og den virker jo fint nok... Kan problemet være at Data-property'en ikke kan sættes til @Self under constructoren?
Jeg har lige siddet med en kammerat og debugget (ret meget faktisk)...
Her er hvad vi kom frem til:
Ved UpdateNewListItem under Data := @Self; siger vi at @Self har adressen $69F890... Hvis jeg typecaster den til en TFile, så har den det indhold den skal...
Når jeg så skal lave en anden operation med TListItem.Data, så har den godt nok den rigtige adresse ($69F890)... Konverterer jeg så den til en TFile, så har den værdien nil!!! Hvorfor sker det?!?
Det gjorde jeg ved at ændre AddNew-proceduren for TFileList:
procedure TFileList.AddNew(SourceFile, DestDir: String; ListItem: TListItem); var I: Integer; begin I := GetNewID; SetLength(vFiles, I+1); vFiles[I] := TFile.CreateNew(SourceFile, DestDir, I, ListItem); ListItem.Data := @vFiles[I]; //<- den er ny! end;
I mellemtiden er der optrådt et nyt problem (igen en pointer)! :)
Fra min form kalder jeg en procedure:
procedure TFileList.AddToQueue(P: PFile); var I: Integer; begin //Lige meget (start) I := GetAvailableThread; if I = -1 then begin P^.Status := fsQueued; vQueue.Push(P); end //Lige meget (slut) else begin P^.Status := fsDownloading; vDownloads[I].Download(P); end; end;
TFile.Status er en property hvor write kalder proceduren:
procedure TFile.SetStatus(const Value: TFileStatus); begin ListItem^.SubItems.Strings[LV_COLUMNS_STATUS] := GetFileStatus(Value); end;
GetFileStatus og LV_COLUMNS_STATUS skal i ikke bekymre jer om, det virker!!! Det som ikke virker er ListItem^. Den er erklæret som en ^TListItem. I constructoren kalder jeg proceduren New(ListItem), hvilket jeg ikke er helt sikker på er nødvendigt, og den bliver selvfølgelig Dispose'd i destructoren... Det underlige er nu at jeg ikke kan skrive til ListItem'en i den procedure...
Jeg har også lige prøvet at lave TFile's constructor om så der i stedet følger en PFile med som parameter... Det virkede heller ikke! :( Nogle bud?
Hejsa... Hvis i stadig kan huske mig, så tænkte jeg på følgende:
Hvis jeg i min TFile laver pointer-variablen til en TListItem, altså ^TListItem, om til en TListItem-variabel, kan det så komme til at kræve for mange ressourcer, eller bliver det bare det samme som før da det jo egentlig er en pointer til heap'en!?
HURRA!!!!!!!!!!! Jeg fik det til at virke... Jeg gjorde som beskrevet for oven ved at bruge en klasse som variabel i stedet for en pointer... Rart!!! :)
I får lidt lidt point for besværet og den lange svartid... ;)
>>zimp Du må også godt lige lægge et svar, så deler jeg nogle point ud... ;)
Tilladte BB-code-tags: [b]fed[/b] [i]kursiv[/i] [u]understreget[/u] Web- og emailadresser omdannes automatisk til links. Der sættes "nofollow" på alle links.