Benchmark: Arbeiten im Thread

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Sehe ich nicht durch. Wirklich nicht. Edit: das bezog sich auf die Frage, was bei dir denn den Unterschied ausmacht.


Mir ist das zu viel gewurschtel. Die von mir und __blackjack__ angemerkten Vorteile eines einzelnen Worker-Threads beziehen sich darauf, dass es nichts bringt, beliebig viele Threads hochzufeuern, die sich dann einfach nur gegenseitig auf die Fuesse treten.

Hier habe ich mal ein kleines Test-Skript zusammengeworfen, das die von mir beschriebene Semantik hat. Wenn man auf einen Knopf drueckt, bekommt der wartende Hintergrund-Thread einfach seinen work-Slot aufgerufen.

Beim anderen Button wird - so wie bei dir in urspruenglichen Code den wir damals diskutiert haben - jeweils ein neuer QThread gestartet. Egal welche Methode man verwendet, es braucht beides mal auf meinem System ca. 6 Sekunden die Items in das Tree-Widgets zu stecken. Womit beide Ansaetze keinen Performance-Einbussen haben. Die ich mir auch nicht erklaeren koennte.

Warum dein Code nun so ein komisches Verhalten zeigt - ja nun. Wie gesagt, das ist mir zu schwurbelig.

Code: Alles auswählen

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import sys
import time
import threading

from PyQt5.QtCore import QTimer, QObject, pyqtSignal, \
     QThread, Qt

from PyQt5.QtWidgets import (
    QApplication,
    QWidget,
    QLabel,
    QTreeWidget,
    QTreeWidgetItem,
    QPushButton,
    QVBoxLayout,
)

TASKNUMBER = 460


class Worker(QObject):
    generated_item = pyqtSignal(str)
    work_started = pyqtSignal()
    work_done = pyqtSignal()

    def work(self):
        self.work_started.emit()
        for i in range(TASKNUMBER):
            for _ in range(1000000):  # just burn some time
                pass
            self.generated_item.emit("Item {}".format(i))
        self.work_done.emit()


class TimeMeasurer(QObject):

    def start(self):
        self._start = time.monotonic()

    def stop(self):
        elapsed = time.monotonic() - self._start
        print("Time elapsed:", elapsed)


class Forwarder(QObject):
    """
    Just a holder for one signal
    """
    enqueue_work = pyqtSignal()


class BatchCounter(QObject):

    count_changed = pyqtSignal(int)

    def __init__(self):
        super().__init__()
        self._lock = threading.Lock()
        self._count = 0

    def inc(self):
        with self._lock:
            self._count += 1
        self.count_changed.emit(self._count)


    def dec(self):
        with self._lock:
            self._count -= 1
        self.count_changed.emit(self._count)


def main():
    forwarder = Forwarder()
    counter = BatchCounter()

    extra_threads = []

    def generate_work_load():
        counter.inc()
        forwarder.enqueue_work.emit()

    def generate_work_load_in_extra_thread():
        def shutdown_thread():
            print("running threads:", len(extra_threads))
            extra_thread.quit()
            extra_thread.wait()
            extra_threads.remove(extra_thread)
            print("shutdown_thread")

        extra_thread = QThread()
        worker = extra_thread.worker = Worker()
        worker.moveToThread(worker_thread)

        extra_thread.started.connect(worker.work)
        tm = extra_thread.tm = TimeMeasurer()

        worker.generated_item.connect(add_item)
        worker.work_started.connect(tm.start)
        worker.work_done.connect(tm.stop)
        worker.work_done.connect(counter.dec)
        worker.work_done.connect(shutdown_thread)
        extra_threads.append(extra_thread)
        counter.inc()
        extra_thread.start()


    app = QApplication(sys.argv)

    worker_thread = QThread()
    worker = Worker()
    worker.moveToThread(worker_thread)

    w = QWidget()
    w.setWindowTitle('ThreadTest')

    layout = QVBoxLayout(w)
    start_in_worker_thread_button = QPushButton(w)
    start_in_worker_thread_button.setText("Add Items in Worker Thread")
    start_in_worker_thread_button.clicked.connect(generate_work_load)

    start_in_extra_thread_button = QPushButton(w)
    start_in_extra_thread_button.setText("Add Items in Extra Thread")
    start_in_extra_thread_button.clicked.connect(generate_work_load_in_extra_thread)

    counting_label = QLabel(w)
    counter.count_changed.connect(lambda count: counting_label.setText("Batches: {}".format(count)))

    tree = QTreeWidget(w)

    layout.addWidget(start_in_worker_thread_button)
    layout.addWidget(start_in_extra_thread_button)
    layout.addWidget(counting_label)
    layout.addWidget(tree)

    def add_item(text):
        parent = QTreeWidgetItem(tree)
        tree.addTopLevelItem(parent)
        parent.setText(0, text)

    w.show()

    tm = TimeMeasurer()

    worker.generated_item.connect(add_item)
    worker.work_started.connect(tm.start)
    worker.work_done.connect(tm.stop)
    worker.work_done.connect(counter.dec)
    forwarder.enqueue_work.connect(worker.work)

    # the ONE background thread
    worker_thread.start()

    app.exec_()
    worker_thread.quit()
    worker_thread.wait()
    sys.exit()

