Electro_Attacks Ответов: 1

Как добавить строки в datatable одновременно (многопоточность)


Я сделал IP-сканер, который содержит Backgroundworker для обработки процесса сканирования. Backgroundworker создает новые потоки, которые выполняли реальное сканирование для каждого IP-адреса...
Моя проблема заключается в том, что мне нужно собрать результаты сканирования для каждого IP-адреса, и эта коллекция должна быть потокобезопасной, особенно в письменной форме...

Код для моего Backgroundworker

Private Sub NwS_BackgroundWorker_DoWork(sender As Object, e As ComponentModel.DoWorkEventArgs) Handles NwS_BackgroundWorker.DoWork
        Dim toDoList As DataTable = e.Argument
        
        ' Progress Calculation
        Dim total As Int64 = NwS_ScanTotal(toDoList), current As Int64 = 0, progress As Int64 = 0

        ' Test for a Network Connection
        Dim netStatus As Boolean = False, netInterfaces As String = ""
        For Each networkCard As NetworkInterface In NetworkInterface.GetAllNetworkInterfaces
            If networkCard.Name = "Ethernet" Or networkCard.Name = "WLAN"
                netInterfaces = netInterfaces & $"
{networkCard.Name} : {networkCard.OperationalStatus.ToString}"
                If networkCard.OperationalStatus = OperationalStatus.Up Then netStatus = True
            End If
        Next
        If netStatus = False
            Dim messageBox = New Message_Box($"No Network Connection detected.{netInterfaces}", MetroColorStyle.Red)
            If messageBox.ShowDialog() = DialogResult.OK
                e.Cancel = True
                NwS_BackgroundWorker.CancelAsync()
            End If
        End If

        
        For Each ipRange As DataRow In toDoList.Rows
            
            Dim ipExport As IPAddress, endIp = Split(ipRange.Item(1), "."), startIp = Split(ipRange.Item(0), ".")

            ' Create a ThreadList
            Dim threadList As List(Of Thread) = New List(Of Thread)

            ' Scan all IPs
            For A As Int16 = startIp(0) To endIp(0)
                For B As Int16 = startIp(1) To endIp(1)
                    For C As Int16 = startIp(2) To endIp(2)
                        For D As Int16 = startIp(3) To endIp(3)
                            ipExport = Net.IPAddress.Parse(A & "." & B & "." & C & "." & D)

                            ' Report Progress
                            current += 1
                            progress = current/ total * 100
                            NwS_BackgroundWorker.ReportProgress(progress, $"Scanning {ipExport}")

                            ' Creating and starting new Background Thread
                            Dim oThread As New Thread(AddressOf Me.NwS_PingHost)
                            oThread.IsBackground = True
                            threadList.Add(oThread)
                            oThread.Start(ipExport)

                            ' End Conditions
                            If NwS_BackgroundWorker.CancellationPending
                                e.Cancel = True
                                Exit Sub
                            End If
                            Next

                        ' Handbrake for decreasing CPU Usage
                        NwS_BackgroundWorker.ReportProgress(progress, " ")
                        NwS_Handbrake(threadList, False)
                        threadList.Clear()
                    Next
                Next
            Next
        Next
        GC.Collect
    End Sub


