806 Stimmen

Wie sollte ich Multithreading-Code in der Einheit testen?

Bisher habe ich den Albtraum des Testens von Multithreading-Code vermieden, da er mir einfach zu sehr wie ein Minenfeld erscheint. Ich möchte fragen, wie die Leute über das Testen von Code, der auf Threads für die erfolgreiche Ausführung angewiesen ist, oder einfach nur, wie die Leute über das Testen dieser Art von Fragen, die nur zeigen, wenn zwei Threads in einer bestimmten Weise interagieren gegangen sind?

Dies scheint ein wirklich zentrales Problem für die Programmierer heute, wäre es nützlich, unser Wissen auf dieser einen imho Pool.

2 Stimmen

Ich hatte vor, eine Frage zu genau diesem Thema zu stellen. Obwohl Will im Folgenden viele gute Punkte anführt, denke ich, dass wir es besser machen können. Ich stimme zu, dass es keinen einzigen "Ansatz" gibt, um das Problem sauber zu lösen. Aber "so gut wie möglich zu testen" setzt die Messlatte sehr niedrig an. Ich werde mit meinen Ergebnissen zurückkommen.

0 Stimmen

In Java: Das Paket java.util.concurrent enthält einige schlecht bekannte Klassen, die helfen können, deterministische JUnit-Tests zu schreiben. Werfen Sie einen Blick auf - CountDownLatch - Semaphor - Austauscher

0 Stimmen

Können Sie bitte einen Link zu Ihrer vorherigen Frage zu Unit-Tests angeben?

1voto

Mike Nakis Punkte 49916

Das Führen mehrerer Threads ist nicht schwierig, sondern ein Kinderspiel. Leider müssen die Threads in der Regel miteinander kommunizieren; das ist das Schwierige daran.

Der Mechanismus, der ursprünglich erfunden wurde, um die Kommunikation zwischen Modulen zu ermöglichen, waren Funktionsaufrufe; wenn Modul A mit Modul B kommunizieren möchte, ruft es einfach eine Funktion in Modul B auf. Leider funktioniert dies nicht mit Threads, denn wenn Sie eine Funktion aufrufen, läuft diese Funktion immer noch im aktuellen Thread.

Um dieses Problem zu lösen, griff man auf einen noch primitiveren Kommunikationsmechanismus zurück: Man deklarierte einfach eine bestimmte Variable und ließ beide Threads auf diese Variable zugreifen. Mit anderen Worten, man erlaubt den Threads, Daten gemeinsam zu nutzen. Die gemeinsame Nutzung von Daten ist buchstäblich das erste, was einem in den Sinn kommt, und es scheint eine gute Wahl zu sein, weil es sehr einfach zu sein scheint. Ich meine, wie schwer kann das schon sein, oder? Was kann schon schief gehen?

Rennbedingungen. Das ist es, was schief gehen kann und wird.

Als die Menschen erkannten, dass ihre Software unter zufälligen, nicht reproduzierbaren katastrophalen Fehlern aufgrund von Wettlaufbedingungen litt, begannen sie, ausgeklügelte Mechanismen wie Sperren und Compare-and-Swap zu erfinden, um sich gegen derartige Vorfälle zu schützen. Diese Mechanismen fallen unter die weit gefasste Kategorie der "Synchronisierung". Leider hat die Synchronisation zwei Probleme:

  1. Es ist sehr schwierig, es richtig zu machen, so dass es sehr fehleranfällig ist.
  2. Es ist völlig untestbar, weil man nicht auf eine Race Condition testen kann.

Der aufmerksame Leser wird feststellen, dass "sehr fehleranfällig" und "völlig untestbar" eine tödliche Kombination .

Die oben erwähnten Mechanismen wurden von großen Teilen der Industrie erfunden und übernommen, bevor sich das Konzept des automatisierten Softwaretests durchsetzte; niemand konnte also erkennen, wie tödlich das Problem war; man betrachtete es einfach als ein schwieriges Thema, für das man Programmiergurus braucht, und alle waren damit einverstanden.

Heutzutage steht bei allem, was wir tun, das Testen an erster Stelle. Wenn also ein bestimmter Mechanismus nicht getestet werden kann, dann kommt die Verwendung dieses Mechanismus einfach nicht in Frage, Punkt. So ist die Synchronisation in Ungnade gefallen; nur noch wenige Menschen praktizieren sie, und es werden täglich weniger.

