Autor Beitrag
delfiphan
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 2684
Erhaltene Danke: 32



BeitragVerfasst: Fr 28.08.09 16:44 
Hallo!

Vielleicht kennen einige das Keyword yield in C#. Salopp ausgedrückt ist es damit möglich, eine Ansammlung von Elementen nach und nach zurückzugeben, ohne alle Elemente zuerst in eine Liste zu packen und als ganzes zurückgeben zu müssen. Damit ist es möglich, gewisse Algorithmen besonders elegant zu implementieren (siehe dazu auch CoRoutines).

Ein einfaches Beispiel hier:
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
    public class Test
    {
        public static IEnumerable<int> GetNumbers()
        {
            for (var i = 0; i < 5; i++) {
                yield return i;  // bricht nicht ab sondern gibt alles nacheinander zurück
            }
        }

        static void RunTest()
        {
            foreach (var i in GetNumbers())
            {
                Console.Write("{0} ", i);
            }
        }
    }


Weder yield noch CoRoutines werden von Delphi direkt unterstützt.

Aber es gibt verschiedene Möglichkeiten, das zu implementieren. Eine relativ einfache Möglichkeit ist es, mit Win32-Fibers zu arbeiten.

Hier ist also eine Delphi-Klasse, die sowas ähnliches wie das C# yield zur Verfügung stellt. Die Methode Enumerate im folgenden Beispielprojekt gibt Strings mittels Yield zurück, welche nach und nach in der for-Schleife im Hauptprogramm angezeigt werden.

Der Vorteil ist, dass die Methode Enumerate keine Liste zusammenstellt, welche sie dann zurückgibt. Die einzelnen Elemente werden erst dann generiert, wenn sie von der for-Schleife angefordert wurden.

D.h. im Programm wird das erste Element angezeigt, ohne, dass die restlichen Elemente schon generiert wurden. Es ist sogar möglich, dass die Liste unendlich lang ist und es gar nicht möglich wäre, alle Elemente vorzuberechnen und z.B. in eine TStringList zu packen...

Um eine solche Art von "Pipelining" zu ermöglichen, bräucht man normalerweise Callbacks oder State-Maschines (z.B. case-statement oder ifs). Entsprechend würde die Lesbarkeit des Codes darunter leiden.

Hier ein Beispielprojekt (Konsolenapplikation). Die Unit ist im Anhang. TMyList implementiert eine unendlich lange Liste in TMyList.Enumerate. Die einzelnen Einträge werden jeweils mit Yield zurückgegeben und im Hauptprogramm angezeigt.

ausblenden volle Höhe Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
{$APPTYPE Console}
uses SysUtils, DphEnumerator;

type
  TMyList = class(TDphEnumeratorList)
  protected
    procedure Enumerate; override;
  end;

procedure TMyList.Enumerate;
  procedure RecursionTest(Level: Integer);
  begin
    Yield('We are at recursion level ' + IntToStr(Level));
    if Level < 3 then
      RecursionTest(Level + 1);
  end;
var
  I: Integer;
begin
  // Output some test strings
  Yield('Hello');
  Yield('World');
  RecursionTest(0);

  I := 0;
  repeat
    Yield('Test ' + IntToStr(I));
    Inc(I);
  until False; // infinite
end;
// [hier zusammenkleben]

Hier ist der Hauptteil der Applikation:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
// [hier zusammenkleben]
var
  List: TMyList;
  S: string;
begin
  List := TMyList.Create;
  try
    // Liste ausgeben bis zum Eintrag "Test 5"
    for S in List do
    begin
      Writeln(S);
      if S = 'Test 5' then
        Break;
    end;

  finally
    List.Free;
  end;
  readln;
end.


Das Programm gibt folgendes aus:
ausblenden Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
Hello
World
We are at recursion level 0
We are at recursion level 1
We are at recursion level 2
We are at recursion level 3
Test 0
Test 1
Test 2
Test 3
Test 4
Test 5


Ob das ganze jetzt in Delphi in dieser Form brauchbar ist, darüber möchte ich jetzt nicht philosophieren. Das ganze hat aber zu mindest einen gewissen akademischen Wert. ;)

Ich habe keine Performance-Tests gemacht. Das ganze könnte man auch ohne Win32 Fibers implementieren (mit einer Portion Assembler).
Einloggen, um Attachments anzusehen!
Boldar
ontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic starofftopic star
Beiträge: 1555
Erhaltene Danke: 70

Win7 Enterprise 64bit, Win XP SP2
Turbo Delphi
BeitragVerfasst: Fr 28.08.09 21:42 
Interessante Sache, aber nur wenn die performance auch stimmt im Vergleich z.B. zur Callbacks-Variante.
Das müsste man dann halt mal testen...
delfiphan Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 2684
Erhaltene Danke: 32



BeitragVerfasst: Fr 28.08.09 23:12 
Es ist schon ein gewisser zusätzlicher Verwaltungsaufwand da. Ein einfacher Callback ist natürlich schneller als ein Fiber-Switch. Aber Fibers sind ja grundsätzlich ganz in Usermode implementiert. Da ist also nichts allzu Ungewöhnliches dabei. Man könnte sich also die Fibers nehmen und alles weglassen, was für Delphi irrelevant ist. Das würde das ganze beschleunigen.

Verbietet man z.B. Exception-Blöcke und lokale Variablen und Aufrufe ausserhalb der Methode, wirds relativ simpel. Je mehr Features man zulassen will, desto mehr gibt es zu berücksichtigen. Im Gegensatz zu C# ist bei der obigen Implementation mit Fibers ziemlich alles erlaubt. Try..Except/Try..Finally Blöcke, Rekursionen, theoretisch kann man die Kontrolle auch an eine weitere Methode weitergeben, die dann wiederum Yield aufruft.

Es sei erwähnt, dass man in C# ziemlich eingeschränkt ist. Das liegt daran, dass der Code zur Compilezeit so umgebaut wird, dass er am Schluss wieder aus "normalem", sequentiellen Code besteht. Es kommen keine Fibers zum Einsatz sondern der Compiler generiert sich irgend ein gigantisches Switch-statement. Zur Laufzeit wird die generierte Methode dann immer wieder aufgerufen, einfach mit anderen Parametern (bzw. State-Parameter). Damit dieses Umbauen überhaupt möglich ist, sind gewisse Sachen nicht erlaubt. Z.B. kann ein yield nicht innerhalb eines try..catch Blockes vorkommen und darf nur genau in einer solchen speziellen Methode gebraucht werden.

Geschwindigkeit ist nur ein Argument. Ein Callback kannst du nicht direkt mit dem hier vergleichen. Du kannst mit Callbacks alleine nicht in aller Allgemeinheit Pipelining betreiben und nicht ohne Weiteres mehrere Module übereinander stülpen.
BenBE
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 8721
Erhaltene Danke: 191