if __name__ == '__main__':
    main()
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@__deets__: Erst einmal danke ich dir. Ich werde mir mal gleich deinen Beispiel-Quelltext ansehen. Aber eines vorweg. Du unterstellst mir, dass meine Beispiel-Quelltexte zu verschwurbelt seien, aber deine wirken auch nicht gerade "übersichtlicher" auf mich. Ich muss mich da auch ziemlich arg durchkämpfen - vielleicht liegt es auch daran, dass ich mich selbst noch als Anfänger betrachte. Bitte, betrachte diese Anmerkung meinerseits nicht als Angriff oder als Trotz-Reaktion.
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das ist natuerlich bedauerlich. Aber gut zu wissen, dann mache ich mir in Zukunft nicht mehr die Muehe, Code zu schreiben. Spart mir UND dir Zeit!
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@deets: Es war wirklich nicht böse gemeint. Nur dachte ich mir "mhmh, zu mir sagt er, mein Quelltext sei unübersichtlich, und selbst sitze ich hier und muss mich durcharbeiten". Aber das nur so am Rande. Ich bin über deine Anmerkungen und Ratschläge dankbar - sehr sogar.

Nun, ich habe meine beiden Beispiel ein wenig überarbeitet. Das heißt, einige Namen habe ich so geändert, dass versucht habe, __deets__"Handschrift" beizubehalten. Im Prinzip sind die Beispiele identisch mit den vorherigen Beispielen - nur etwas aufgeräumter und mit hoffentlich besseren Namen versehen. Ich präsentiere euch wieder zwei Beispiele, dich sich jedoch nur minimal voneinander unterscheiden, aber die Namen aller Methoden, Signalen etc. bleiben gleich.

Sirius3 hat mich auf ein Problem aufmerksam gemacht. Ich muss irgendwie hinkriegen, dass der Timer nicht den Hauptthread landet.

Fangen wir mit der ersten Variante an: Hier wird die Arbeit beim Erzeugen des Threads via started() aufgenommen. Ganz klassisch und unproblematisch. In dieser Variante beträgt die Zeit: Duration: 0:00:46.948000

Code: Alles auswählen

#!/usr/bin/env python
#-*- coding:utf-8 -*-

import sys
import time
from datetime import datetime

from PyQt4.QtCore import QTimer, QObject, pyqtSignal, \
                         QThread, Qt

from PyQt4.QtGui import (QApplication, QDialog, QLabel, \
                         QTreeWidget, QTreeWidgetItem, \
                         QPushButton, QVBoxLayout)