Сканирование Хоста добавление потоков данных
Private Sub NwS_PingHost(ByVal ipExport As IPAddress)
' Set NetworkScan Variables for Ping Request
        Dim status As String = "Unknown", hostName As String = " ", macImport As String = " ", macAddress As String = " "
        Dim ipLong As Int64 = IPtoLong(ipExport)

        Dim result As Net.NetworkInformation.PingReply
        Dim sendPing As New Net.NetworkInformation.Ping

        Try
            ' Try to send Ping Request
            result = sendPing.Send(ipExport, 120)
            If result.Status = Net.NetworkInformation.IPStatus.Success Then

                ' Resolve Hostname
                status = "Used"
                hostName = Net.Dns.GetHostEntry(ipExport).HostName

                'Resolve Mac- Address
                Dim arp = New ArpRequest(ipExport)
                macImport = arp.GetResponse().ToString()
                If macImport <> Nothing Then
                    macAddress = Regex.Replace(macImport, "(.{2})(.{2})(.{2})(.{2})(.{2})(.{2})", "$1:$2:$3:$4:$5:$6")
                End If
            Else
                status = "Free"
                hostName = ""
            End If
            

            ' Ping Exception: Non valid IP
        Catch ex As PingException
            status = "Unknown"

            ' Hostname Exception: IP resolving Error
        Catch ex As SocketException
            status = "Unknown"

            ' Hostname Exception: Non valid IP
        Catch ex As System.ArgumentException
            status = "Unknown"

            ' Mac Exceptions: (Arp Request)
        Catch ex As System.ComponentModel.Win32Exception
            If ex.NativeErrorCode = 67 Then
                status = "Used"
                macAddress = "--:--:--:--:--:--"
            ElseIf ex.NativeErrorCode = 111 Then
                status = "Used"
                macAddress = "--:--:--:--:--:--"
            ElseIf ex.NativeErrorCode = 31 Then
                status = "Used"
                macAddress = "--:--:--:--:--:--"
            ElseIf ex.NativeErrorCode = 87 Then
                status = "Used"
                macAddress = "--:--:--:--:--:--"
            ElseIf ex.NativeErrorCode = 1784 Then
                status = "Used"
                macAddress = "--:--:--:--:--:--"
            ElseIf ex.NativeErrorCode = 1168 Then
                status = "Used"
                macAddress = "--:--:--:--:--:--"
            End If
        Catch ex As Exception

        End Try

        ' Add results to DataGrid and remove Thread from List
        If status = "Used" Then
            NwSData.Rows.Add(My.Resources.Host, ipExport.ToString, hostName, macAddress, status, ipLong.ToString("D10"))
        ElseIf status = "Free" Then
            NwSData.Rows.Add(My.Resources.Free, ipExport.ToString, hostName, macAddress, status, ipLong.ToString("D10"))
        Else
            NwSData.Rows.Add(My.Resources.Unresolvable, ipExport.ToString, hostName, macAddress, status, ipLong.ToString("D10"))
        End If

        ' CleanUP
        ipExport = Nothing
        status = ""
        hostName = ""
        macImport = ""
        macAddress = ""
    End Sub


Что я уже пробовал:

Я создал DataTable Object = Unsafe for multithreading --> исключение в больших сканированиях "внутренний индекс поврежден '5'"
Я искал потокобезопасные методы для добавления строк или избегал этого исключения...
Источник данных должен быть привязан к DTGV, иначе загрузка всех результатов (например, 0.0.0.0 - 255.255.255.255) приведет к замораживанию или отмене программы...
У тебя есть какие-нибудь идеи?

Мои Источники Данных:
Public NwSData As DataTable = New DataTable("NwS_Log")
    Public NwSBinding As BindingSource = New BindingSource()
    ' Create NwS DataSource
    Private Sub NwS_DataSource()
        NwSData.Columns.Add("NwS_Column1", GetType(System.Drawing.Image))
        NwSData.Columns.Add("NwS_Column2", GetType(String))
        NwSData.Columns.Add("NwS_Column3", GetType(String))
        NwSData.Columns.Add("NwS_Column4", GetType(String))
        NwSData.Columns.Add("NwS_Column5", GetType(String))
        NwSData.Columns.Add("NwS_Column6", GetType(String))
        NwSBinding.DataSource = NwSdata
        NwS_Log.DataSource = NwSBinding
        NwS_Log.Columns(0).AutoSizeMode = DataGridViewAutoSizeColumnMode.None
        NwS_Log.Columns(0).HeaderText = "Status"
        NwS_Log.Columns(1).AutoSizeMode = DataGridViewAutoSizeColumnMode.None
        NwS_Log.Columns(1).HeaderText = "IP Address"
        NwS_Log.Columns(2).AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill
        NwS_Log.Columns(2).HeaderText = "Hostname"
        NwS_Log.Columns(3).AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill
        NwS_Log.Columns(3).HeaderText = "Mac - Address"
        NwS_Log.Columns(4).Visible = False
        NwS_Log.Columns(4).HeaderText = "Status String"
        NwS_Log.Columns(5).Visible = False
        NwS_Log.Columns(5).HeaderText = "Bytes"

        ' Put each of the columns into programmatic sort mode.
        For Each column As DataGridViewColumn In NwS_Log.Columns
            column.SortMode = DataGridViewColumnSortMode.Programmatic
        Next
    End Sub