Win95, Win98SE, Win2K, WinXP
D1S, D3S, D4S, D5E, D6E, D7E, D9PE, D10E, D12P, DXEP, L0.9\FPC2.0
BeitragVerfasst: Sa 29.08.09 00:04 
Das ist das Erste Mal in 5 Jahren, seit dem ich weiß, dass die WinAPI Fibers hat, dass ich eine sinnvolle Einsatzmöglichkeit dafür sehe ;-) Werd mir das bei Gelegenheit mal von der Implementierung her ansehen ;-)

_________________
Anyone who is capable of being elected president should on no account be allowed to do the job.
Ich code EdgeMonkey - In dubio pro Setting.
delfiphan Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 2684
Erhaltene Danke: 32



BeitragVerfasst: Sa 29.08.09 00:59 
Hier noch ein komplizierteres Beispiel, wo 6 "Module" übereinandergestülpt werden. So einfach lässt sich das kaum auf andere Art implementieren.

Das Beispiel nimmt den String "nemmoklliW ni red !ekcerelkciwtnE seiD tsi nie retiewz .ztaS leiV !ssapS" als Stream. Darauf werden 6 Aktionen angewandt.

1. Der String wird getrennt in einzelne Wörter
2. Die Reihenfolge der Buchstaben der einzelnen Wörter werden umgekehrt
3. Satzende werden markiert
4. Die einzelnen Wörter werden gemäss Markierungen zusammengefügt
5. Die Sequenz wird verdoppelt (2x ausgegeben)
6. Die einzelnen Strings werden durchnummeriert

Das ist jetzt offensichtlich ein konstruiertes Beispiel. Die Schritte könnten natürlich zusammengefasst werden aber darum geht es jetzt nicht. Hier sollen sie strikt voneinander getrennt implementiert werden. Die einzelnen Schritte sind eben Module, die Eingabesequenzen in Ausgabesequenzen umwandeln. Sie funktionieren wie Pipes in DOS/Unix und können beliebig aneinandergereiht werden, sind aber getrennt voneinander implementiert.

Es sei nochmals betont, dass hier keine Listen manipuliert und von Modul zu Modul weitergegeben werden. Der Eingabestring könnte eine ganze Festplatte füllen - trotzdem würde die Applikation sofort mit der Ausgabe beginnen.

Der Code sieht dann so aus:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
// [hier zusammenkleben]
var
  S: string;
  List: TDphEnumeratorList;
  Stream: TStream;

begin
  Stream := TStringStream.Create('nemmoklliW ni red !ekcerelkciwtnE seiD tsi nie retiewz .ztaS leiV !ssapS');
  try
    List := TNumberItems.Create(
              TRepeater.Create(
                TImplodeByMarkers.Create(
                  TMarkSentences.Create(
                    TReverseItems.Create(
                      TExplodeStream.Create(Stream))))));
    try
      for S in List do
        WriteLn(Format('%-40s Stream.Position = %d',[S, Stream.Position]));
    finally
      List.Free;
    end;
  finally
    Stream.Free;
  end;
  readln;
end.

Die Ausgabe davon ist:
ausblenden Quelltext
1:
2:
3:
4:
5:
6:
1. Willkommen in der Entwicklerecke!     Stream.Position = 34
2. Dies ist ein zweiter Satz.            Stream.Position = 61
3. Viel Spass!                           Stream.Position = 72
4. Willkommen in der Entwicklerecke!     Stream.Position = 34
5. Dies ist ein zweiter Satz.            Stream.Position = 61
6. Viel Spass!                           Stream.Position = 72


Der Code der einzelnen Module ist jeweils trivial:
ausblenden volle Höhe Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
99:
100:
101:
102:
103:
104:
105:
106:
107:
108:
109:
110:
111:
112:
113:
114:
115:
116:
117:
118:
119:
120:
121:
122:
123:
124:
125:
126:
127:
128:
129:
130:
131:
132:
133:
134:
135:
136:
137:
138:
139:
140:
141:
142:
143:
144:
145:
146:
{$APPTYPE Console}
uses SysUtils, Classes, StrUtils, DphEnumerator;

type
  TExplodeStream = class(TDphEnumeratorList)
  private
    FStream: TStream;
  public
    constructor Create(Input: TStream);
    procedure Enumerate; override;
  end;

  TInput = class(TDphEnumeratorList)
  protected
    FInput: TDphEnumeratorList;
  public
    constructor Create(Input: TDphEnumeratorList);
    destructor Destroy; override;
  end;

  TReverseItems = class(TInput)
    procedure Enumerate; override;
  end;

  TMarkSentences = class(TInput)
    procedure Enumerate; override;
  end;

  TImplodeByMarkers = class(TInput)
    procedure Enumerate; override;
  end;

  TRepeater = class(TInput)
    procedure Enumerate; override;
  end;

  TNumberItems = class(TInput)
    procedure Enumerate; override;
  end;
  
{ TExplodeStream }

constructor TExplodeStream.Create(Input: TStream);
begin
  FStream := Input;
end;

procedure TExplodeStream.Enumerate;
var
  S: string;
  C: Char;
begin
  FStream.Position := 0;
  while FStream.Read(C, SizeOf(C)) = SizeOf(C) do
  begin
    if C = ' ' then
    begin
      Yield(S);
      S := '';
    end else
      S := S + C;
  end;
  Yield(S);
end;

{ TReverseItems }

procedure TReverseItems.Enumerate;
var
  S: string;
begin
  for S in FInput do
    Yield(ReverseString(S));
end;

{ TMarkSentences }

procedure TMarkSentences.Enumerate;
var
  S: string;
begin
  for S in FInput do
  begin
    Yield(S);
    if (S <> ''and (S[Length(S)] in ['.''!''?']) then
      Yield('<marker>');
  end;
end;

{ TImplodeByMarkers }

procedure TImplodeByMarkers.Enumerate;
var
  S, T: string;
begin
  for S in FInput do
  begin
    if S = '<marker>' then
    begin
      Yield(T);
      T := '';
    end else
      T := T + S + ' ';
  end;
end;

{ TRepeater }

procedure TRepeater.Enumerate;
var
  S: string;
begin
  for S in FInput do
    Yield(S);
  for S in FInput do
    Yield(S);
end;

{ TNumberItems }

procedure TNumberItems.Enumerate;
var
  S: string;
  I: Integer;
begin
  I := 1;
  for S in FInput do
  begin
    Yield(Format('%d. %s', [I, S]));
    Inc(I);
  end;
end;

{ TInput }

constructor TInput.Create(Input: TDphEnumeratorList);
begin
  FInput := Input;
end;

destructor TInput.Destroy;
begin
  FInput.Free;
  inherited;
end;
// [hier zusammenkleben]


Wenn jemand eine konkrete Anwendung dafür findet bzw. die Performance braucht, kann ich bei Gelegenheit auch mal versuchen, eine Non-Fiber Alternative zu schreiben. So kompliziert dürfte das eigentlich nicht sein (wenn ich nichts übersehe). Für einen Kontextswitch muss man sich jeweils den Stack Pointer, Instruction Pointer, und Exception Handler merken; die meisten Registers kann mittels asm-Befehl pushad/popad auf einen Schlag speichern/laden. Das wäre halt wieder ein mehr oder weniger handgestricktes Fiber. ;)
delfiphan Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 2684
Erhaltene Danke: 32