class Worker(QObject):
    
    generated_item = pyqtSignal(str)
    count_item = pyqtSignal(int)
    
    def __init__(self, parent=None):
        QObject.__init__(self, parent)

        parent.start_order_signal.connect(self.start_work_timer)

        self.worker_generator = self.generate_work()

        # start to stopp the time
        self.start_time = datetime.now()

        self.work_timer = QTimer()
        self.work_timer.setSingleShot(False)
        self.work_timer.setInterval(1)
        self.work_timer.timeout.connect(self.populate_item)

    def start_work_timer(self):
        self.work_timer.start()
    
    def generate_work(self,
                      tasknumber=10000):

        count = 0
               
        for name in xrange(tasknumber):

            count += 1
            self.count_item.emit(count)
            
            yield str(name)

    def populate_item(self):

        try:
            self.generated_item.emit(next(self.worker_generator))

        except StopIteration:

            self.work_timer.stop()

            # end with to stopp the time
            self.end_time = datetime.now()

            # Show how much time has been elapsed.
            print('Duration: {}'.format(self.end_time - self.start_time))
        
class MainThread(QDialog):

    start_order_signal = pyqtSignal()

    def __init__(self,
                 parent = None):
        QDialog.__init__(self, parent)

        self.setWindowTitle('ThreadTest')

        layout = QVBoxLayout(self)

        self.tree = QTreeWidget(self)
        self.counting_label = QLabel(self)
        
        self.create_worker_thread_button = QPushButton(self)
        self.create_worker_thread_button.setText("Create Worker Thread and start work order")
        self.create_worker_thread_button.clicked.connect(self.generate_work_load_thread)
        
        self.close_button = QPushButton(self)
        self.close_button.setText("Close")
        self.close_button.clicked.connect(self.close)
   
        layout.addWidget(self.counting_label)
        layout.addWidget(self.tree)
        layout.addWidget(self.create_worker_thread_button)
        layout.addWidget(self.close_button)

        self.generate_work_load_thread()

    def generate_work_load_thread(self):

        self.task_thread = QThread()

        self.task_thread.work = Worker(self)
        self.task_thread.work.moveToThread(self.task_thread)
        
        self.task_thread.work.generated_item.connect(self.add_item)
        self.task_thread.work.count_item.connect(self.count_population)
        
        self.task_thread.started.connect(self.task_thread.work.start_work_timer)
        self.task_thread.finished.connect(self.task_thread.deleteLater)

        self.task_thread.start()

    def count_population(self,
                         count):

        self.counting_label.setText("Item(s): {}".format(count))
    
    def add_item(self, text):
        
        parent = QTreeWidgetItem(self.tree)
        self.tree.addTopLevelItem(parent)
        parent.setText(0, text)
        
def main():

    app = QApplication(sys.argv)

    w = MainThread()
    w.show()

    app.exec_()
    sys.exit()

if __name__ == '__main__':
    main()
In der zweiten Variante wird beim Start des Programmes der Thread erzeugt, aber hier fällt die started()- Methode weg, denn wir wollen nur erst einmal den Thread erzeugen, und zu einem späteren Zeitpunkt Arbeitsaufträge senden. Damit der bereits erzeugte Thread zum späteren Zeitpunkt Aufträge annehmen kann, arbeite ich mit dem pyqtSignal(), welcher im Hauptthread liegt und von dort auch emitiert wird. Hier beträgt die Zeit: 0:01:44.222000.

Code: Alles auswählen

#!/usr/bin/env python
#!/usr/bin/env python
#-*- coding:utf-8 -*-

import sys
import time
from datetime import datetime

from PyQt4.QtCore import QTimer, QObject, pyqtSignal, \
                         QThread, Qt

from PyQt4.QtGui import (QApplication, QDialog, QLabel, \
                         QTreeWidget, QTreeWidgetItem, \
                         QPushButton, QVBoxLayout)