1 Ответов

Рейтинг:
12

Richard Deeming

Как вы уже выяснили, DataTable класс не является потокобезопасным. Если вы попытаетесь добавить строки из нескольких потоков одновременно, внутреннее состояние будет повреждено.

Простое решение состоит в том, чтобы обернуть сложение строк в a SyncLock блок:

' Add results to DataGrid and remove Thread from List
SyncLock NwSData
    If status = "Used" Then
        NwSData.Rows.Add(My.Resources.Host, ipExport.ToString, hostName, macAddress, status, ipLong.ToString("D10"))
    ElseIf status = "Free" Then
        NwSData.Rows.Add(My.Resources.Free, ipExport.ToString, hostName, macAddress, status, ipLong.ToString("D10"))
    Else
        NwSData.Rows.Add(My.Resources.Unresolvable, ipExport.ToString, hostName, macAddress, status, ipLong.ToString("D10"))
    End If
End SyncLock
Оператор SyncLock - Visual Basic | Microsoft Docs[^]

Вам нужно будет измерить, какое влияние это оказывает на производительность вашего кода, прежде чем решить, следует ли исследовать более сложное решение.

РЕДАКТИРОВАТЬ:
Основываясь на трассировке стека исключения, я подозреваю, что сетке не нравится, когда источник данных обновляется из фонового потока. Возможно, вам придется использовать BeginInvoke чтобы обновить таблицу из потока пользовательского интерфейса, который устранил бы необходимость в SyncLock блок. Попробуйте что-нибудь вроде этого:
' Add results to DataGrid and remove Thread from List
If status = "Used" Then
    BeginInvoke(Sub()
        NwSData.Rows.Add(My.Resources.Host, ipExport.ToString, hostName, macAddress, status, ipLong.ToString("D10"))
    End Sub)
ElseIf status = "Free" Then
    BeginInvoke(Sub()
        NwSData.Rows.Add(My.Resources.Free, ipExport.ToString, hostName, macAddress, status, ipLong.ToString("D10"))
    End Sub)
Else
    BeginInvoke(Sub()
        NwSData.Rows.Add(My.Resources.Unresolvable, ipExport.ToString, hostName, macAddress, status, ipLong.ToString("D10"))
    End Sub)
End If


[no name]

Ладно, вчера я попробовал Synclock, но, может быть, я слишком устал... Потому что я использовал его для всего Сканхоста ---> Очень большое влияние, лол
Теперь с добавлением только в DataRow влияние почти не измеримо :)
Так что спасибо за эту первую идею :)

Но в какой-то момент на моем тестовом сканировании (что всегда приводило к исключению выше "0.0.0.0 - 254 + 100.0.0.1 - 192.0.0.254) он выдает мне новое исключение "System.NullReferenceException: ссылка на объект не была установлена на объект"
Помечается, что Х:
NwSData.Rows.Add(My.Resources.Бесплатно, ipExport.ToString, имя хоста, MAC-адрес, статус, ipLong.ToString("D10"))

У вас есть идея, откуда это берется?

Richard Deeming

Говорит ли он вам, какая переменная находится в этой строке Nothing?

[no name]

Это подробный список для исключения, отладчик не упоминает имя varname
ipExport = {110.0.0.219}
имя хоста = ""
maxAddress = " "
статус = "бесплатно"
Мой.Ресурсы.Бесплатная система.Рисование.Растровый}
ipLong = 1845493979
Дело в том, что мне интересно, что NwSData = {NwS_Log} (мой DTGV)

