sergiu reznicencu sergiu reznicencu - 4 months ago 23
Vb.net Question

Reading and writing from queue

I'm saving a set of ~300 bitmaps in a concurrent queue. I'm doing this for an over-tcp video streaming program. If the server slows down I save the received bitmaps in this queue (buffering). I created a separate project to test this but I'm having some problems.

While the writing thread is working (writing to the queue) the picture box is showing the images from the queue but it seems that it skips many of them(it is like it's reading the picture just added in the "list" by the writing thread-not FIFO behaviour). When the writing thread finishes the picture box it blocks although the loop in which I read from the queue is still working (when the picture box blocks the queue is not empty).

Here's the code:

Imports System
Imports System.Drawing
Imports System.IO
Imports System.Threading
Imports System.Collections.Concurrent

Public Class Form1
Dim writeth As New Thread(AddressOf write), readth As New Thread(AddressOf read)
Dim que As New ConcurrentQueue(Of Bitmap), finished As Boolean


Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load

End Sub

Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
'Start button

writeth.Start()
readth.Start()
End Sub

Sub draw(ByRef pic As Bitmap)
If PictureBox1.Image IsNot Nothing Then
PictureBox1.Image.Dispose()
PictureBox1.Image = Nothing
End If

PictureBox1.Image = pic
End Sub

Sub read()
Dim bit As Bitmap
While (Not finished Or Not que.IsEmpty)
If que.TryDequeue(bit) Then
draw(bit.Clone)

'Still working after the writing stopped
If finished Then Debug.Print("picture:" & que.Count)

Thread.Sleep(2000) 'Simulates the slow-down of the server
End If
End While
End Sub

Sub write()
Dim count As Integer = 0
Dim crop_bit As New Bitmap(320, 240), bit As Bitmap
Dim g As Graphics = Graphics.FromImage(crop_bit)

For Each fil As String In Directory.GetFiles(Application.StartupPath & "/pictures")
count += 1
Debug.Print(count)

bit = Image.FromFile(fil)
g.DrawImage(bit, 0, 0, 320, 240)

que.Enqueue(crop_bit)
bit.Dispose()
Next
finished = True
'At this point the picture box freezes but the reading loop still works
End Sub
End Class


There's no error. I think there might be copies in the queue(because the picture box appears to freeze)? I tried the same code with integers and it works perfectly. What's the problem?

Answer

First, turn on Option Strict. Second, you should not access UI controls from another thread. The core problem is that you aren't really putting 300+ different images in the que. Rather, the code redraws the next image to the same Bitmap object over and over. You are also using a potentially stale graphics object.

Some other things might be artifacts of trying to get it to work, but there is no reason to clone the image for display - it just results in one more thing to dispose of.

This is using the same crop_bit image over and over.

Sub write()
    Dim count As Integer = 0
    Dim crop_bit As New Bitmap(320, 240), bit As Bitmap
    Dim g As Graphics = Graphics.FromImage(crop_bit)
    ...
    que.Enqueue(crop_bit)   

Using the same crop_bit means that by time the Read method processes que(4) it might have been changed to image 5; then 6; then 7 by the Write method. With a short delay, I could get "Object is in use elsewhere" exceptions.

A change to the debug reporting makes it a bit clearer what is going on:

' in "read"
Console.WriteLine("tag {0:00} as # {1:00}", 
        bit.Tag.ToString, rCount)

tag is the number assigned to it when it went into the queue, rCount is it's "Dequeue count" or what position it was in the queue:

tag 13 as # 04
tag 16 as # 05
tag 20 as # 06
tag 24 as # 07
tag 28 as # 08

The second number is correct, but you can see that the 14th and 15th image objects were overwritten by image 16. When the writer finishes, you are left with many copies of the last image loaded.


Fixed with the tag used to mark the item index and reporting done in the Reader method - when they come out:

' for picture box display
Private DisplayImg As Action(Of Bitmap)
...
' initialize when you start the work:
DisplayImg = AddressOf Display

Sub Reader()
    Dim bit As Bitmap = Nothing
    Do
        If que.TryDequeue(bit) Then
            ' do not acccess the UI from a different thread
            ' we know we are on a diff thread, just Invoke
            pbImg.Invoke(DisplayImg, bit)

            ' report on the item
            Console.WriteLine(bit.Tag.ToString)
            Thread.Sleep(100) 'Simulates the slow-down of the server
        End If
    Loop Until (finished AndAlso que.IsEmpty)
End Sub

Sub Writer()
    Dim count As Integer = 0
    Dim crop_bit As Bitmap

    ' enumerate files is more efficient - loads one at a time
    For Each fil As String In Directory.EnumerateFiles(filepath, "*.jpg")
        count += 1
        ' need a NEW bitmap for each file
        crop_bit = New Bitmap(320, 240)

        ' need to use and dispose of NEW graphics for each
        '  use a NEW img from file and dispose of it
        Using g As Graphics = Graphics.FromImage(crop_bit),
             img = Image.FromFile(fil)
            g.DrawImage(img, 0, 0, 320, 240)
        End Using
        ' put a collar on them
        crop_bit.Tag = count.ToString
        que.Enqueue(crop_bit)
    Next
    finished = True
End Sub

Sub Display(pic As Bitmap)
   '... the same,
   ' handles the display AND disposal
   ...
End Sub

I ran some 2000+ thru as a test and didn't see GDI Object change at all, so it doesn't seem to leak.