BeitragVerfasst: Sa 29.08.09 18:02 
Hier ein paar Performance Test. Ich habe jetzt den Context-Switch noch von Hand programmiert (ein Context-Switch lässt sich in ca. 20 Zeilen Assembler realisieren). Der Geschwindigkeits-Unterschied zu den Win32-Fibers ist aber relativ klein.

Performance-Test mit 10'000'000 Durchläufen. Pro Iteration wird 1 String zurückgegeben.

Resultat
Einfacher Funktionsaufruf (1): 836 ms
DphEnumerator ohne Fibers (2): 1326 ms
DphEnumerator mit Win32-Fibers (2): 1581 ms

(1)
ausblenden Delphi-Quelltext
1:
2:
3:
4:
function GetString(I: Integer): string;
begin
  Result := 'Test';
end;


(2)
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
procedure TTest.Enumerate;
var
  I: Integer;
begin
  for I := 0 to N - 1 do
    Yield('Test');
end;


Interpretation
Der einfache Funktionsaufruf ist 1.59x schneller als Yield. Wenn man bedenkt, dass der Funktionsaufruf lediglich eine Stringzuweisung macht, ist das nicht schlecht. Das Yield kostet also nicht mal eine zusätzliche Stringzuweisung.

Wenn man jedoch nicht mit Strings arbeiten will, sondern mit Integers oder Chars, dann lohnt es sich eher nicht mehr. Der Overhead wäre dann zu gross. Der Aufruf einer leeren Funktion (procedure X; begin end;), ohne Eingabe/Ausgabeparameter ist ca. 20x schneller als eine Iteration mit Yield (= 2 Context Switches).
--> Ein Contextswitch kostet also ca. 10 leere Prozeduraufrufe.
Kha
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 3803
Erhaltene Danke: 176

Arch Linux
Python, C, C++ (vim)
BeitragVerfasst: Sa 29.08.09 20:07 
user profile iconBenBE hat folgendes geschrieben Zum zitierten Posting springen:
Das ist das Erste Mal in 5 Jahren, seit dem ich weiß, dass die WinAPI Fibers hat, dass ich eine sinnvolle Einsatzmöglichkeit dafür sehe ;-)
Du stiehlst mir die Worte aus dem Munde ;) . Geniale Idee :zustimm: .

Dass die Iterators von C# (quasi von innen) ein Spezialfall von Coroutines sind, hast du ja schon erwähnt. Von außen (IEnumerabl allgemein) ist es ein Spezialfall/Nachahmung von Lazy Evaluation, stammt also wie quasi jedes Feature der letzten zwei C#-Versionen aus der funktionalen Programmierung. Allerdings sind die Listen beispielsweise von Haskell insofern mächtiger, als dass sie lazy sind, trotzdem falls nötig die bereits enumerierten Daten speichern. In C# sollte man dagegen im Allgemeinen vermeiden, ein IEnumerable mehrmals zu durchlaufen und wird so oft gezwungen, die Daten mitten im Algorithmus doch wieder eager in einer List<T> zu cachen. Solche netten Sachen wie
ausblenden Quelltext
1:
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)					

sind mit IEnumerable wirklich keine gute Idee ;) .
Die .NET-BCL kennt solche lazy Listen zwar nicht, ich will aber nicht behaupten, dass man sie in C# oder Delphi nicht nachbauen könnte - wurde auch schon getan.

user profile icondelfiphan hat folgendes geschrieben Zum zitierten Posting springen:
Es sei erwähnt, dass man in C# ziemlich eingeschränkt ist.
Das liegt aber weniger an der Transformation in eine State Machine als an der Frage nach dem Sinn ;) . Fangen wir mit yield im try eines try-finally an: Angenommen, wir iterieren über eine Datei und wollen das Handle im finally natürlich wieder freizugeben. Wenn die Exception in der Yield-Methode auftritt, funktioniert alles wie gewünscht, wenn sie in der for-Schleife auftritt, passiert dagegen überhaupt nichts - wo sollte die Anwendung auch in die Yield-Methode zurückspringen? Diesen Fall erlaubt C# sogar, indem der finally-Code in die Dispose-Methode des IEnumerators gesteckt wird. yield in finally und catch machen aber aus ähnlichen Gründen einfach keinen Sinn, ich würde sie also nicht unbedingt in deiner Feature-Matrix erwähnen :zwinker: .

Zwei "Nice-To-Have"-Features, die du genannt hast - Rekursion und die Möglichkeit, nur einen Teil der Methode zum Iterator zu machen - bietet C# nicht, aber zum Beispiel F#, das genau die gleiche Transformation benutzt.
ausblenden Quelltext
1:
2:
3:
4:
let rec nums i =
    seq {
        yield i
        yield! nums (i+1) }

ausblenden Quelltext
1:
2:
> nums 3;;
val it : seq<int> = seq [3; 4; 5; 6; ...]

Interessanterweise hat das F#-Team zuerst auf Monads gesetzt (der Code nach jedem yield landete in einer Continuation), ist dann aber wegen der grausigen Performance ebenfalls auf die komplizierten State Machines umgeschwenkt.


Ich könnte mit der yield-Unterstützung von C# gut leben, denn der nächste Schritt besteht sowieso darin, so wenig yield verwenden zu müssen wie nötig ;) , indem eine Reihe von "Modul-Primitiven" bereitgestellt wird, die über Higher-Order-Functions die meisten selbstgestrickten Iteratoren ersetzen können. In C# leben die in Enumerable und stammen natürlich allesamt quasi direkt aus funktionalen Sprachen, allen voran das berühmte Trio map, filter, fold.
...und zugegebenermaßen fragt man sich dann irgendwo dort, warum man nicht gleich eine funktionale Sprache benutzt :| .

user profile icondelfiphan hat folgendes geschrieben Zum zitierten Posting springen:
Geschwindigkeit ist nur ein Argument. Ein Callback kannst du nicht direkt mit dem hier vergleichen. Du kannst mit Callbacks alleine nicht in aller Allgemeinheit Pipelining betreiben und nicht ohne Weiteres mehrere Module übereinander stülpen.
Wie Eric Lippert im Link zu yield in try oben geschrieben hat, sind ein Pull-Modell wie IEnumerable und ein Push-Modell über Callbacks eigentlich isomorph. Mithilfe einer geeigneten Lib sollte das Ergebnis nicht viel anders als mit LINQ aussehen und ist auf jeden Fall modular:
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
var pieces =
  new DataProducer<string>()
  .Explode() // selbst schreiben bitte ;)
  .Select(s => s.Reverse());
  ...
  .ForEach(Console.WriteLine);

