I have a code below that loops thru each row in a datagridview which contains the server and the service name and uses the ServiceController reference to check the status of each service and return the value on the cell.
For Each dgvrow As DataGridViewRow In DataGridView1.Rows
myController = New ServiceController With {
.MachineName = dgvrow.Cells(0).Value,
.ServiceName = dgvrow.Cells(1).Value
}
dgvrow.Cells(2).Value = myController.Status.ToString
Next
This works but it runs sequentially and it is taking time for each thread to finish before it goes to the next row so I want to run it on parallel.
I search here and stumbled upon Parallel.ForEach but I could not find the right code/combination to make this work.
My intitial attempt was
Parallel.ForEach(dgvrow as DataGridViewRow in DatagridView1.Rows
Sub(myServer)
myController = New ServiceController With {
.MachineName = dgvrow.Cells(0).Value,
.ServiceName = dgvrow.Cells(1).Value
}
dgvrow.Cells(2).Value = myController.Status.ToString
End Sub
)
Which is definitely wrong, not sure what to put after the ForEach part
The expected result should look like below, I expect the Service Status Column to get filled up simultaneously.
<table border=1>
<tr>
<th>Server Name</th>
<th>Service Name</th>
<th>Service Status</th>
</tr>
<tr>
<th>Server 1</th>
<th>Service 1</th>
<th>Not Running</th>
</tr>
<tr>
<th>Server 2</th>
<th>Service 2</th>
<th>Running</th>
</tr>
<tr>
<th>Server 3</th>
<th>Service 3</th>
<th>Not Running</th>
</tr>
</table>
A DataGridView
is a UI element - you're not allowed to access it from any thread other than the UI. You can't use Parallel.ForEach
on it.
What you can do is extract the data as a non-UI object, work in parallel on that, and then assign the results back.
Try this:
'UI thread
Dim inputs = DataGridView1.Rows.Cast(Of DataGridViewRow).Select(Function(dgvrow) New With _
{
.MachineName = dgvrow.Cells(0).Value,
.ServiceName = dgvrow.Cells(1).Value
}).ToArray()
'Parallel
Dim serviceControllers = inputs.AsParallel().Select(Function (x) New ServiceController With _
{
.MachineName = x.MachineName,
.ServiceName = x.ServiceName
}).ToArray()
'UI thread
For Each x In serviceControllers.Zip(DataGridView1.Rows.Cast(Of DataGridViewRow), Function (sc, dgvrow) New With { sc, dgvrow })
x.dgvrow.Cells(2).Value = x.sc.Status.ToString
Next
Use this if the rows change order during the computation.
'UI thread
Dim inputs = DataGridView1.Rows.Cast(Of DataGridViewRow).Select(Function(dgvrow) New With _
{
.MachineName = dgvrow.Cells(0).Value,
.ServiceName = dgvrow.Cells(1).Value,
.dgvrow = dgvrow
}).ToArray()
'Parallel
Dim results = inputs.AsParallel().Select(Function (x) New With
{
.sc = New ServiceController With _
{
.MachineName = x.MachineName,
.ServiceName = x.ServiceName
}, _
.dgvrow = x.dgvrow
}).ToArray()
'UI thread
For Each x In results
x.dgvrow.Cells(2).Value = x.sc.Status.ToString
Next
Keep in mind if rows are deleted during computation this will also fail.
You should use Microsoft's Reactive Framework (aka Rx) - NuGet System.Reactive.Windows.Forms
and add using System.Reactive.Linq;
- then you can do this:
'UI thread
Dim inputs = DataGridView1.Rows.Cast(Of DataGridViewRow).Select(Function(dgvrow) New With _
{
.MachineName = CType(dgvrow.Cells(0).Value, String),
.ServiceName = CType(dgvrow.Cells(1).Value, String),
.Row = dgvrow
}).ToArray()
'Rx query
Dim query = _
From x In inputs.ToObservable()
From s In Observable.Start(Function () FetchStatus(x.MachineName, x.ServiceName))
Select New With
{
x.Row,
.Status = s
}
'Rx subscription
Dim subscription As IDisposable = _
query _
.ObserveOn(DataGridView1) _
.Subscribe(Sub (x) x.Row.Cells(2).Value = x.Status)
This will update each row as quickly as the respond comes back - and it'll be processed in parallel.
I have assumed that you have a function with this signature: Function FetchStatus(MachineName As String, ServiceName As String) As String
.