class Worker(QObject):
    
    generated_item = pyqtSignal(str)
    count_item = pyqtSignal(int)
    
    def __init__(self, parent=None):
        QObject.__init__(self, parent)

        parent.start_order_signal.connect(self.start_work_timer)

        self.worker_generator = self.generate_work()

        # start to stopp the time
        self.start_time = datetime.now()

        self.work_timer = QTimer()
        self.work_timer.setSingleShot(False)
        self.work_timer.setInterval(1)
        self.work_timer.timeout.connect(self.populate_item)

    def start_work_timer(self):
        self.work_timer.start()
    
    def generate_work(self,
                      tasknumber=10000):

        count = 0
               
        for name in xrange(tasknumber):

            count += 1
            self.count_item.emit(count)
            
            yield str(name)

    def populate_item(self):

        try:
            self.generated_item.emit(next(self.worker_generator))

        except StopIteration:

            self.work_timer.stop()

            # end with to stopp the time
            self.end_time = datetime.now()

            # Show how much time has been elapsed.
            print('Duration: {}'.format(self.end_time - self.start_time))
        
class MainThread(QDialog):

    start_order_signal = pyqtSignal()

    def __init__(self,
                 parent = None):
        QDialog.__init__(self, parent)

        self.setWindowTitle('ThreadTest')

        layout = QVBoxLayout(self)

        self.tree = QTreeWidget(self)
        self.counting_label = QLabel(self)
        
        self.create_worker_thread_button = QPushButton(self)
        self.create_worker_thread_button.setText("Just create Worker Thread")
        self.create_worker_thread_button.clicked.connect(self.generate_work_load_thread)

        self.work_order_in_worker_thread_button = QPushButton(self)
        self.work_order_in_worker_thread_button.setText("Start work order")
        self.work_order_in_worker_thread_button.clicked.connect(self.start_order_signal.emit)
        
        self.close_button = QPushButton(self)
        self.close_button.setText("Close")
        self.close_button.clicked.connect(self.close)
   
        layout.addWidget(self.counting_label)
        layout.addWidget(self.tree)
        layout.addWidget(self.create_worker_thread_button)
        layout.addWidget(self.work_order_in_worker_thread_button)
        layout.addWidget(self.close_button)

    def generate_work_load_thread(self):

        self.task_thread = QThread()

        self.task_thread.work = Worker(self)
        self.task_thread.work.moveToThread(self.task_thread)
        
        self.task_thread.work.generated_item.connect(self.add_item)
        self.task_thread.work.count_item.connect(self.count_population)

        self.task_thread.finished.connect(self.task_thread.deleteLater)

        self.task_thread.start()

    def count_population(self,
                         count):

        self.counting_label.setText("Item(s): {}".format(count))
    
    def add_item(self, text):
        
        parent = QTreeWidgetItem(self.tree)
        self.tree.addTopLevelItem(parent)
        parent.setText(0, text)
        
def main():

    app = QApplication(sys.argv)

    w = MainThread()
    w.show()

    app.exec_()
    sys.exit()

if __name__ == '__main__':
    main()

Jetzt müsste ich mir eine Lösung einfallen lassen, wie ich einem bereits erstellen Thread zum späteren Zeitpunkt mit Aufträge bestücke, so dass der Timer nicht im Hauptthread liegt, sondern weiterhin im Thread bleibt. Selbst mit pyqtSignal() komme ich nicht weiter.
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Sophus hat geschrieben: Montag 19. November 2018, 15:09 @deets: Es war wirklich nicht böse gemeint. Nur dachte ich mir "mhmh, zu mir sagt er, mein Quelltext sei unübersichtlich, und selbst sitze ich hier und muss mich durcharbeiten". Aber das nur so am Rande.
Selbstverstaendlich war es "boese" gemeint. Du sagst es doch im naechsten Satz: es ist eine Retourkutsche. Insbesondere da es sonst keine inhaltliche Auseinandersetzung mit meinem Post gab. Nach der Retourkutsche zu schreiben, es sei aber keine solche gewesen, aendert daran nichts.
Jetzt müsste ich mir eine Lösung einfallen lassen, wie ich einem bereits erstellen Thread zum späteren Zeitpunkt mit Aufträge bestücke, so dass der Timer nicht im Hauptthread liegt, sondern weiterhin im Thread bleibt. Selbst mit pyqtSignal() komme ich nicht weiter.
Genau das zeige ich in meinem zu komplizierten Code....
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@__deets__: Was ich eher hinderlich finde, ist, dass du in deinem Beispiel alles noch mehr verschachtelst und verkomplizierst, als meine Beispiele. In meinen Beispielen sind es nur zwei Klasse (Hauptthread und Worker) - fertig. Du arbeitest in deinem Beispiel mit drei Klassen (TimeMeasurer, Forwarder, BatchCounter). Dabei möchte ich doch nur ganz einfach aus dem Hauptthread, also aus der GUI heraus, dem Worker Aufträge liefern. Braucht es dafür gleich drei Klassen? Und genau dies finde ich kompliziert, weil ich hin und her springen muss, was macht diese Klasse, und wieso wird diese Klasse mit eingebunden und da ist ja noch eine. Ich nahm an, es gäbe eine strukturell einfachere Lösung, wie ich vom Hauptthread aus mit dem Worker in einer bi-dualen Beziehung arbeiten kann, ohne dass zwischen Hauptthread und Worker drei weitere Klassen dazwischen geschaltet werden müssen.
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Man muss keine Batches zaehlen. Man muss auch keine Zeit messen. Zwei von unglaublich vielen drei (3!) Klassen koennen dementsprechend weg. Zweitens erreiche ich mit meinem einen Beispiel BEIDE use-cases darzustellen, unter Wiederverwendung von konsintent genutzen Klassen. Eine ehrliche Betrachtung wuerde also deine zwei Skripte meinem einen gegenueberstellen, und da sieht es mit dem Vergleich schon ganz anders aus.