pieces.ProduceAndEnd("...");

Anstatt dass jedes Modul Daten vom davorliegenden anfordert, schiebt jedes die Daten zum nächsten. Kein großer Unterschied, nur das Einsammeln des Ergebnisses kann ggf. etwas anders laufen.

_________________
>λ=
delfiphan Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 2684
Erhaltene Danke: 32



BeitragVerfasst: Sa 29.08.09 22:52 
Ich hab auch lange nicht gewusst, wie man Fibers einsetzen kann. Dann sah ich C# yield und dachte, das müsste auch in Delphi mit Fibers implementierbar sein. Und siehe da, die sind wie dafür geschaffen :)

user profile iconKha hat folgendes geschrieben Zum zitierten Posting springen:
... sollte das Ergebnis nicht viel anders als mit LINQ aussehen und ist auf jeden Fall modular:

In C# sieht es mit LINQ am Schluss vielleicht so aus:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
Stream
 .ExplodeStream()
 .ReverseItems()
 .MarkSentences()
 .ImplodeByMarkers()
 .Repeater()
 .TNumberItems();

aber ich denke semantisch passiert dann trotzdem sowas wie folgendes (oder?):

ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
TNumberItems.Create(
  TRepeater.Create(
    TImplodeByMarkers.Create(
      TMarkSentences.Create(
        TReverseItems.Create(
          TExplodeStream.Create(Stream))))));


Die Verschachtelung wird über C# Extension Methods zusammengetrickst.

Wenn wir schon dabei sind :) das geht auch in Delphi mit class helpers (sieht man eher selten, sind auch etwas buggy wenn man sie zu viel verwendet). Einfach folgendes hinzugefügt:

ausblenden volle Höhe Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
  TDphEnumeratorListHelper = class helper for TDphEnumeratorList
    function ReverseItems: TDphEnumeratorList;
    function MarkSentences: TDphEnumeratorList;
    function ImplodeByMarkers: TDphEnumeratorList;
    function Repeater: TDphEnumeratorList;
    function NumberItems: TDphEnumeratorList;
  end;

function TDphEnumeratorListHelper.ReverseItems: TDphEnumeratorList;
begin
  Result := TReverseItems.Create(Self);
end;

function TDphEnumeratorListHelper.MarkSentences: TDphEnumeratorList;
begin
  Result := TMarkSentences.Create(Self);
end;

function TDphEnumeratorListHelper.ImplodeByMarkers: TDphEnumeratorList;
begin
  Result := TImplodeByMarkers.Create(Self);
end;

function TDphEnumeratorListHelper.Repeater: TDphEnumeratorList;
begin
  Result := TRepeater.Create(Self);
end;

function TDphEnumeratorListHelper.NumberItems: TDphEnumeratorList;
begin
  Result := TNumberItems.Create(Self);
end;

und schon kann ich dieselbe Syntax in Delphi verwenden:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
List := TExplodeStream.Create(Stream)
         .ReverseItems
         .MarkSentences
         .ImplodeByMarkers
         .Repeater
         .NumberItems;


user profile iconKha hat folgendes geschrieben Zum zitierten Posting springen:
Wie Eric Lippert im Link zu yield in try oben geschrieben hat, sind ein Pull-Modell wie IEnumerable und ein Push-Modell über Callbacks eigentlich isomorph.

Das ist bestimmt so. Man kann damit nichts, was man nicht schon vorher konnte. Das "Problem" ist halt, dass der Callback jedesmal von Anfang ausgeführt wird. Wenn der Algorithmus hinter dem Callback selbst eine komplexe, womöglich verschachtelte Sache ist, muss er sich den Stand immer merken und bei jedem Call wieder dorthin zurückkehren können. Rekursionen musst du dann wohl auch in Iterationen umwandeln. Gehen tut es bestimmt, aber es ist nicht mehr sehr elegant.

Beispiel: Wenn der Callback ein Mathe-Parser (Bottom-Zp-Parser) ist, dann ist es denkbar ungünstig, wenn die Eingabedaten gepusht werden. Der tyParser arbeitet z.B. rekursiv. Bei komplizierten Ausdrücken hat man schnell mal einen sehr tiefen Callstack. Praktisch in jeder Rekursionstiefe holt er sich "das nächste Zeichen" aus dem String und arbeitet damit weiter. Der Parser ist so konzipiert, dass er einmal von links nach rechts durch den Ausdruck läuft und danach das Resultat hat.

Ich müsste jetzt den Artikel über Push und Pull durchlesen, aber ich sehe nicht, wie man das irgendwie ohne Weiteres mit Callbacks gebacken kriegt, ohne den Code komplett umstrukturieren zu müssen. :) Wenn du ohne CoRoutines (yield usw.) Module haben willst, musst du die alle als Callback schreiben (egal ob Eingabe in Ausgabe gepusht oder Ausgabe von Eingabe gepullt werden). Durch die Callbacks verlieren die Module aber einen eigenen Kontrollfluss. Die Callbacks müssen für jeden Call neu gestartet werden können und muss sich erinnern, wo er steckengeblieben ist. Das mag für einen StreamReader trivial sein, nicht aber für einen rekursiven Parser :) Genau um das zu vereinfachen kann man CoRoutines nutzen. Man kann es als Hilfsmittel sehen.

// Edit: Syntaxfehler gefixt


Zuletzt bearbeitet von delfiphan am So 30.08.09 15:41, insgesamt 1-mal bearbeitet
Kha
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 3803
Erhaltene Danke: 176

Arch Linux
Python, C, C++ (vim)
BeitragVerfasst: So 30.08.09 01:11 
user profile icondelfiphan hat folgendes geschrieben Zum zitierten Posting springen:
aber ich denke semantisch passiert dann trotzdem sowas wie folgendes (oder?):
Jupp, exakt. Und wenn es jetzt noch Lambdas für Win32 gäbe (was aber ohne GC der Übersichtlichkeit schaden dürfte), könnte man damit wirklich LINQ 1:1 übernehmen. Dass es Class Helpers nicht für Interfaces gibt, wäre noch ein kleines Manko, aber Interfaces sind in Delphi.Win32 ja sowieso nicht weit verbreitet.