Ohne Synchronisierung können Threads keine Daten gemeinsam nutzen; die ursprüngliche Anforderung war jedoch nicht die gemeinsame Nutzung von Daten, sondern die Möglichkeit, dass Threads miteinander kommunizieren können. Neben der gemeinsamen Nutzung von Daten gibt es andere, elegantere Mechanismen für die Kommunikation zwischen Threads.

Ein solcher Mechanismus ist das Message-Passing, auch bekannt als Events.

Bei der Nachrichtenübermittlung gibt es im gesamten Softwaresystem nur einen einzigen Ort, an dem die Synchronisierung zum Einsatz kommt, und das ist die Klasse der gleichzeitigen blockierenden Warteschlangen, die wir zum Speichern von Nachrichten verwenden. (Die Idee ist, dass wir in der Lage sein sollten, zumindest diesen kleinen Teil richtig hinzubekommen).

Das Tolle an der Nachrichtenübermittlung ist, dass sie nicht unter Race Conditions leidet und vollständig testbar ist.

0voto

Red Rooster Punkte 199

Für J2E-Code habe ich SilkPerformer, LoadRunner und JMeter für Gleichzeitigkeitstests von Threads verwendet. Sie tun alle das Gleiche. Im Grunde bieten sie eine relativ einfache Schnittstelle für die Verwaltung ihrer Version des Proxy-Servers, die erforderlich ist, um den TCP/IP-Datenstrom zu analysieren und mehrere Benutzer zu simulieren, die gleichzeitig Anfragen an Ihren Anwendungsserver stellen. Mit dem Proxy-Server können Sie z. B. die gestellten Anfragen analysieren, indem Sie die gesamte an den Server gesendete Seite und URL sowie die Antwort des Servers nach der Bearbeitung der Anfrage anzeigen.

Sie können einige Bugs im unsicheren http-Modus finden, in dem Sie zumindest die gesendeten Formulardaten analysieren und systematisch für jeden Benutzer ändern können. Die wirklichen Tests sind jedoch im https-Modus (Secured Socket Layers) möglich. Dann müssen Sie sich auch mit der systematischen Änderung der Sitzungs- und Cookie-Daten befassen, was etwas komplizierter sein kann.

Der beste Fehler, den ich je beim Testen der Gleichzeitigkeit gefunden habe, war, als ich entdeckte, dass der Entwickler sich auf die Java-Garbage-Collection verlassen hatte, um die Verbindungsanforderung, die bei der Anmeldung zum LDAP-Server aufgebaut wurde, zu schließen, wenn er sich anmeldete. Dies führte dazu, dass die Benutzer die Sitzungen anderer Benutzer mitbekamen und sehr verwirrende Ergebnisse erzielten, wenn sie versuchten zu analysieren, was passierte, wenn der Server in die Knie ging und kaum in der Lage war, alle paar Sekunden eine Transaktion abzuschließen.

Am Ende werden Sie oder jemand anderes sich wahrscheinlich an die Arbeit machen und den Code auf Fehler wie den eben erwähnten analysieren müssen. Und eine offene, abteilungsübergreifende Diskussion, wie die, die stattfand, als wir das oben beschriebene Problem aufdeckten, ist sehr nützlich. Aber diese Tools sind die beste Lösung für das Testen von Multithreading-Code. JMeter ist quelloffen. SilkPerformer und LoadRunner sind proprietär. Wenn Sie wirklich wissen wollen, ob Ihre Anwendung thread-sicher ist, machen das die großen Jungs. Ich habe dies beruflich für sehr große Unternehmen getan, ich kann also nicht raten. Ich spreche aus persönlicher Erfahrung.

Ein Wort der Warnung: Es braucht einige Zeit, um diese Werkzeuge zu verstehen. Es ist nicht damit getan, einfach die Software zu installieren und die grafische Benutzeroberfläche zu starten, es sei denn, Sie haben bereits Erfahrungen mit der Multi-Thread-Programmierung gesammelt. Ich habe versucht, die 3 kritischen Kategorien von Bereichen zu identifizieren, die Sie verstehen müssen (Formulare, Sitzungs- und Cookie-Daten), in der Hoffnung, dass zumindest das Verständnis dieser Themen Ihnen helfen wird, sich auf schnelle Ergebnisse zu konzentrieren, anstatt die gesamte Dokumentation durchlesen zu müssen.

