MultiThreading mit SQL Server

  • C#
  • .NET (FX) 4.0

Es gibt 9 Antworten in diesem Thema. Der letzte Beitrag () ist von Rattenfänger.

    MultiThreading mit SQL Server

    Hallo Liebe Gemeinde,

    Erstmal Danke an alle hier im Forum, Ihr seid einfach Super und sehr geduldig :-).

    Nun zu meinen Erfolgen sowie auch einem Problem, was ich nicht lösen kann bzw. nicht weiß wie ich das verhindern ändern kann.

    Erfolge:
    Habe es heute endlich geschafft meinen SQL-Test im Multithreading abzuschließen und bin begeistert.
    konnte mit 20 Threads in meine Lokale SQL-Devoloper Datenbank schreiben.
    Test war 1000000 Datensätze simultan in die Tabelle zu schreiben.
    Das funktioniert wunderbar hat nur 86 sek. gedauert. Für lokal und für einen Test supi.

    Problem:
    Jedoch mache ich beim erstellen der Tabelle noch was Falsch. Und zwar zeigt er mir einen Indexierungsfehler bei diesem Schlüssel an(Bild Anhang). Dieser Schlüssel wird automatisch vom System erstellt und ich musste Ihn löschen damit es Problemlos funktioniert. Nur gehe ich mal davon aus das es einen besseren weg gibt ohne den Schlüssel zu löschen. Hier mal der Code mit dem ich die Tabelle erstelle.

    C#-Quellcode

    1. public void CreateNewRealTimeTable(string _TableName)
    2. {
    3. SqlConnection con = new SqlConnection
    4. {
    5. ConnectionString = "Data Source= ***********\\TZCADSQLSERVER;" +
    6. "Initial Catalog = RealTimeDatabase;" +
    7. "Integrated Security= True;"
    8. };
    9. con.Open();
    10. _TableName = "CREATE TABLE " + _TableName;
    11. _TableName = _TableName + "(InstrumentID int NOT NULL PRIMARY KEY,";
    12. _TableName = _TableName + "Instrument NCHAR(6) NULL,";
    13. _TableName = _TableName + "Time datetime NULL,";
    14. _TableName = _TableName + "Bid NUMERIC(18, 7) NULL,";
    15. _TableName = _TableName + "Ask NUMERIC(18, 7) NULL,";
    16. _TableName = _TableName + "High NUMERIC(18, 7) NULL,";
    17. _TableName = _TableName + "Low NUMERIC(18, 7) NULL,";
    18. _TableName = _TableName + "PipCost NUMERIC(18, 7) NULL);";
    19. using (SqlCommand cmd = new SqlCommand(_TableName, con))
    20. {
    21. cmd.ExecuteNonQuery();
    22. }
    23. }


    Gruß
    Bilder
    • PK_Schlüssel.PNG

      11 kB, 309×242, 198 mal angesehen
    Habe das Problem gelöst ohne den Schlüssel löschen zu müssen. Das problem lag beim erstellen der Threads. eine kleine sleep Schleife hat das Problem gelöst.

    C#-Quellcode

    1. private void StartThreads(object source, EventArgs args)
    2. {
    3. int anzahlthreads = 30;
    4. for (int i = 0; i <= anzahlthreads - 1; i ++)
    5. {
    6. int _ThreadNr = i + 1;
    7. String _Threadname;
    8. _Threadname = "Thread_" + _ThreadNr.ToString();
    9. System.Threading.Thread thread = new System.Threading.Thread(delegate () { Addrows2(i, 1000000, anzahlthreads, _Threadname); });
    10. thread.Start();
    11. System.Threading.Thread.Sleep(10);
    12. }
    13. _endtime = System.DateTime.Now;
    14. }


    ohne die sleep anweisung, haben manchmal 2 Threads den selben Index erstellt. warum, kann ich leider nicht sagen. aber jetzt funktioniert es.

    In der praxis werde ich bei meinem System, niemals gleichzeitig soviele Threads brauchen, es ist halt nur ein Test um zu sehen was möglich ist.
    Da du ja nix über den Schlüssel geschrieben hast kann ich nur vermuten das dieser anhand eines Timestamps generiert wird. Und wenn 2 Verbindungen mal zufällig zur Gleichen Zeit inserten dann kann es je nach Genauigkeit des Timestamps vermutlich mal zu gleichen Werten kommen.

    Probier mal mit deiner Tabelle folgendes
    Spoiler anzeigen

    VB.NET-Quellcode

    1. Private Shared Sub Main(ByVal args As String())
    2. Console.WriteLine("Enter the table name to insert:")
    3. Dim _TableName = Console.ReadLine()
    4. Dim sw = New Stopwatch()
    5. sw.Start()
    6. Dim sql = New SqlConnection("YourConnectionstring")
    7. sql.Open()
    8. Console.WriteLine("Connected: " & sw.Elapsed)
    9. Dim _sql = "CREATE TABLE " & _TableName
    10. _sql = _sql & "(InstrumentID int NOT NULL PRIMARY KEY,"
    11. _sql = _sql & "Instrument NCHAR(6) NULL,"
    12. _sql = _sql & "[Time] datetime NULL,"
    13. _sql = _sql & "Bid NUMERIC(18, 7) NULL,"
    14. _sql = _sql & "Ask NUMERIC(18, 7) NULL,"
    15. _sql = _sql & "High NUMERIC(18, 7) NULL,"
    16. _sql = _sql & "Low NUMERIC(18, 7) NULL,"
    17. _sql = _sql & "PipCost NUMERIC(18, 7) NULL);"
    18. Using cmd = New SqlCommand(_sql, sql)
    19. cmd.ExecuteNonQuery()
    20. End Using
    21. Console.WriteLine("Table created: " & sw.Elapsed)
    22. Dim dt = New DataTable()
    23. dt.Columns.Add("InstrumentID")
    24. dt.Columns.Add("Instrument")
    25. dt.Columns.Add("Time")
    26. dt.Columns.Add("Bid")
    27. dt.Columns.Add("Ask")
    28. dt.Columns.Add("High")
    29. dt.Columns.Add("Low")
    30. dt.Columns.Add("PipCost")
    31. Dim bulk = New SqlBulkCopy(sql) With {.DestinationTableName = _TableName, .BatchSize = 10000}
    32. For i = 0 To 1000000 - 1
    33. Dim row = dt.NewRow()
    34. row("Instrument") = "abcede"
    35. row("Time") = DateTime.Now
    36. row("InstrumentID") = i
    37. row("Bid") = i
    38. row("Ask") = i
    39. row("High") = i
    40. row("Low") = i
    41. row("PipCost") = i
    42. dt.Rows.Add(row)
    43. Next
    44. Console.WriteLine("In Memory: " & sw.Elapsed)
    45. bulk.WriteToServer(dt)
    46. sw.[Stop]()
    47. Console.WriteLine("In database: " & sw.Elapsed)
    48. Console.Read()
    49. End Sub


    Dauert bei mir 40 Sekunden mit allem und ist völlig ohne Threading. Würde mich mal interessieren wie das im Vgl dazu abschneidet von der Performance.

    LG
    Das ist meine Signatur und sie wird wunderbar sein!
    Mir ist noch nicht klar warum Threading irgendeinen Einfluss auf nen Datenbank-Server haben soll. Die Dinger sind ja dafür gemacht viele Kanäle offen zu halten zu diversen Usern. Was mir bei dem Code den man sieht nicht klar wird ist, ob jeder Thread einen eigenen Kanal öffnet oder alle über einen funken. Ich würde sogar soweit gehen zu behaupten, dass das Codeseitige erstellen der Datensätze länger dauert als das schreiben der Datensätze auf die Datenbank. Die Performance ist natürlich in deinem Fall weiterhin davon abhängig, dass beides über den selben Rechner läuft. Ich vermute mal du hast keine 20 Kerne, wie der kleinere Server den ich gerade betreue ;D
    @Mono Teste ich gleich mal aus. Aber so wie ich das sehe schreibst du erst in eine Datable, die ja nicht Multithreading fähig beim Schreiben ist(nach meinem aktuellen stand), was ich aber unbedingt brauche.

    @KBT habe selbst noch keinen server(also seperaten rechner) wo die Datenbank liegt. Aktuell ist alles noch Lokal und wie oben schon geschrieben, ist dies nur ein Test gewesen. Nein habe Lokal nur 4CPU kerne(G7 Laptop), das wird für meine Anwendung auch reichen :-).

    Die Threads werden bzw. können gleichzeitigt reinschreiben. Hier sollen diverse Berechnungen in verschiedenen Threads gemacht werden um sehr schnell an Realtime Daten zu kommen.
    Was wird bei folgendem Code ausgegeben?

    C#-Quellcode

    1. var Actions = new List<Action>();
    2. for (int i = 0; i <= 3; i++)
    3. {
    4. Actions.Add(() => { Console.Write(i); });
    5. }
    6. foreach (var i in Actions)
    7. {
    8. i();
    9. }
    10. Console.ReadLine();

    Denke ganz scharf nach. Gehe jeden Schritt im Kopf durch und überlege, was passiert.
    Achte besonders darauf, was es bedeutet, eine Variable von außerhalb einer Lambdamethode innerhalb dieser Lambdamethode zu verwenden.
    Teste den Code. Passiert das, was Du erwartet hast?

    Hier steht die Lösung. Erst überlegen/ausprobieren, dann hier weiterlesen!

    Interessant, dass C# hier keine Warnung bringt. Der äquivalente Code in VB produziert eine Warnung bei i in Zeile 3:

    VB.NET-Quellcode

    1. Dim Actions As New List(Of Action)
    2. For i = 0 To 3
    3. Actions.Add(Sub() Console.Write(i))
    4. Next
    5. For Each i In Actions
    6. i()
    7. Next
    8. Console.ReadLine()

    Die Verwendung der Iterationsvariablen in einem Lambda-Ausdruck kann zu unerwarteten Ergebnissen führen. Erstellen Sie stattdessen in der Schleife eine lokale Variable, und weisen Sie dieser den Wert der Iterationsvariablen zu.


    Dachtest Du, es würde 0123 ausgegeben werden? Dann hast Du beim Punkt "was es bedeutet, eine Variable von außerhalb einer Lambdamethode innerhalb dieser Lambdamethode zu verwenden" nicht richtig aufgepasst.
    Eine Variable von außerhalb einer Lambdamethode innerhalb dieser Lambdamethode zu verwenden bedeutet nämlich, dass der Wert rauskommt, den die Variable zu dem Zeitpunkt hat, an dem die Auswertung stattfindet. Es bedeutet nicht, dass der Wert herauskommt, den die Variable hatte, als der Delegat der Lambdamethode erstellt wurde!

    Überlege nochmal, ob Du jetzt verstehst, warum 4444 ausgegeben wird.

    Hier steht die Lösung. Erst überlegen/ausprobieren, dann hier weiterlesen!

    Die For-Schleife deklariert eine lokale Variable i außerhalb des Schleifeninhalts. Diese Variable wird einmal mit 0 initialisiert und dann am Ende jeder Iteration erhöht.
    Es gibt nur eine Variable.
    Das ist der Knackpunkt. Wenn es nur eine Variable gibt, bei einer Auswertung "der Wert rauskommt, den die Variable zu dem Zeitpunkt hat, an dem die Auswertung stattfindet" und alle Auswertungen geschehen, nachdem die For-Schleife bereits verlassen wurde, dann muss bei allen Auswertungen der gleiche Wert herauskommen.
    Und dieser Wert ist 4, weil i nach der Iteration i == 3 nochmal um 1 erhöht wird. 4 ist dann nicht mehr kleiner oder gleich 3, deshalb wird die For-Schleife verlassen.

    Überlege jetzt, warum das in Deinem Code oben manchmal Probleme macht und wie man es löst.

    Hier steht die Lösung. Erst überlegen/ausprobieren, dann hier weiterlesen!

    Es gibt keine Garantie, dass der Thread, den Du startest, weit genug kommt, i auszuwerten, bevor i am Ende der Iteration inkrementiert wird.
    Die Lösung ist einfach aber unintuitiv. Der Knackpunkt liegt darin, dass es bisher nur eine Variable gibt. Mach mehrere daraus:

    C#-Quellcode

    1. var Actions = new List<Action>();
    2. for (int i = 0; i <= 3; i++)
    3. {
    4. var i2 = i;
    5. Actions.Add(delegate { Console.WriteLine(i2); });
    6. }
    7. foreach (var i in Actions)
    8. {
    9. i();
    10. }
    11. Console.ReadLine();

    Hier hat jede Iteration ihre eigene Variable. Der Delegat, der bei Iteration i == 0 erstellt wird, verwendet eine andere lokale Variable i2 als der Delegat, der bei Iteration i == 1 erstellt wird, etc. Dadurch ist es egal, wann die Auswertung stattfindet. Der Wert der jeweiligen lokalen Variable ändert sich nie.


    "Luckily luh... luckily it wasn't poi-"
    -- Brady in Wonderland, 23. Februar 2015, 1:56
    Desktop Pinner | ApplicationSettings | OnUtils
    Habe mir deine erläuterung angeschaut und verstehe. Deine lösung habe ich mir nicht angeschaut!!! Möchte wirklich selbst darauf kommen und verstehe langsam(10%-20%), habe so was schon geahnt jedoch nicht verstanden und den sleep eingebaut. Bei mir 2017 habe ich kein problem mit den vb code.

    VB.NET-Quellcode

    1. Dim Actions As New List(Of Action)
    2. For i = 0 To 3
    3. Actions.Add(Sub() Console.Write(i))
    4. Next
    5. For Each i In Actions
    6. i()
    7. Next
    8. Console.ReadLine()
    er funktioniert und gibt das selbe ergebniss wie c#
    Perfekt!

    hier meine lösung: ist das richtig?

    VB.NET-Quellcode

    1. Sub Main()
    2. Dim Actions As New List(Of Action)
    3. For i = 0 To 3
    4. Dim i1 As Integer = i
    5. Actions.Add(Sub() Console.Write(i1))
    6. Next
    7. For Each i In Actions
    8. i()
    9. Next
    10. Console.ReadLine()
    11. End Sub


    zumindest gibt es das raus was es soll


    @Rattenfänger
    Es ist durchaus möglich, dass das in späteren Versionen (ich habe hier 2010) geändert wurde und deshalb keine Warnung mehr kommt, sondern im Hintergrund sozusagen eine unsichtbare Variable eingefügt wird.

    Ja, die Lösung ist richtig. Warum sie richtig ist, kannst Du jetzt nachlesen ;)
    "Luckily luh... luckily it wasn't poi-"
    -- Brady in Wonderland, 23. Februar 2015, 1:56
    Desktop Pinner | ApplicationSettings | OnUtils
    habe ich schon und auch verstanden, habe jetzt meinen code dementsprechend geändert! Also ein delegate garantiert nicht das ein übergebenerwert unverändert bleibt, ganz im gegenteil er wird die Variable noch verändern(besonders wenn threads innerhalb kürzesterzeit nebeneinander laufen "Threadunsicher"!)! zumindest in einer schleife! Einfach und simple ausgedrückt.

    Perfekt, du hast mir mit deiner fragestellung sehr viel beigebracht!!!!! DANKE @Niko Ortner

    Hier meine verbesserung :-).

    C#-Quellcode

    1. private void StartThreads(object source, EventArgs args)
    2. {
    3. int anzahlthreads = 10;
    4. for (int i = 0; i <= anzahlthreads - 1; i ++)
    5. {
    6. int _ThreadNr = i + 1;
    7. int _zähler = i;
    8. String _Threadname;
    9. _Threadname = "Thread_" + _ThreadNr.ToString();
    10. System.Threading.Thread thread = new System.Threading.Thread(delegate () { Addrows2(_zähler, 1000000, anzahlthreads, _Threadname); });
    11. thread.Start();
    12. //System.Threading.Thread.Sleep(10);
    13. }
    14. //con.Close();
    15. _endtime = System.DateTime.Now;
    16. }
    17. private void Addrows2(int i3, int i1, int i2, string _Threadname)
    18. {
    19. SqlConnection con = new SqlConnection
    20. {
    21. ConnectionString = "Data Source= **********\\TZCADSQLSERVER;" +
    22. "Initial Catalog = RealTimeDatabase;" +
    23. "Integrated Security= True;"
    24. };
    25. con.Open();
    26. for (int i = i3; i <= i1; i += i2)
    27. {
    28. DateTime _beginThread = DateTime.Now;
    29. testevent prg = new testevent();
    30. SqlCommand command = new SqlCommand("INSERT INTO RealTimeTable (InstrumentID, Instrument)" +
    31. "Values(" + i + ", '" + _Threadname + "')", con);
    32. command.ExecuteNonQuery();
    33. }
    34. con.Close();
    35. _endtime = DateTime.Now;
    36. }

    Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „Rattenfänger“ ()