user profile icondelfiphan hat folgendes geschrieben Zum zitierten Posting springen:
Das "Problem" ist halt, dass der Callback jedesmal von Anfang ausgeführt wird. Wenn der Algorithmus hinter dem Callback selbst eine komplexe State-Maschine ist, muss er sich die States immer merken und dorthin zurückkehren.
Stimmt, das Module-Schreiben wird auf jeden Fall lästiger. Aber im "Ursprung" ändert sich im Vergleich zu yield quasi nichts und für die weitere Pipeline will ich mich wie gesagt möglichst auf schon vorhandene Module verlassen ;) .
Dass das "Einsammeln des Ergebnisses [...] etwas anders laufen" kann, war vielleicht ein Understatement, es ist vielmehr das Kriterium, um sich zwischen den zwei Modellen zu entscheiden. Bei sequentiellem Abarbeiten wie in einem Parser macht ein Push-Modell wirklich keinen Sinn (wobei...mit Monads könnte man da was drehen :D ...), wenn die Reihenfolge dagegen unwichtig ist und vor allem mehrere Sinks benötigt werden, sieht es schon besser aus - die restliche Erklärung überlasse ich lieber mal Jon Skeet im Push-Linqk oben ;) .

Bzw. ein einfaches Beispiel: Generische Min- und Max-Module wird wohl jede gut sortierte Lib bieten, egal ob Push oder Pull. Wenn ich von einer Sequenz nun aber beide Extremwerte brauche, habe ich in Pull ein Problem, denn ich kann über die Sequenz eben nur genau einmal enumerieren. Zum Glück brauche ich wenigstens keinen neuen yield-Iterator, sondern kann die Primitive einsetzen:
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
Tuple<T, T> minMax =
  mySeq.Aggregate((acc, x) => {
    if (acc == null)
      return Tuple.Create(x, x);
    else
      return Tuple.Create(Compare.Min(acc.Item1, x), Compare.Max(acc.Item2, x))
  }, null);

Console.WriteLine("{0}-{1}", minMax.Item1, minMax.Item2);


Beim Push-Modell sind mehrere Sinks dagegen kein Problem.
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
var producer = new DataProducer<T>();
IFuture<T> min = producer.Min();
IFuture<T> max = producer.Max();

producer.ProduceAndEnd(mySeq);
Console.WriteLine("{0}-{1}", min.Value, max.Value);

_________________
>λ=
delfiphan Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 2684
Erhaltene Danke: 32



BeitragVerfasst: So 30.08.09 18:36 
user profile iconKha hat folgendes geschrieben Zum zitierten Posting springen:
Jupp, exakt. Und wenn es jetzt noch Lambdas für Win32 gäbe (was aber ohne GC der Übersichtlichkeit schaden dürfte), könnte man damit wirklich LINQ 1:1 übernehmen. Dass es Class Helpers nicht für Interfaces gibt, wäre noch ein kleines Manko, aber Interfaces sind in Delphi.Win32 ja sowieso nicht weit verbreitet.

Ja, anonyme Methoden sparen oft viel Schreibarbeit. Dafür ist Delphi-Code etwas übersichtlicher, wenn man sich eine Seite eines Quelltexts anschaut. In C# kann man relativ viel als "ein Befehl" (/eine Zeile oder was auch immer) schreiben: Klasse instanzieren, Properties setzen und auch gleich noch ein Event (irgendwann kommt das bestimmt) das auch noch als anonyme Methode oder Lambda-Expression geschrieben ist. Womöglich ist das ganze auch noch Teil eines : ? Ausdrucks in einer weiteren if-Abfrage! ;)

Spass beiseite - das mit LINQ in Delphi klingt verlockend aber spätestens wenn Expression Trees zum Zug kommen, dann ist es nicht mehr möglich, etwas Analoges in Delphi zu basteln :)
Kha
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 3803
Erhaltene Danke: 176

Arch Linux
Python, C, C++ (vim)
BeitragVerfasst: Mo 31.08.09 00:21 
Ich lasse mich nur ungern zu Übersichtlichkeit zwingen, das will ich lieber selbst vermasseln :zwinker: .
user profile icondelfiphan hat folgendes geschrieben Zum zitierten Posting springen:
Spass beiseite - das mit LINQ in Delphi klingt verlockend aber spätestens wenn Expression Trees zum Zug kommen, dann ist es nicht mehr möglich, etwas Analoges in Delphi zu basteln :)
Durch den Thread hatte ich nur noch IEnumerable im Kopf, bei allen anderen LINQ-Providern hast du natürlich recht :) .
Und gerade ist mir erst wieder eingefallen, dass man so ein Push-Modell nicht nur in einer obskuren Lib von Jon Skeet findet, sondern sogar im nächsten .NET-Framework :lol: .

_________________
>λ=
delfiphan Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 2684
Erhaltene Danke: 32



BeitragVerfasst: Mo 31.08.09 10:34 
Hab den "dualen" Fall jetzt auch mal implementiert. Funktioniert ganz gut. Man könnte jetzt im Hauptprogramm eine Schleife haben und die einzelnen Items jeweils an mehrere Abnehmer pushen. Ich glaube das wäre genau das Push-Prinzip wie im Artikel von Jon Skeet beschrieben.
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
// ...
  function HandlePushedItems: stringoverride;
// ...

function TReverseTest.HandlePushedItems: string// better name to be found
var
  S: string;
begin
  for S in Self do
    Result := Result + S + ' ';
end;

var
  Test: TReverseTest;
begin
  Test := TReverseTest.Create; // Reversed case: No Input given at constructor
  try
    Test.Push('test1'); // Reversed case: We push data into the object here. With IEnumerable, we would pull data out.
    Test.Push('test2');
    Test.Push('test3');
    Writeln(Test.Done); // Reversed case: Output is fetched at the end.
    // Output: test1 test2 test3
  finally
    Test.Free;
  end;
  readln;
end.

Oben liefert HandlePushedItems einfach einen String zurück. Damit eine Verkettung möglich wäre, müsste er das Resultat weiterpushen. War jetzt nur so ein Test mit der Stringrückgabe.

Coole Sache, danke für die Artikel, war sehr interessant, das mal genauer anzusehen :)

[Edit] Es scheint, als ob man in der zukünftigen Version von C# in diesem Fall auf CoRoutinen (o.ä.) verzichtet. Stattdessen muss man gemäss diesem Artikel: introducing-rx-linq-to-events OnNext und OnCompleted Methoden schreiben, die als Callback fungieren.

Sie sprechen in dem einen Video (Link weiter oben) zwar immer davon, dass es die Umkehrung ist und man liest, dass OnNext dann die Umkehrung von Yield usw, aber so genau 1:1 ist es dann noch wieder nicht. Der Enumeration-Fall (normaler Fall) definieren sie so:
() -> ( () -> T )
Man ruft also eine Funktion auf ohne Parameter (GetEnumerator Factory); die Rückgabe ist eine Funktion (bzw. Enumerator-Instanz), welche man mehrere Male aufrufen kann, um die Daten zu pullen.

Die Umkehrung ist demnach:
() <- ( () <- T )
bzw.
( T -> () ) -> ()
Man ruft eine Funktion ohne Rückgabewert. Der Parameter ist eine Funktion, d.h. ein Callback, der selbst einen Parameter hat. Diese Funktion ruft man dann z.B. immer dann auf, wenn ein Event passiert und der Parameter sind die gepushten Daten.

