Search code examples
.netvb.netmultithreadingbitmapconcurrent-queue

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?


Solution

  • 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.