0voto

Gleichzeitigkeit ist ein komplexes Zusammenspiel zwischen dem Speichermodell, der Hardware, den Caches und unserem Code. Zumindest im Fall von Java wurden solche Tests teilweise in Angriff genommen, und zwar hauptsächlich durch jcstress . Die Schöpfer dieser Bibliothek sind dafür bekannt, dass sie viele JVM-, GC- und Java-Gleichzeitigkeitsfunktionen entwickelt haben.

Aber auch diese Bibliothek erfordert gute Kenntnisse der Java Memory Model Spezifikation, damit wir genau wissen, was wir testen. Aber ich denke, der Schwerpunkt dieser Bemühungen liegt auf Mircobenchmarks. Keine großen Geschäftsanwendungen.

0voto

gterzian Punkte 527

Es gibt einen Artikel zu diesem Thema, in dem Rust als Sprache für den Beispielcode verwendet wird:

https://medium.com/@polyglot_factotum/rust-concurrency-five-easy-pieces-871f1c62906a

Zusammenfassend lässt sich sagen, dass der Trick darin besteht, Ihre nebenläufige Logik so zu schreiben, dass sie gegenüber dem Nicht-Determinismus, der mit mehreren Ausführungssträngen einhergeht, robust ist, indem Sie Tools wie Channels und Condvars verwenden.

Wenn Sie Ihre "Komponenten" auf diese Weise strukturiert haben, ist es am einfachsten, sie zu testen, indem Sie Kanäle verwenden, um Nachrichten an sie zu senden, und dann andere Kanäle blockieren, um sicherzustellen, dass die Komponente bestimmte erwartete Nachrichten sendet.

Der verlinkte Artikel ist vollständig mit Unit-Tests geschrieben.

0voto

Tim Punkte 81