Системы.Исключение NullReferenceException
HResult=0x80004003
Message = ссылка на объект не была установлена на объект
исходная система.Окна.Формы
Фоновый Мониторинг:
в системе.Окна.Формы.Практическое руководство.FlushDisplayedChanged()
в системе.Окна.Формы.Практическое руководство.PerformLayoutPrivate(логическое useRowShortcut, логическое computeVisibleRows, логическое invalidInAdjustFillingColumns, логическое repositionEditingControl)
в системе.Окна.Формы.DataGridView.ResetUIState(Boolean useRowShortcut, Boolean computeVisibleRows)
в системе.Окна.Формы.DataGridViewRowCollection.OnCollectionChanged_PreNotification(ОСО CollectionChangeAction и rowindex типа int32, int32 значение количества строк, ячейки datagridviewrow&амп; ячейки datagridviewrow, логическое changeIsInsertion)
в системе.Окна.Формы.DataGridViewRowCollection.OnCollectionChanged(CollectionChangeEventArgs е и rowindex типа int32, int32 значение количества строк, логическое changeIsDeletion, логическое changeIsInsertion, логическое recreateNewRow, точка newCurrentCell)
в системе.Окна.Формы.DataGridViewRowCollection.InsertInternal(параметр rowindex типа int32, ячейки datagridviewrow ячейки datagridviewrow, логическое силу)
в системе.Окна.Формы.Практическое руководство.DataGridViewDataConnection.ProcessListChanged(ListChangedEventArgs e)
в системе.Окна.Формы.DataGridView.DataGridViewDataConnection.currencyManager_ListChanged(отправитель объекта, ListChangedEventArgs e)
в системе.Окна.Формы.CurrencyManager.OnListChanged(ListChangedEventArgs e)
в системе.Окна.Формы.CurrencyManager.List_ListChanged(отправитель объекта, ListChangedEventArgs e)
в системе.Окна.Формы.Объектом bindingsource.OnListChanged(ListChangedEventArgs e)
в системе.Окна.Формы.Объектом bindingsource.InnerList_ListChanged(отправитель объекта, ListChangedEventArgs e)
в System.Data.DataView.OnListChanged(ListChangedEventArgs e)
в System.Data.DataView.IndexListChanged(отправитель объекта, ListChangedEventArgs e)
в системе.Данных.Слушателей 1.Уведомить[Т1,Т2,Т3](Т1 арг1, арг2 Т2, Т3 значение arg3, действие 4 Действие)
в System.Data.Индекс.OnListChanged(ListChangedEventArgs e)
в System.Data.Индекс.OnListChanged(ListChangedType changedType, индекс Int32)
в System.Data.Индекс.InsertRecord(Int32 record, Boolean fireEvent)
в System.Data.DataTable.RecordStateChanged(типа int32 истории1, dataviewrowstate вместе oldState1, dataviewrowstate вместе newState1, int32 и record2, dataviewrowstate вместе oldState2, dataviewrowstate вместе newState2)
в System.Data.DataTable.SetNewRecordWorker(Строкаданных подряд, типа int32 proposedRecord, DataRowAction действий, логическое isInMerge, логическое suppressEnsurePropertyChanged, установки типа int32, boolean значение кнопкуfireevent, исключение&амп; deferredException)
в System.Data.DataTable.Метод insertrow(Строкаданных подряд, типа int64 proposedID, int32 значение поз, логическое кнопкуfireevent)
в System.Data.DataRowCollection.Добавить(Object[] values)
в IP_Tool.Главная.NwS_PingHost(ipExport адрес) в Атак C:\Users\Electro \источник\репозитории\продвинутый инструмент ИС\Главная.В. Б.:линия 535
в IP_Tool.Main._Lambda$__R260-16(объект a0)
в системе.Нарезание резьбы.Параллельном режиме.RunInternal(параллельном режиме параллельном режиме, ContextCallback обратного вызова, состояние объекта, логическое preserveSyncCtx)
в системе.Нарезание резьбы.Параллельном режиме.Выполнения(в параллельном режиме параллельном режиме, ContextCallback обратного вызова, состояние объекта, логическое preserveSyncCtx)
в системе.Нарезание резьбы.Параллельном режиме.Выполнения(в параллельном режиме параллельном режиме, ContextCallback обратного вызова, состояние объекта)
в системе.Нарезание резьбы.ThreadHelper.ThreadStart(Object obj)