Und der Forwarder ist nur ein Container fuer ein Signal. Ich haette auch ein QMeta-Objekt verwenden koennen, ob das deinem Verstaendnis zutraeglich gewesen waere wage ich zu bezweifeln. Wenn man aber nicht eine von diesen zwei Moeglichkeiten waehlt, dann erreicht man eben auch nicht das Ziel, sondern baut etwas wie du, das nicht funktioniert. So wie du, wenn du eigentlich Code im Hintergrundthread anwerfen willst, der dann aber im Main -Thread ausgefuehrt wird, da die fuer dieses Verhalten notwendige Signal/Slot-Verbindung fehlt.

Ich habe es schon mehrfach gesagt, hier, und in dem anderen Thread den du hier zu Beginn zitierst. Und womoeglich schon in der Vergangenheit, denn Hinweise auf Fehler in deinem threaded Code habe ich dir ja schon haeufiger mal gegeben: solange du nicht fundamental verstehst, wie QThreads, das Prinzip der Thread-Ownership von Objekten, und der Semantik von queued signal slot connections in Qt verstehst, wirst du dich weiterhin verheddern. Und so viel weniger als mein Code ist fuer ein solches Beispiel auch nicht moeglich. Nebenlaeufige Programmierung ist halt kompliziert.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@__deets__: Beziehen wir uns jetzt einmal auf meine wieder erneut eingestellten Beispiele. In der zweiten Variante liegt der Hund darin begraben, dass das start_order_signal ()-Signal im Hauptthread (MainThread()) ansiedelt, welcher zwar im Worker() mit entsprechender Methode verbunden ist, jedoch im Hauptthread emittiert wird. Und diese Verbindung zieht den Prozess in den Hauptthread?

UPDATE:

Ich habe dein Beispiel hinzugezogen und deine Forwarder()-Klasse mit eingebunden. Nachfolgend zeige ich das überarbeitete Beispiel. Ich hoffe, dass ich es richtig verstanden und umgesetzt habe. Die Zeitmessung liegt bei: Duration: 0:01:03.155000. Irgendwie habe Gefühl, dass mit der started() das befüllen des TreeWidgets schneller ist, denn ich bleibe dabei immer unter einer Minute.

Code: Alles auswählen

#!/usr/bin/env python
#-*- coding:utf-8 -*-

import sys
import time
from datetime import datetime

from PyQt4.QtCore import QTimer, QObject, pyqtSignal, \
                         QThread, Qt

from PyQt4.QtGui import (QApplication, QDialog, QLabel, \
                         QTreeWidget, QTreeWidgetItem, \
                         QPushButton, QVBoxLayout)