Es ist nicht perfekt, aber ich habe dieses Hilfsmittel für meine Tests in C# geschrieben:

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace Proto.Promises.Tests.Threading
{
    public class ThreadHelper
    {
        public static readonly int multiThreadCount = Environment.ProcessorCount * 100;
        private static readonly int[] offsets = new int[] { 0, 10, 100, 1000 };

        private readonly Stack<Task> _executingTasks = new Stack<Task>(multiThreadCount);
        private readonly Barrier _barrier = new Barrier(1);
        private int _currentParticipants = 0;
        private readonly TimeSpan _timeout;

        public ThreadHelper() : this(TimeSpan.FromSeconds(10)) { } // 10 second timeout should be enough for most cases.

        public ThreadHelper(TimeSpan timeout)
        {
            _timeout = timeout;
        }

        /// <summary>
        /// Execute the action multiple times in parallel threads.
        /// </summary>
        public void ExecuteMultiActionParallel(Action action)
        {
            for (int i = 0; i < multiThreadCount; ++i)
            {
                AddParallelAction(action);
            }
            ExecutePendingParallelActions();
        }

        /// <summary>
        /// Execute the action once in a separate thread.
        /// </summary>
        public void ExecuteSingleAction(Action action)
        {
            AddParallelAction(action);
            ExecutePendingParallelActions();
        }

        /// <summary>
        /// Add an action to be run in parallel.
        /// </summary>
        public void AddParallelAction(Action action)
        {
            var taskSource = new TaskCompletionSource<bool>();
            lock (_executingTasks)
            {
                ++_currentParticipants;
                _barrier.AddParticipant();
                _executingTasks.Push(taskSource.Task);
            }
            new Thread(() =>
            {
                try
                {
                    _barrier.SignalAndWait(); // Try to make actions run in lock-step to increase likelihood of breaking race conditions.
                    action.Invoke();
                    taskSource.SetResult(true);
                }
                catch (Exception e)
                {
                    taskSource.SetException(e);
                }
            }).Start();
        }

        /// <summary>
        /// Runs the pending actions in parallel, attempting to run them in lock-step.
        /// </summary>
        public void ExecutePendingParallelActions()
        {
            Task[] tasks;
            lock (_executingTasks)
            {
                _barrier.SignalAndWait();
                _barrier.RemoveParticipants(_currentParticipants);
                _currentParticipants = 0;
                tasks = _executingTasks.ToArray();
                _executingTasks.Clear();
            }
            try
            {
                if (!Task.WaitAll(tasks, _timeout))
                {
                    throw new TimeoutException($"Action(s) timed out after {_timeout}, there may be a deadlock.");
                }
            }
            catch (AggregateException e)
            {
                // Only throw one exception instead of aggregate to try to avoid overloading the test error output.
                throw e.Flatten().InnerException;
            }
        }

        /// <summary>
        /// Run each action in parallel multiple times with differing offsets for each run.
        /// <para/>The number of runs is 4^actions.Length, so be careful if you don't want the test to run too long.
        /// </summary>
        /// <param name="expandToProcessorCount">If true, copies each action on additional threads up to the processor count. This can help test more without increasing the time it takes to complete.
        /// <para/>Example: 2 actions with 6 processors, runs each action 3 times in parallel.</param>
        /// <param name="setup">The action to run before each parallel run.</param>
        /// <param name="teardown">The action to run after each parallel run.</param>
        /// <param name="actions">The actions to run in parallel.</param>
        public void ExecuteParallelActionsWithOffsets(bool expandToProcessorCount, Action setup, Action teardown, params Action[] actions)
        {
            setup += () => { };
            teardown += () => { };
            int actionCount = actions.Length;
            int expandCount = expandToProcessorCount ? Math.Max(Environment.ProcessorCount / actionCount, 1) : 1;
            foreach (var combo in GenerateCombinations(offsets, actionCount))
            {
                setup.Invoke();
                for (int k = 0; k < expandCount; ++k)
                {
                    for (int i = 0; i < actionCount; ++i)
                    {
                        int offset = combo[i];
                        Action action = actions[i];
                        AddParallelAction(() =>
                        {
                            for (int j = offset; j > 0; --j) { } // Just spin in a loop for the offset.
                            action.Invoke();
                        });
                    }
                }
                ExecutePendingParallelActions();
                teardown.Invoke();
            }
        }

        // Input: [1, 2, 3], 3
        // Ouput: [
        //          [1, 1, 1],
        //          [2, 1, 1],
        //          [3, 1, 1],
        //          [1, 2, 1],
        //          [2, 2, 1],
        //          [3, 2, 1],
        //          [1, 3, 1],
        //          [2, 3, 1],
        //          [3, 3, 1],
        //          [1, 1, 2],
        //          [2, 1, 2],
        //          [3, 1, 2],
        //          [1, 2, 2],
        //          [2, 2, 2],
        //          [3, 2, 2],
        //          [1, 3, 2],
        //          [2, 3, 2],
        //          [3, 3, 2],
        //          [1, 1, 3],
        //          [2, 1, 3],
        //          [3, 1, 3],
        //          [1, 2, 3],
        //          [2, 2, 3],
        //          [3, 2, 3],
        //          [1, 3, 3],
        //          [2, 3, 3],
        //          [3, 3, 3]
        //        ]
        private static IEnumerable<int[]> GenerateCombinations(int[] options, int count)
        {
            int[] indexTracker = new int[count];
            int[] combo = new int[count];
            for (int i = 0; i < count; ++i)
            {
                combo[i] = options[0];
            }
            // Same algorithm as picking a combination lock.
            int rollovers = 0;
            while (rollovers < count)
            {
                yield return combo; // No need to duplicate the array since we're just reading it.
                for (int i = 0; i < count; ++i)
                {
                    int index = ++indexTracker[i];
                    if (index == options.Length)
                    {
                        indexTracker[i] = 0;
                        combo[i] = options[0];
                        if (i == rollovers)
                        {
                            ++rollovers;
                        }
                    }
                    else
                    {
                        combo[i] = options[index];
                        break;
                    }
                }
            }
        }
    }
}

Beispiel für die Verwendung:

[Test]
public void DeferredMayBeBeResolvedAndPromiseAwaitedConcurrently_void0()
{
    Promise.Deferred deferred = default(Promise.Deferred);
    Promise promise = default(Promise);

    int invokedCount = 0;

    var threadHelper = new ThreadHelper();
    threadHelper.ExecuteParallelActionsWithOffsets(false,
        // Setup
        () =>
        {
            invokedCount = 0;
            deferred = Promise.NewDeferred();
            promise = deferred.Promise;
        },
        // Teardown
        () => Assert.AreEqual(1, invokedCount),
        // Parallel Actions
        () => deferred.Resolve(),
        () => promise.Then(() => { Interlocked.Increment(ref invokedCount); }).Forget()
    );
}

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X