Threat içinden form nesnelerine ait eventlar oluşturmak

Başlatan bunalmis, 15 Nisan 2010, 20:29:20

z

#15
Thread ile Form arasinda mesajlaşma yapan bır program.

Formunuza memo1 ve 4 tane buton yerleştirin. Hemen deneyebir ve programı kendinize uyarlayabilirsiniz.

Butonlardan ilki memo1 i temizliyor.
Birisi threadi başlatıyor.

Diğer 2 butondan ilki  threda bana X yolla diye mesaj yolluyor.
Diğer buton ise threda bana Y yolla diye mesaj yolluyor.

Thread da kendine gelen mesaja uyarak forma x yada y yolluyor.

Formda ise gelen mesaj memo1e yazılıyor.

Program üzerinde oynadım ve bazı fazlalıklar var. 


unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls;

  const
  MyMsg=WM_USER+1;
  AMsg=WM_User+2;

type
  TForm1 = class(TForm)
  Button1: TButton;
    Memo1: TMemo;
    Xyolla: TButton;
    Label1: TLabel;
    Yyolla: TButton;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure OnMyMsg(var M:Tmessage); message MyMsg;
    procedure XyollaClick(Sender: TObject);
    procedure YyollaClick(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;


type
  TMyThread = class(TThread)
  protected
  procedure Execute; override;
  end;


var
  Form1: TForm1;
  MyThread:TMyThread;
  MyID:integer;
  msg:shortstring;
  i:integer;

implementation

{$R *.dfm}

procedure Tform1.OnMyMsg(var M:Tmessage);
var msgstrptr:PShortstring;
begin
        msgstrptr:=ptr(M.WParam);
        memo1.Lines.Add(msgstrptr^);
        dispose(msgstrptr);
end;

Procedure TMyThread.Execute;
var TmpMsg:TMsg;
PMsg:Pshortstring;
msgstrptr:PShortstring;
msg:Shortstring;
begin
        MyID:=getcurrentthreadid;
        msg:='A';
        while not terminated do
          begin
             i:=(i+1) and $7f;
             new(msgstrptr);
             msgstrptr^:=msg;
             postmessage(form1.Handle,MyMsg,integer(msgstrptr),0);
             sleep(10);
             if peekmessage(TmpMsg,0,AMsg,Amsg,PM_NoREMOVE) then
                begin
                  getmessage(TmpMsg,0,0,0);
                  PMsg:=ptr(TmpMsg.wparam);
                  msg:=pmsg^;
                  dispose(pmsg);
                  dispatchmessage(TmpMsg);
                end;  
          end;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
        MyThread:=TMyThread.Create(false);
end;

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
        MyThread.Terminate;
end;


procedure TForm1.XyollaClick(Sender: TObject);
var msg1:Pshortstring;
m:shortstring;
begin
        m:='X';
        new(msg1);
        msg1^:=m;
        postthreadmessage(MyID,AMsg,integer(msg1),0);
end;

procedure TForm1.YyollaClick(Sender: TObject);
var msg1:Pshortstring;
m:shortstring;
begin
        m:='Y';
        new(msg1);
        msg1^:=m;
        postthreadmessage(MyID,AMsg,integer(msg1),0);
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
        memo1.Text:='';
end;

end.
Bana e^st de diyebilirsiniz.   www.cncdesigner.com

z

#16
Çok büyük gelişmeler oldu ve sanırım Thread ile Forum arasındaki baglantiyi mesajlasma sayesinde yendim.

Yaşadığım sıkıntıları ve sebeplerini keşfettiğim kadarıyla açıklayayım. Keşfettiğim kadarıyla diyorum çünkü
visual dilleri hakkıyla bilen biri değilim. Gözlemlerime dayanarak açıklayacağım yanlışım varsa arkadaşlar düzeltir diye umuyorum.

1. Thread fonskiyon ile form fonskiyonları ortak değişkenler üzerinde işlem yapacaklar  ve bu değişken içerikleri her iki tarafça da sık sık okunarak yorumlanacaksa çok dikkatli olunmalı ve muhakkak criticalsection işlemi yapılmalıdır.

2. Thread fonskiyonu ile form nesnelerinin ıvır zıvır properties değerleri değiştirilmemelidir. Buna yapmaya çalıştığım click eventini
tetikleme de dahil.

3. Controller nikli üyemizin eleştirdiği ve ozaman kafama dank eden while do tipi klasik döngülü program yazımı windows için
uygun yapı değil aksi halde uzun döngüler, kullanıcının form üzerindeki nesnelerle oynayamaması anlamına geliyor.

Mesela benim thread olayına girmemin sebebi sonsuz döngü içindeyken form üzerindeki motor devrini ayarlayan sürgülü potu
mouse ile oynatamamamdı.

Bu nedenle wswsx in dediği gibi programları özellikle de uzun zaman alan program parçalarını muhakkak thread içinde
yazmamız gerekiyor.

Gelelim thread içinden form üzerindeki nesneleri tetikleme işine.

Bunu dün şans eseri bulduğum bir siteden mesaj yollama yöntemiyle halledilebileceğini öğrendim.

Thread içinden forma mesaj yolluyoruz. Mesajın ID si var bu bu ID ile ortama düşen mesaj ilgili prosedürün otomatik çalışmasını sağlıyor.

Benim işimi aşağıdaki örnek program çözdü görünüyor.

Thread ile uğraşmaya yeni başladıysanız aşağıdaki programı muhakkak deneyin.

Hemen bır form açın ve üstüne memo1 ve buton ekleyin.

Butonu tıkladığınızda thread çalışıyor ve forma mesaj yolluyor.
Mesaj OnFormMsg procedurunu başlatıyor.

Thread içinde istediğiniz kadar oyalanabilirsiniz. Bu ana formu hiç etkilemez. Thread görevini yaptığında form üzerindeki
procedüre otomatik işlediği için artık ufkunuz genişledi demektir. Hiçbir değişken içeriği polling yöntemi ile sınanmadığı,
görevini yap devret mantığı işlediği için başımın derdi olan windows uyarı mesajları ile programınız bölünmez.
Aşağıdaki program mantığında hiç kimse değişkenlere bir başka threat ile aynı anda ulaşmayacağı için global alanda istediğimiz
kadar ortak kullanıma açık değişken üzerinden veri aktarımı yapabiliriz.

Aşağıdaki programda thread program i değerini artırıyor. Eğer i=0 ise hata oldu değilse OK diyor. Bu yapıpyı anlamak için yaptığım basit bir test programı.

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls;

  const
  FormMsg=WM_USER+1;  // forma gelen mesajlarýn mesaj numarasý

type
  TForm1 = class(TForm)
  Button1: TButton;
    Memo1: TMemo;
    procedure Button1Click(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);

    procedure OnFormMsg(var M:Tmessage); message FormMsg;

  private
    { Private declarations }
  public
    { Public declarations }
  end;


type
  TMyThread = class(TThread)
  protected
  procedure Execute; override;
  end;


var
  Form1: TForm1;
  MyThread:TMyThread;
  msg:shortstring;
  i:integer;

implementation

{$R *.dfm}

procedure Tform1.OnFormMsg(var M:Tmessage);
var msgstrptr:PShortstring;
begin
        msgstrptr:=ptr(M.WParam);
        memo1.Lines.Add(msgstrptr^);
        Memo1.lines.Add(inttostr(i));
        dispose(msgstrptr);
end;

Procedure TMyThread.Execute;
var TmpMsg:TMsg;
PMsg:Pshortstring;
msgstrptr:PShortstring;
msg:Shortstring;
begin
        msg:='Ok';
        i:=(i+1) and $7;
        if i=0 then msg:='Hata';
        new(msgstrptr);
        msgstrptr^:=msg;
        postmessage(form1.Handle,FormMsg,integer(msgstrptr),0);
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
        MyThread:=TMyThread.Create(false);
end;

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
        MyThread.Terminate;
end;

end.
Bana e^st de diyebilirsiniz.   www.cncdesigner.com

t2

Daha önce verilen, Thread içinden dışarı string veya data göndermek için örnek:
threadDemo.zip - 411.7 Kb

ssSimpleIPC

birumher

#18
Thread içinden form üzerindeki componentlere veya forma erişme şansın var.
Yapılması gereken VCL componentlere erişirken synchronize komutu ile erişmek. Bu, programın görsel kısmının threadi ile bizim threadimizin çakışmaması için gerekli.
Delphi ni thread demosunda çok güzel bir örneği var.
Threadler create edilirken parametre olarak gerekli componentlerin referanslarını alıyorlar. Synchonize içinden de bu componentlerle istediklerini yapıyorlar.
Delphi Demolarından bulup inceleyebilirsin. SortThds dosyasının içeriği aşağıda...

Burada form üzerindeki componentlere erişen kısım DoVisualSwap procedure ü.
Dikkat edilirse muhakkak synchronize ile çalıştırılmış..
unit SortThds;

interface

uses
  Classes, Graphics, ExtCtrls;

type

{ TSortThread }

  PThreadSortArray = ^TThreadSortArray;
  TThreadSortArray = array[0..MaxInt div SizeOf(Integer) - 1] of Integer;

  TSortThread = class(TThread)
  private
    FBox: TPaintBox;
    FSortArray: PThreadSortArray;
    FSize: Integer;
    FA, FB, FI, FJ: Integer;
    procedure DoVisualSwap;
  protected
    procedure Execute; override;
    procedure VisualSwap(A, B, I, J: Integer);
    procedure Sort(var A: array of Integer); virtual; abstract;
  public
    constructor Create(Box: TPaintBox; var SortArray: array of Integer);
  end;

{ TBubbleSort }

  TBubbleSort = class(TSortThread)
  protected
    procedure Sort(var A: array of Integer); override;
  end;

{ TSelectionSort }

  TSelectionSort = class(TSortThread)
  protected
    procedure Sort(var A: array of Integer); override;
  end;

{ TQuickSort }

  TQuickSort = class(TSortThread)
  protected
    procedure Sort(var A: array of Integer); override;
  end;

procedure PaintLine(Canvas: TCanvas; I, Len: Integer);

implementation

procedure PaintLine(Canvas: TCanvas; I, Len: Integer);
begin
  Canvas.PolyLine([Point(0, I * 2 + 1), Point(Len, I * 2 + 1)]);
end;

{ TSortThread }

constructor TSortThread.Create(Box: TPaintBox; var SortArray: array of Integer);
begin
  FBox := Box;
  FSortArray := @SortArray;
  FSize := High(SortArray) - Low(SortArray) + 1;
  FreeOnTerminate := True;
  inherited Create(False);
end;

{ Since DoVisualSwap uses a VCL component (i.e., the TPaintBox) it should never
  be called directly by this thread.  DoVisualSwap should be called by passing
  it to the Synchronize method which causes DoVisualSwap to be executed by the
  main VCL thread, avoiding multi-thread conflicts. See VisualSwap for an
  example of calling Synchronize. }

procedure TSortThread.DoVisualSwap;
begin
  with FBox do
  begin
    Canvas.Pen.Color := clBtnFace;
    PaintLine(Canvas, FI, FA);
    PaintLine(Canvas, FJ, FB);
    Canvas.Pen.Color := clRed;
    PaintLine(Canvas, FI, FB);
    PaintLine(Canvas, FJ, FA);
  end;
end;

{ VisusalSwap is a wrapper on DoVisualSwap making it easier to use.  The
  parameters are copied to instance variables so they are accessable
  by the main VCL thread when it executes DoVisualSwap }

procedure TSortThread.VisualSwap(A, B, I, J: Integer);
begin
  FA := A;
  FB := B;
  FI := I;
  FJ := J;
  Synchronize(DoVisualSwap);
end;

{ The Execute method is called when the thread starts }

procedure TSortThread.Execute;
begin
  NameThreadForDebugging(AnsiString(ClassName));
  Sort(Slice(FSortArray^, FSize));
end;

{ TBubbleSort }

procedure TBubbleSort.Sort(var A: array of Integer);
var
  I, J, T: Integer;
begin
  for I := High(A) downto Low(A) do
    for J := Low(A) to High(A) - 1 do
      if A[J] > A[J + 1] then
      begin
        VisualSwap(A[J], A[J + 1], J, J + 1);
        T := A[J];
        A[J] := A[J + 1];
        A[J + 1] := T;
        if Terminated then Exit;
      end;
end;

{ TSelectionSort }

procedure TSelectionSort.Sort(var A: array of Integer);
var
  I, J, T: Integer;
begin
  for I := Low(A) to High(A) - 1 do
    for J := High(A) downto I + 1 do
      if A[I] > A[J] then
      begin
        VisualSwap(A[I], A[J], I, J);
        T := A[I];
        A[I] := A[J];
        A[J] := T;
        if Terminated then Exit;
      end;
end;

{ TQuickSort }

procedure TQuickSort.Sort(var A: array of Integer);

  procedure QuickSort(var A: array of Integer; iLo, iHi: Integer);
  var
    Lo, Hi, Mid, T: Integer;
  begin
    Lo := iLo;
    Hi := iHi;
    Mid := A[(Lo + Hi) div 2];
    repeat
      while A[Lo] < Mid do Inc(Lo);
      while A[Hi] > Mid do Dec(Hi);
      if Lo <= Hi then
      begin
        VisualSwap(A[Lo], A[Hi], Lo, Hi);
        T := A[Lo];
        A[Lo] := A[Hi];
        A[Hi] := T;
        Inc(Lo);
        Dec(Hi);
      end;
    until Lo > Hi;
    if Hi > iLo then QuickSort(A, iLo, Hi);
    if Lo < iHi then QuickSort(A, Lo, iHi);
    if Terminated then Exit;
  end;

begin
  QuickSort(A, Low(A), High(A));
end;

end.
Birkan.Herguner

z

#19
Thread  icinde synchronize yapacaginiza thread kodunu dogrudan form icine yazin ayni sey diye bir konu okumustum.

Bu arada en son verdigim ornegin Procedure TMyThread.Execute; un son satirina MyThread.Destroy satiri eklemek lazim.
Bana e^st de diyebilirsiniz.   www.cncdesigner.com

z

Alıntı yapılan: t2 - 16 Nisan 2010, 12:30:31
Daha önce verilen, Thread içinden dışarı string veya data göndermek için örnek:
threadDemo.zip - 411.7 Kb

ssSimpleIPC

T2 bu ornekte mesajlar aheste aheste geliyor. Mesajlarin son surat vizir vizir gelme durumu olamazmi?
Bana e^st de diyebilirsiniz.   www.cncdesigner.com

t2

Olur tabi neden olmasın .  bu örnek gözle görebilelim diye o şekilde yapıldı. dikkat edersen 1000mS lik sleep var . onu kaldır. tabi ozaman alıcı tarafta bir  sıkışma olabilir. Ama mesaj yerine ulaşmadan asla bırakmaz.  Bu yüzden  ben mesajı gönderirkern ayrı bir thread oluşturuyorum. o işin takipçisi oluyor. asıl iş aksamıyor.

Eğer mesajı alacak uygun bir alıcı yoksa sorun olmuyor kilitlenme olmuyor. Fakat alıcıda bir döngü varsa döngü işini bitirdikten sonra mesajı alabilir. işte o yüzden ayrı thread ile gönderiyorum.

t2

Alıcı tarafta da mesajı bir thread ile alırsan tadından yenmez. Form kendi işleriyle iştigal ederken gelen mesajlar itinayla değerlendirilir. veya asıl thread e mesaj gönderilir.

20ms yaptım süper oldu. Thread hem kendi formuna  hemde diğer forma mesaj gönderiyor.  tabi senin  uygulamada buffer beklenecek. beuufer hazırsa gönder denecek. 


procedure TThread1.Execute;
begin

  thss := TssSimpleIPC.Create(nil); // thread deki gönderici
  thss.IPCName := ss.IPCName; // isimler aynı olmazsa gönderilen mesaj ulaşmaz.
  // aynı isimde başka uygulama içinde bu nesneden varsa onlara da mesaj gönderilir.

  while (not self.Terminated) or (not application.Terminated) do
  begin

    thss.SendTextMessage(timetostr(now) + ' >Has thread mesaji :)');
    sleep(20);
    application.ProcessMessages;
    if application.Terminated then
      Terminate;
  end;

end;

hasangurlek

Alıntı yapılan: bunalmis - 16 Nisan 2010, 11:26:39
Mesela benim thread olayına girmemin sebebi sonsuz döngü içindeyken form üzerindeki motor devrini ayarlayan sürgülü potu
mouse ile oynatamamamdı.

Hocam thread kullanarak bu problemi aştınız ama arka planda çalışan thread içindeki sonsuz döngüde CPU e nefes aldırıyormusunuz ? Sonsuz döngüler fanın sürekli max devirlerde çalışmasına ve/veya CPU in aşırı ısınmasına sebep olabilir, bunun için programınız koşarken CPU kullanımına ve sıcaklığına ayrıca bakmanızı tavsiye ederim.
http://www.cyber-warrior.org, Although they like whiteness, sometimes twilight is required...  Hala evlilermi bilinmez ama kesinlikle artık uygun değiller !!!

z

#24
Threadi sonsuz dongude birakmam CPU nun sonsuz dongude kalmasina neden olmuyorki.
Sonucta sonsuz dongudeki thread, cpunun belli zaman araliklarinda isledigi rutin değilmi?
Bana e^st de diyebilirsiniz.   www.cncdesigner.com

z

Bu arada iyiki sordun.

PC deki islemci kullaniciya ait hic kod kosturmuyorsa yani islemci kullanimi %0 iken CPU ne yapiyor? Sleep moduna girip uyudugunu
sanmiyorum.

Sonucta biryerlerde volta atmiyormu? Bu islem de sonucta islemciye %100 yuk getirmiyormu?  Yoksa apayri bir olaylar mi var?
Bana e^st de diyebilirsiniz.   www.cncdesigner.com

hasangurlek

#26
Alıntı yapılan: bunalmis - 16 Nisan 2010, 15:55:07
Threadi sonsuz dongude birakmam CPU nun sonsuz dongude kalmasina neden olmuyorki.
Sonucta sonsuz dongudeki thread, cpunun belli zaman araliklarinda isledigi rutin değilmi?

Sonsuz döngü içinde CPU ya diğer processleri işletmesi için izin vermelisiniz. Bunu sleep, doevents vs. türü komutlarla yapabilirsiniz. CPU işletilecek processeslerin yoğunluğuna göre çalışma frekansını ayarlar, CPU kullanımı arttıkça çalışma hızı yükselir, frekans arttıkça ısı artar, ısıyı düşürmek için fan devri artırılır. Bilgisayar koruma amaçlı CPU max temperature reset özelliğine sahipse büyük ihtimalle bir müddet sonra resetlenir.

TEST
if eax==INVALID_HANDLE_VALUE
JMP TEST


Bunun gibi bir kodda eax içeriği sürekli INVALID_HANDLE_VALUE olursa CPU tüm zamanını bu kodu çalıştırmaya ayıracaktır. Bunun ne demek olduğunu zaten form üzerindeki nesneleri kumanda edemediğiniz zaman anlamıştınız. Ancak bu kod bir arka plan thread içindeyse görsel olarak birşey farkedilmez fakat cpu çalışma frekansı max seviyeye çıkar bununla birlikte fan max devirde çalışır. Notebook gibi soğutma problemi olan makinelerde sıkıntı daha çabuk ortaya çıkar.

Maalesef CPU nun yatacak zamanı olmaz, siz task managerde cpu kullanımını 0 görseniz bile 20 mili saniyelik periyodlarda pek çok process CPU yu kullanır durumdadır. http://technet.microsoft.com/tr-tr/sysinternals/bb896645%28en-us%29.aspx bu programla proceslerin oluşturduğu eventleri inceleyebilirsiniz.
http://www.cyber-warrior.org, Although they like whiteness, sometimes twilight is required...  Hala evlilermi bilinmez ama kesinlikle artık uygun değiller !!!

z

Cevapinizda aradiklarimi  bulamadim.

Programin isleyen bir bolumune While (true) do; gibi bir satirdan olusan kodu kullansam bile islemci gucunun tamamini kapamiyorum.
Threadi de gene benzer komutla sonsuz donguye soksam bile gene cpu gucunun tamamini kullanamiyorum.

Gorev yoneticiye gore CPU kullanimi %100 fakat mouse ile gorev yoneticiyi ordan buraya surukleyebiliyor, ekranda acik sayfalar arasinda gecis yapabiliyorum.

Bu tip dongulerle, sadece dongunun bulundugu programa ayrilmis islemci gucunun %100 unu kapabiliriz.
Buradaki islemci gucu cipin isleme kapasitesi (gucu) ile ayni anlama gelmiyor.

Sonucta siradan programlarda  %100 cpu gucunu kullanmak mumkun degil. Cunku her threada ayrilmis zaman dilimi belli.
Bana e^st de diyebilirsiniz.   www.cncdesigner.com

z

Thread icinden forma mesaj gondermeyle ilgili bir sorum olacak.

Formda bir butona bastigimizda bufferdaki verilerin thread fonskiyonuna devredildigini ve threadin islemeye basladigini,
thread fonksiyon da bufferdaki verileri  sirayla isledigini ve her isledigi veri icin forma su numarali veriyi isledim gibisinden
mesaj attigini varsayalim.

Ancak formda bu mesajlari yakalayan procedure bir sebeple mesajlarin bazilarini kacirmasi durumunda ne olur? Bu mesajlar
FIFO tarzi bir bufferda depolaniyor ve islendikce azaliyormu?

Bu kismi tam anlayamadim.


Bana e^st de diyebilirsiniz.   www.cncdesigner.com

hasangurlek

#29
Alıntı yapılan: bunalmis - 17 Nisan 2010, 00:23:05
Programin isleyen bir bolumune While (true) do; gibi bir satirdan olusan kodu kullansam bile islemci gucunun tamamini kapamiyorum.
Threadi de gene benzer komutla sonsuz donguye soksam bile gene cpu gucunun tamamini kullanamiyorum.

Gorev yoneticiye gore CPU kullanimi %100 fakat mouse ile gorev yoneticiyi ordan buraya surukleyebiliyor, 

Alıntı yapılan: hasangurlek - 16 Nisan 2010, 23:05:12
...Bunun ne demek olduğunu zaten form üzerindeki nesneleri kumanda edemediğiniz zaman anlamıştınız. Ancak bu kod bir arka plan thread içindeyse görsel olarak birşey farkedilmez fakat cpu çalışma frekansı max seviyeye çıkar bununla birlikte fan max devirde çalışır...

CPU i ne kadar süreyle %100 kapasitede çalıştırmayı düşünüyorsunuz ? Örneğin Sabah 8 akşam 17 gibimi, yoksa 7/24 mü ? veya bilgisayar açık kaldığı sürece ve yahut belirsiz bir süre boyunca kullanıcıya veya harici donanıma bırakılmış bir şekilde. Kodlarınızı sonsuz döngüler yerine olayları tetikleyecek şekilde veya bir olay olduğunda koşacak şekilde geliştirmelisiniz.

Alıntı yapılan: bunalmis - 17 Nisan 2010, 00:23:05
Bu tip dongulerle, sadece dongunun bulundugu programa ayrilmis islemci gucunun %100 unu kapabiliriz.
Buradaki islemci gucu cipin isleme kapasitesi (gucu) ile ayni anlama gelmiyor.

%100 yanlış !!!

Görev yöneticisinde gördüğünüz değer sadece sizin procesin kullanımı değil, CPU nun gerçek kullanım değeridir.

Alıntı yapılan: bunalmis - 17 Nisan 2010, 00:23:05
Sonucta siradan programlarda  %100 cpu gucunu kullanmak mumkun değil. Cunku her threada ayrilmis zaman dilimi belli.

%100 yanlış !!!

Eğer sadece zamana dayalı yürütme olsaydı Eventler icat edilmezdi,  Sadece 3 satır kodla CPU i sömürebilirsiniz.
http://www.cyber-warrior.org, Although they like whiteness, sometimes twilight is required...  Hala evlilermi bilinmez ama kesinlikle artık uygun değiller !!!