Soweit so gut. Beim Enumerator ist es ja schon so, dass mehrere Male MoveNext ausgeführt wird - soweit sind die "Formeln" korrekt. Aber: Die vom Programmierer geschriebene Funktion wird aber genau einmal ausgeführt. Durch die Anwendung von CoRoutinen (bzw. Transformation des Compilers) wird diese Funktion einfach stückweise ausgeführt. Dieser letzte Schritt wird beim Umkehrfall nicht berücksichtigt, obwohl es möglich wäre.

Der Callback, der bei jedem Event ausgeführt wird, kann theoretisch auch ein Stück einer grösseren Funktion sein, die durch CoRoutinen stückweise ausgeführt wird. Die Frage ist lediglich, ob man das braucht - oder ob jeder Event sowieso auf gleiche Art und Weise "stateless" gehandhabt wird. Ich denke mal, dass man gesagt hat, dass man das sowieso nicht braucht - obwohl ich finde, dass man z.B. eine Maximumsberechnung dadurch schön kompakt in einer Schleife hätte (Beispiel oben).

Durch das Nichtanbieten von CoRoutines beim umgekehrten Fall gehen die Leute aber auch ein ernstes Problem aus dem Weg. Und zwar im Zusammenhang mit Queries und Subqueries mit LINQ im umgekehrten Fall.
introducing-rx-linq-to-events hat folgendes geschrieben:

ausblenden C#-Quelltext
1:
2:
3:
4:
IObservable<Event<MouseEventArgs>> draggingEvent =
     from mouseLeftDownEvent in control.GetMouseLeftDown()
     from mouseMoveEvent in control.GetMouseMove().Until(control.GetMouseLeftUp())
     select mouseMoveEvent;

Wenn in diesem Drag-Beispiel nach einem MouseDown event noch während dem Draggen (d.h. bevor wieder ein MouseUp gekommen ist) wieder ein MouseDown Event käme, dann gäbe es bei CoRoutines ein Problem: Die MoveNext Operation des Subqueries (MouseMove) "blockiert" sozusagen bis zum nächsten Aufruf, obwohl ein Wert für den Hauptquery (MouseDown) zur Verfügung stünde. Wenn das mit Threads implementiert wäre gäbe es vermutlich ein Deadlock; bei Fibers ist nicht klar, wie zurückgesprungen werden soll - man hat eigentlich nur einen einzigen Working-Fiber, will aber dort eigentlich an eine andere Stelle hinspringen. Man müsste dann das Subquery also entweder zuerst abbrechen bevor man weiterarbeiten könnte - oder man bräucht irgendwie plötzlich mehrere parallele Ausführungspfade im Query...

Bei Events gibt es diese Probleme nur in abgeschwächter Form - es kommt nie zu einem Deadlock oder Abbrüchen. Ich glaube was passieren würde ist, dass, wenn man State-Variablen bei den Events einsetzen würde, diese dann unerwünschterweise überschrieben würden - analog zu den Fibers wo man das Subquery abbrechen würde; aber hier wird der Zustand einfach überschrieben. Und diese Überschriebenwerden wäre dann Sache des Entwicklers; bei einem Framework mit CoRoutines müsste hingegen das Framework etwas Intelligentes unternehmen ;)

Wenn die Eventabarbeitung "stateless" ist wird nie was überschrieben und es gibt keine Probleme. Aber immer stateless ist sie bestimmt nicht: Vielleicht möchte jemand einen Parser im Push-Prinzip betreiben können, weil die zu parsenden Daten nach und nach als Events in Form von Netzwerk-Paketen reinkommen. Dafür bräuchte man die ganze Umkehrung, nicht nur die Hälfte :). [/Edit]
Kha
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 3803
Erhaltene Danke: 176

Arch Linux
Python, C, C++ (vim)
BeitragVerfasst: Di 01.09.09 00:22 
user profile icondelfiphan hat folgendes geschrieben Zum zitierten Posting springen:
Hab den "dualen" Fall jetzt auch mal implementiert.
Wem's Spaß macht :mrgreen: ...

user profile icondelfiphan hat folgendes geschrieben Zum zitierten Posting springen:
Der Enumeration-Fall (normaler Fall) definieren sie so:
() -> ( () -> T )
Man ruft also eine Funktion auf ohne Parameter (GetEnumerator Factory); die Rückgabe ist eine Funktion (bzw. Enumerator-Instanz), welche man mehrere Male aufrufen kann, um die Daten zu pullen.

Die Umkehrung ist demnach:
() <- ( () <- T )
bzw.
( T -> () ) -> ()
Wow, dass man die Umkehrung sogar an den Signaturen ablesen kann, habe ich noch nicht gesehen :D . Muss ich mir wohl doch mal das Video zu Gemüte führen ;) .

user profile icondelfiphan hat folgendes geschrieben Zum zitierten Posting springen:
Soweit so gut. Beim Enumerator ist es ja schon so, dass mehrere Male MoveNext ausgeführt wird - soweit sind die "Formeln" korrekt. Aber: Die vom Programmierer geschriebene Funktion wird aber genau einmal ausgeführt. Durch die Anwendung von CoRoutinen (bzw. Transformation des Compilers) wird diese Funktion einfach stückweise ausgeführt. Dieser letzte Schritt wird beim Umkehrfall nicht berücksichtigt, obwohl es möglich wäre.
Ich denke nicht, dass Push mit Coroutines Sinn macht bzw. würdest du letzten Endes wohl wieder bei Pull landen. Eine Coroutine springt im Program Flow zwar hin und her, läuft insgesamt aber in einem Durchgang von Anfang bis Ende durch. Das Push-Modell wird dagegen mehrmals von außen angestoßen - was nur über Callbacks überhaupt möglich ist und, wie du schon festgestellt hast, asynchrones Abarbeiten erst ermöglicht.
Dass der Compiler generell auch für Push Syntax Sugar bereitstellen könnte, der eine Methode in Callbacks zerlegt, stimmt natürlich. Wie gesagt könnte man statt einer neuen Syntax dafür aber Monads nutzen und genau das haben sie ja schon getan und "LINQ to Events" getauft :zwinker: .

user profile icondelfiphan hat folgendes geschrieben Zum zitierten Posting springen:
obwohl ich finde, dass man z.B. eine Maximumsberechnung dadurch schön kompakt in einer Schleife hätte (Beispiel oben).
Hm, meinst du mein Push-LINQ-Beispiel von oben? Dann verstehe ich den Hinweis nicht ganz :) , dort habe ich schließlich absichtlich auf einer Ebenen gearbeitet, auf der gar kein Push mehr sichtbar ist. Damit wir nicht aneinander vorbeireden, die (vereinfachte) Implementierung ohne Primitive:
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
T MaxEnumerable<T>(IEnumerable<T> seq)
{
  var value = default(T);
  foreach (var element in seq)
    value = Compare.Max(value, element);

  return value;
}