class Forwarder(QObject):
    """
    Just a holder for one signal
    """
    enqueue_work = pyqtSignal()
    
    def __init__(self, parent=None):
        QObject.__init__(self, parent)
        
class Worker(QObject):
    
    generated_item = pyqtSignal(str)
    count_item = pyqtSignal(int)
    
    def __init__(self, parent=None):
        QObject.__init__(self, parent)

        self.worker_generator = self.generate_work()

        # start to stopp the time
        self.start_time = datetime.now()

        self.work_timer = QTimer()
        self.work_timer.setSingleShot(False)
        self.work_timer.setInterval(1)
        self.work_timer.timeout.connect(self.populate_item)

    def start_work_timer(self):
        self.work_timer.start()
    
    def generate_work(self,
                      tasknumber=10000):

        count = 0
               
        for name in xrange(tasknumber):

            count += 1
            self.count_item.emit(count)
            
            yield str(name)

    def populate_item(self):

        try:
            self.generated_item.emit(next(self.worker_generator))

        except StopIteration:

            self.work_timer.stop()

            # end with to stopp the time
            self.end_time = datetime.now()

            # Show how much time has been elapsed.
            print('Duration: {}'.format(self.end_time - self.start_time))
        
class MainThread(QDialog):

    def __init__(self,
                 parent = None):
        QDialog.__init__(self, parent)

        self.setWindowTitle('ThreadTest')

        self.generate_work_load_thread()

        layout = QVBoxLayout(self)

        self.tree = QTreeWidget(self)
        self.counting_label = QLabel(self)
        
        self.create_worker_thread_button = QPushButton(self)
        self.create_worker_thread_button.setText("Create Worker Thread and start work order")
        self.create_worker_thread_button.clicked.connect(self.forwarder.enqueue_work.emit)
        
        self.close_button = QPushButton(self)
        self.close_button.setText("Close")
        self.close_button.clicked.connect(self.close)
   
        layout.addWidget(self.counting_label)
        layout.addWidget(self.tree)
        layout.addWidget(self.create_worker_thread_button)
        layout.addWidget(self.close_button)

    def generate_work_load_thread(self):

        self.forwarder = Forwarder()

        self.task_thread = QThread()

        self.task_thread.work = Worker(self)
        self.task_thread.work.moveToThread(self.task_thread)
        
        self.task_thread.work.generated_item.connect(self.add_item)
        self.task_thread.work.count_item.connect(self.count_population)

        self.forwarder.enqueue_work.connect(self.task_thread.work.start_work_timer)
        
        self.task_thread.finished.connect(self.task_thread.deleteLater)

        self.task_thread.start()

    def count_population(self,
                         count):

        self.counting_label.setText("Item(s): {}".format(count))
    
    def add_item(self, text):
        
        parent = QTreeWidgetItem(self.tree)
        self.tree.addTopLevelItem(parent)
        parent.setText(0, text)
        
def main():

    app = QApplication(sys.argv)

    w = MainThread()
    w.show()

    app.exec_()
    sys.exit()

if __name__ == '__main__':
    main()
Zuletzt geändert von Sophus am Montag 19. November 2018, 17:29, insgesamt 1-mal geändert.
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich beziehe mich nicht auf deine Beispiele. Mein Beispiel funktioniert. Erarbeite dir das Verstaendnis dafuer. Dann kannst du es fuer dich nutzen. Oder lass es sein. Fuer mich ist EOD.
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Und um das noch mal klar zu machen: der Forwarder ist nur ein Signal. Ich haette auf den auch verzichten koennen faellt mir gerade auf, weil ich einfach das Button-clicked auf sowohl den counter.inc als auch den Worker haette connecten koennen. Das ist also der irrelevanteste Teil des Codes. Es gebt um ein Signal, das einen Slot im anderen Thread aufruft.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Also doch "überzogen"? :)
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Gottlob machst du ja keine Fehler. Da ist jede Zeile zwingend notwendig. Zb einen zweiten counter von 0 bis tasknumber, weil's so schoen ist...
Antworten