Это исключение было первоначально вызвано этим списком вызовов:
[внешний код]
IP_Tool.Главная.NwS_PingHost(System.Net.IPAddress) в Main.vb
[внешний код]

Richard Deeming

Основываясь на трассировке стека, я подозреваю, что представление сетки не любит, когда его источник данных обновляется из фонового потока. Вы можете попробовать использовать BeginInvoke чтобы обновить таблицу из потока пользовательского интерфейса. Я добавил образец к своему решению - хотя я немного заржавел на точном VB.NET синтаксис. :)

[no name]

Ну, а когда я снова начинаю с Invokes, не замораживает ли он мой пользовательский интерфейс, когда ~ 200 потоков хотят обновить DataGridView через Invokes?
Не было бы лучше, если бы я использовал NwSBinding.SuspendBinding() в начале сканирования и чем NwSBidning.ResumeBinding() это на заднем плане рабочего.Событие ReportProgress?

И меня удивляет, что DTGV распознает его так поздно, что обновления поступали из разных потоков, а не из пользовательского интерфейса... Это было около 2.805 строк, прежде чем он проверял его... И до того как исключение было выброшено оно обновлялось очень быстро так что я мог видеть каждую новую запись :/

Я думаю, что это может быть проблема, созданная BindingSource...

Richard Deeming

Я бы очень надеялся, что Invoked-код достаточно мал, чтобы полностью заморозить пользовательский интерфейс, но вам придется его протестировать, чтобы увидеть.

В противном случае, предполагая, что NwS_Handbrake метод ожидает завершения ожидающих потоков, вы можете возобновить, а затем повторно приостановить привязку, когда это будет сделано. Вы должны были бы убедиться, что всегда возобновляете привязку в RunWorkerCompleted событие, чтобы справиться с отменой и ошибками.

[no name]

Так что теперь я попробовал сканировать снова без каких-либо изменений, и он завершил сканирование без каких-либо проблем... (Ну, полоса прокрутки TGV все еще есть... Думаю, это можно назвать уничтожением...)

Так что, поскольку я пробовал те же диапазоны и с теми же условиями (ручной тормоз отключен), это может быть снова гоночное условие... Вопрос в другом: откуда она взялась? xD
Таким образом, чтение публичных vars не должно быть проблемой, но что такое счетчик производительности?
И тогда я должен выяснить, как я могу исправить полосу прокрутки на больших сканированиях... Может быть, перерисовать DTGV или бар?

Private Sub NwS_Handbrake(ByVal threadList As List(Of Thread), ByVal Mode As Boolean)
        Do
            Dim pause As Boolean = False

            ' Handbrake: Wait for Threads
            Dim tAlive As Integer = 0
            If Mode Or My.Settings.Handbrake
                For Each t In threadList
                    If t.IsAlive() <> False Then
                        tAlive += 1
                    End If
                Next t

                If tAlive > 0 Then pause = True
            End If

            ' Handbrake: CPU Limit
            If My.Settings.Handbrake = False And Mode = False
                Dim cpuLoad = Main_cpuWatcher.NextValue()
                If cpuLoad > 80 Then pause = True
            End IF

            If pause = False Then Exit Do
            Thread.Sleep(1000)
        Loop
    End Sub

[no name]

Итак последнее обновление:

Большое спасибо за Ваше время, без вас я бы все равно за Потокобезопасные таблицы данных, просто потому, что я использовал использование Synclock в неправильной точке xD

Теперь программа работает нормально, полосы прокрутки тоже были исправлены, просто перерисовав их с помощью:
NwS_Log.ScrollBars = ScrollBars.None
NwS_Log.ScrollBars = ScrollBars.Vertical


Тодд Говард сказал бы: "это просто работает" xD

Maciej Los

5ed!