IFuture<T> MaxJonSkeet<T>(IDataProducer<T> source)
{
  var value = default(T);
  var ret = new Future<T>();

  source.DataProduced += element =>
    value = Compare.Max(value, element);
  source.EndOfData +=
    ret.Value = value;

  return ret;
}

Hm, und Reactive? Erst jetzt wird mir richtig klar, dass Skeet und Reactive zwei ganz unterschiedliche Ziele verfolgen: Push-LINQ geht immer noch von einem IEnumerable als Datenquelle aus, also von einer synchronen Quelle. Nur so kann IFuture (das gar keine "HasValue"-Property besitzt) funktionieren: Man kann sich sicher sein, dass nach DataProducer.ProduceAndEnd o.ä. alle IFutures gefüllt sind. Push-LINQ kann/könnte wirklich überall eingesetzt werden, wo davor LINQ eingesetzt wurde.
Bei Reactive geht es dagegen rein um (asynchrone) Events, die vielleicht sogar auf verschiedenen Threads ankommen (schon deshalb kann kein Code wie MaxJonSkeet dort funktionieren). Ironischerweise gibt es hier nur eine Möglichkeit, Aggregat-Funktionen wie Max zu benutzen: Man konvertiert das IObservable in ein synchrones, blockierendes IEnumerable :D . Intern geschieht das, indem der Enumerator über Semaphoren darauf wartet, dass eine threadsichere Queue die einkommenden Daten anbietet.
Ein "Max-Problem", das auf IObservable Sinn macht, wäre eher etwas wie "basierend auf den einkommenden Daten, gib mir das bisherige Maximum aus". Könnte so aussehen:
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
myNetworkStreamObservable
.Synchronize() // schön geordnet bitte!
.Scan(Compare.Max) // gibt für jeden OnNext-Aufruf Max des bisherigen und des derzeitigen Items zurück
.Send(SynchronizationContext.Current) // nachdem wir unsere kostspieliege Max-Berechnung ausgeführt haben, machen wir lieber auf dem Hauptthread weiter
.Do(i => maxTextBox.Text = i.ToString());

Dafür, dass ich mir etwas komplett anderes drunter vorgestellt habe, muss ich sagen: Nie war asynchrone Programmierung schöner :D .

In das Scan-Modul könntest du jetzt auch deinen Parser einsetzen, ein Problem mit dem State sehe ich da nicht. Wenn du von mehreren Streams gleichzeitig lesen willst (was ja in etwa dem "from ... from" entsprechen würde... :gruebel: ?), bräuchtest du ja sowieso für jeden Stream eine separate Instanz.

_________________
>λ=
BenBE
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 8721
Erhaltene Danke: 191

Win95, Win98SE, Win2K, WinXP
D1S, D3S, D4S, D5E, D6E, D7E, D9PE, D10E, D12P, DXEP, L0.9\FPC2.0
BeitragVerfasst: Di 01.09.09 01:15 
You both lost me ...

Ging dieser Thread nicht um Fibers und Enumeratoren???

:nixweiss: :?!?: :nixweiss:

_________________
Anyone who is capable of being elected president should on no account be allowed to do the job.
Ich code EdgeMonkey - In dubio pro Setting.
delfiphan Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 2684
Erhaltene Danke: 32



BeitragVerfasst: Di 01.09.09 09:12 
user profile iconKha hat folgendes geschrieben Zum zitierten Posting springen:
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
T MaxEnumerable<T>(IEnumerable<T> seq)
{
  var value = default(T);
  foreach (var element in seq)
    value = Compare.Max(value, element);

  return value;
}

IFuture<T> MaxJonSkeet<T>(IDataProducer<T> source)
{
  var value = default(T);
  var ret = new Future<T>();

  source.DataProduced += element =>
    value = Compare.Max(value, element);
  source.EndOfData +=
    ret.Value = value;

  return ret;
}

Genau dieser Unterschied meinte ich. Mit CoRoutinen würden beide Maximumsberechnungen ungefähr gleich aussehen. Statt dass man Callbacks definiert:
ausblenden C#-Quelltext
1:
2:
  source.DataProduced += element =>
    value = Compare.Max(value, element);

wäre der Callback sozusagen automatisch die Routine innerhalb des foreach:
ausblenden C#-Quelltext
1:
2:
  foreach (var element in seq)
    value = Compare.Max(value, element);

Und die CoRoutinen würden für jede Iteration so ne Schleife ausführen. Es ist schlussendlich dasselbe, aber man würde es etwas anders schreiben. In foreach zu denken fühlt sich für mich "natürlicher" an als über Callbacks zu arbeiten. Bestimmt auch nur Gewöhnungssache. :)

Das hier ist schon nett:
user profile iconKha hat folgendes geschrieben Zum zitierten Posting springen:
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
myNetworkStreamObservable
.Synchronize() // schön geordnet bitte!
.Scan(Compare.Max) // gibt für jeden OnNext-Aufruf Max des bisherigen und des derzeitigen Items zurück
.Send(SynchronizationContext.Current) // nachdem wir unsere kostspieliege Max-Berechnung ausgeführt haben, machen wir lieber auf dem Hauptthread weiter
.Do(i => maxTextBox.Text = i.ToString());

Ist jetzt zwar ein bisschen ein anderes Thema, aber wenn man schon strikt sequentielle und nicht "verschachtelte" Aufrufe hintereinanderstellt, könnte man vielleicht auch sowas wie Fibers/etc. nutzen... Nur so ein Gedanke. Eine Funktion würde dann in Stücken ausgeführt. Anstatt Events hintereinander zu definieren und so einen Pseudoprogrammfluss anzugeben könnte man mit Fibers eine echte Funktion mit einem richtigen Programmfluss schreiben. Die Funktion würde dann in Stücken ausgeführt. Beim jeweiligen nächsten Event geht es wieder ein Stückchen weiter. Ja man könnte sogar inmitten einer Prozedur angeben, dass der Thread gewechselt werden soll! Das wäre mal was. Man hat eine Prozedur und kann zwischendrin mal einen Teil in einem anderen Thread laufen lassen. Was man heute in Delphi in eine Synchronize() Prozedur packt, könnte man quasi "inline" in der gleichen Prozedur geschehen: Bitte die nächsten paar Zeilen im MainThread, dann wieder normal weiter :) (Klar, inline Methoden würden das Problem auch lösen ;))
BenBE
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 8721
Erhaltene Danke: 191

Win95, Win98SE, Win2K, WinXP
D1S, D3S, D4S, D5E, D6E, D7E, D9PE, D10E, D12P, DXEP, L0.9\FPC2.0
BeitragVerfasst: Di 01.09.09 10:13 
user profile icondelfiphan hat folgendes geschrieben Zum zitierten Posting springen:
Bitte die nächsten paar Zeilen im MainThread, dann wieder normal weiter :) (Klar, inline Methoden würden das Problem auch lösen ;))

Bitte die nächsten paar Posts zum MainTopic, dann wieder nromal weiter :) (Klar, Inline-Topics würden das Problem auch lösen ;-))

MfG,
BenBE.

_________________
Anyone who is capable of being elected president should on no account be allowed to do the job.
Ich code EdgeMonkey - In dubio pro Setting.
delfiphan Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 2684
Erhaltene Danke: 32



BeitragVerfasst: Di 01.09.09 13:16 
;) :mrgreen: Naja, es geht immer noch um das gleiche Thema. Was man damit alles anstellen kann, weitere Einsatzmöglichkeiten und der duale ("umgekehrte") Fall von Enumerators. Jedenfalls habe ich in jedem Beitrag entweder das Wort Enumerator oder Fiber drin (wie im Titel!) ;) Ausser beim einen kurzen Post, wo es um LINQ ging ;)

Das mit dem Inline Thread-Switch könnte ich sogar konkret brauchen und würde manchen Code einiges kompakter machen. Wenn man sonst mit Worker-Threads arbeiten will ist es immer so eine Sache mit der Parameterübergabe (Eingabe und Rückgabe) und das Exception-Handling ist auch lästig. Hier ist alles schön beieinander.
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
procedure TForm1.Button1Click(Sender: TObject);
begin
  TFiber.Create.Execute(FiberButton1Click); // automatische Freigabe durch TFiber
end;

procedure TForm1.FiberButton1Click(Fiber: TFiber; Sender: TObject);
var
  I: Integer;
  Items: TStrings;
begin
  Items := TStringList.Create;
  try
    Items.Assign(ListBox1);
    Fiber.ContinueInWorkerThread// ab hier wird in einem Worker-Thread gearbeitet, der MainThread wird nicht mehr blockiert
    try
      for I := 0 to Items.Count - 1 do
        // very expensive operation
    finally
      Fiber.ContinueInMainThread// ab hier wieder MainThread
    end;
    ListBox1.Assign(Items);
  finally
    Items.Free;
  end;
end;

Ich weiss nicht, ob das konkret mit Win32-Fibers geht (ein Fiber von einem Thread auf ein anderes transferieren); aber mit dem selbstgestrickten Fiber müsste es kein Problem sein.

Edit: Scheint zu klappen (siehe Screenshot); Kleiner Stolperstein bei den Exceptions. Da RaiseListPtr in der System.pas eine threadvar ist, muss man diese beim Context-Switch manuell rüberkopieren. Lässt sich aber mit SetRaiseList wunderbar bewerkstelligen. :)
Einloggen, um Attachments anzusehen!
Kha
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 3803
Erhaltene Danke: 176

Arch Linux
Python, C, C++ (vim)
BeitragVerfasst: Di 01.09.09 23:52 
user profile icondelfiphan hat folgendes geschrieben Zum zitierten Posting springen:
Das wäre mal was. Man hat eine Prozedur und kann zwischendrin mal einen Teil in einem anderen Thread laufen lassen. Was man heute in Delphi in eine Synchronize() Prozedur packt, könnte man quasi "inline" in der gleichen Prozedur geschehen: Bitte die nächsten paar Zeilen im MainThread, dann wieder normal weiter :) (Klar, inline Methoden würden das Problem auch lösen ;))
Sind die anonymen Methoden von D09 nicht inline genug ;) ? Mit
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
TaskScheduler current = TaskScheduler.FromCurrentSynchronizationContext();

...

var items = ...;

Task.TaskFactory
.StartNew(() => ProccesItems(items))
.ContinueWith(task => {
  task.Dispose(); // wirft ggf. die im Nebenthread aufgetretene Exception
  ListBox1.Items.Assign(items);
}, current);

könnte ich durchaus leben, try-finally habe ich mal gekonnt ignoriert :D . Wenn deine Coroutine sowohl Exceptions sauber verarbeitet als auch den UI-Thread in der Zwischenzeit unblockiert lässt, wäre das natürlich absolut genial - bei letzterem kann ich mir aber absolut nicht vorstellen, wie das funktionieren soll :eyecrazy: . Sicher, dass das geht :gruebel: ?

Und damit ich noch ein Titelwort erwähne :P : Beim Herumstöbern bin ich hier (zweitletzter Comment - wofür hat man bitte Perma-Links, wenn sie nicht funktionieren -.-' ?) wieder auf Fibers gestoßen :zwinker: . Und mit diesem Axum-Projekt (s. Blogtext) wären wir wieder bei Compiler-Transformationen :mrgreen: .

_________________
>λ=
delfiphan Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 2684
Erhaltene Danke: 32



BeitragVerfasst: Mi 02.09.09 00:29 
user profile iconKha hat folgendes geschrieben Zum zitierten Posting springen:
user profile icondelfiphan hat folgendes geschrieben Zum zitierten Posting springen:
Wenn deine Coroutine sowohl Exceptions sauber verarbeitet als auch den UI-Thread in der Zwischenzeit unblockiert lässt, wäre das natürlich absolut genial - bei letzterem kann ich mir aber absolut nicht vorstellen, wie das funktionieren soll :eyecrazy: . Sicher, dass das geht :gruebel: ?

Yep, beides geht. UI-Thread wird freigegeben und Exceptions werden transparent zwischen Threads hin- und hergeschoben. Beispielprojekt mit Sources im Anhang. :) Ich poste die Sache vielleicht noch separat als Open-Source-Unit. Bevor ich das mache muss ich aber wohl noch paar Sachen bereinigen. Hier aber schon mal vorab.

//Edit: siehe www.delphi-forum.de/...er+Delphi_94597.html


Zuletzt bearbeitet von delfiphan am Fr 04.09.09 20:29, insgesamt 1-mal bearbeitet
Martok
ontopic starontopic starontopic starontopic starontopic starontopic starofftopic starofftopic star
Beiträge: 3661
Erhaltene Danke: 604

Win 8.1, Win 10 x64
Pascal: Lazarus Snapshot, Delphi 7,2007; PHP, JS: WebStorm
BeitragVerfasst: Mi 02.09.09 00:53 
Wow. Nicht schlecht, aber irgendwo inkonsequent. Man kann damit zwar den Hauptthread/Fiber transparent verlassen... aber irgendwie ist das grad fast schon sinnlos, außer dass man sich einen neuen Thread spart.

Echte parallelität so herzustellen wäre cool. Also z.B.
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
for i:= 0 to 1000 do begin
  ForkSomethingThreadLike;
  LenghtyOperation;
end;
ComeBackFromAllThreads;


parfor anyone? ;)

_________________
"The phoenix's price isn't inevitable. It's not part of some deep balance built into the universe. It's just the parts of the game where you haven't figured out yet how to cheat."