Search code examples
c#multithreadingcomthread-safetytask

How to not block main UI thread from a worker thread while working with a large global object on the main thread


I'm working with an external library for a card scanner which to keep simple, I'll refer to as ScanLib. The scanning process is long, and of course, it tends to block the UI thread while calling methods like ScanLib.Scan(), thus I figured this would be a great time to use Tasks. Here the simplified (or TLDR) code for context(Full code below for more details, but for time saving's sake, here it is summarized):

public partial class MainForm : Form () 
{
//Here I initialize the reference variable which I will use to work with the Scanner library
ScanLib scanLibraryReference = new ScanLib();

   // Form constructor
   public Form()
   {
      //This initializes the scanner and it's library components, it runs until the program closes
      Task scannerInitTask = Task.Run(() => scanLibraryReference.InitializeScanLibrary())
      InitializeComponent();
   }

   private void Button_Click(object sender, EventArgs e) {
      Task scannerTask = Task.Run(() => scanLibraryReference.ScanCard()); 
   }
}

The problem is, that when I try to scan a card, it freezes the main UI thread, even tough I run both the ScanLib.InitializeScanLib() and ScanLib.ScanCard() methods inside other tasks to try not block the main UI thread since the latter is time consuming. I have read up on reasons for main UI thread blocking, and I think it might be one of 2 things:

  1. I'm using the global declared scanLibraryReference variable to use the library, and even tough I use it in a task, it might block the main UI thread while using it since the variable is declared on it.
  2. All ScanLib methods, according to documentation can throw many errors which come in numbers (error 1001,1002,1003 etc.), and to simplify error logging the documentation asked me to declare about +100 constants similar to: public const int SCANLIB_ERR_SCANFAILED = 1001;. All these "error constants" are declared in another file of type public partial class of MainForm, so maybe usage of these constants from another task (thread) might freeze the main UI thread

Those are my main suspects, but having said that, you would think I would have solved it, but I haven't, and that's where my question lies: I will constantly need a ScanLib reference throughout the program's duration, but if I create it on the main UI thread, it gets blocked. I could try to create a new task like: Task backgroundWorker = Task.Run(() => { ScanLib scanLibRef = new ScanLib(); scanLibRef.InitializeLibrary() } ); But it is my understanding this variable will now live on this thread, and can't be used from another, or can it? Or even If I create a simple thread to just house the variable, that thread will die once it is done declaring a variable. I've tought about just doing this with Thread functions, but then a problem comes of how to call it back into action when I press a button, and feed it a function to run the scanner. Can anyone suggest a solution on how can I declare a global variable I need to constantly use without blocking the main UI thread?

Full code requested (Sorry if it's long, all the Console.WriteLines are used for debugging)

    using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using ScannerTest;
using System.Threading;

namespace ScannerTest
{
    public partial class Form1 : Form
    {
        // All 4 variables below are global and used extensively to call scanner methods
        // Main Scan basic functions Library
        NetScanW.SLibEx scanSlibxEx = new NetScanW.SLibEx();
        // Main Scan extended functios Library
        NetScanW.CImage scanCImage = new NetScanW.CImage();
        // Main Scan OCR functions library
        NetScanW.IdData scanIdData = new NetScanW.IdData();
        // Main Scan extended functions 2 library
        NetScanWex.SLibEx scanWexSlibxEx = new NetScanWex.SLibEx();
        string ImageSource = @"C:\Scans\";

        public Form1()
        {
            Task initTask = Task.Run(() => InitScanLibraries());
            InitializeComponent();
            Console.WriteLine("\nForm initialized succesfully.");
        }

        #region Button events

        // The normal way of scanning which of course blocks the main UI thread
        private void ScanCardNonAsync_Click(object sender, EventArgs e)
        {
            Console.WriteLine("");
            Console.WriteLine("*********************");
            Console.WriteLine("Starting new scan...");
            string currentScan = ImageSource + "MyScan.bmp";
            string modifiedScan = ImageSource + "MyEditedScan.bmp";
            ScanCard(currentScan, modifiedScan);
            OCRscan();
            GetOCRData();
            Console.WriteLine("Scan finalized..");
            Console.WriteLine("*********************");
        }

        // My attempt at scanning asynchronously which still blocks the UI thread
        private void ScanCardAsync_Click(object sender, EventArgs e)
        {
            Console.WriteLine("");
            Console.WriteLine("*********************");
            Console.WriteLine("Starting new scan...");
            string currentScan = ImageSource + "MyScan.bmp";
            string modifiedScan = ImageSource + "MyEditedScan.bmp";
            // Here I chain the methods in a task chain to scan a Card
            Task ScannerStart = new Task(() => ScanCard(currentScan, modifiedScan));
            Task ScannerStep2 = ScannerStart.ContinueWith((x) => OCRscan());
            Task ScannerStep3 = ScannerStep2.ContinueWith((y) => GetOCRData());
            ScannerStart.Start();
        }

        #endregion

        #region Methods

        private void InitScanLibraries()
        {
            switch (scanSlibxEx.InitLibrary("49B2MFWE8WUJXLBW"))
            {
                case SLIB_ERR_SCANNER_BUSSY:
                    System.Console.WriteLine("ERROR: Scanner Busy...");
                    break;
                case LICENSE_VALID:
                    System.Console.WriteLine("");
                    System.Console.WriteLine("**********************************");
                    System.Console.WriteLine("SlibxEx initialized succesfully...");
                    break;
                case LICENSE_INVALID:
                    System.Console.WriteLine("ERROR: License Invalid");
                    break;
                case LICENSE_EXPIRED:
                    System.Console.WriteLine("ERROR: License Expired");
                    break;
                case SLIB_ERR_DRIVER_NOT_FOUND:
                    System.Console.WriteLine("ERROR: Driver not found");
                    break;
                case SLIB_ERR_SCANNER_NOT_FOUND:
                    System.Console.WriteLine("ERROR: Scanner not found");
                    break;
            }

            switch (scanIdData.InitLibrary("49B2MFWE8WUJXLBW"))
            {
                case SLIB_ERR_SCANNER_BUSSY:
                    System.Console.WriteLine("ERROR: Scanner Busy...");
                    break;
                case LICENSE_VALID:
                    System.Console.WriteLine("License validation succesful...");
                    break;
                case LICENSE_INVALID:
                    System.Console.WriteLine("ERROR: License Invalid");
                    break;
                case LICENSE_EXPIRED:
                    System.Console.WriteLine("ERROR: License Expired");
                    break;
                case SLIB_ERR_DRIVER_NOT_FOUND:
                    System.Console.WriteLine("ERROR: Driver not found");
                    break;
                case SLIB_ERR_SCANNER_NOT_FOUND:
                    System.Console.WriteLine("ERROR: Scanner not found");
                    break;
                case GENERAL_ERR_PLUG_NOT_FOUND:
                    System.Console.WriteLine("ERROR: Attatched scanner is not one of the following:\n ScanShell 600 \n ScanShell 800 \n ScanShell1000");
                    break;
                case SLIB_LIBRARY_ALREADY_INITIALIZED:
                    System.Console.WriteLine("ERROR: Call ignored, library already initialized");
                    break;
            }

        }

        private void ScanCard(string ImagePath, string ModifiedImagePath)
        {
            Console.WriteLine("Attempting scan...");

            switch (scanSlibxEx.ScanToFile(ImagePath))
            {
                case SLIB_ERR_NONE:
                    Console.WriteLine("Scan succesful...");
                    break;
                case SLIB_ERR_SCANNER_BUSSY:
                    Console.WriteLine("ERROR: Scanner is busy...");
                    break;
                case LICENSE_INVALID:
                    Console.WriteLine("ERROR: License invalid");
                    break;
                case SLIB_ERR_SCANNER_NOT_FOUND:
                    Console.WriteLine("ERROR: Scanner not found");
                    break;
                case SLIB_ERR_SCANNER_GENERAL_FAIL:
                    Console.WriteLine("ERROR: Scanner general fail");
                    break;
                case SLIB_ERR_HARDWARE_ERROR:
                    Console.WriteLine("ERROR: Hardware error");
                    break;
                case SLIB_ERR_PAPER_FED_ERROR:
                    Console.WriteLine("ERROR: Paper fed error");
                    break;
                case SLIB_ERR_SCANABORT:
                    Console.WriteLine("ERROR: Scan aborted");
                    break;
                case SLIB_ERR_NO_PAPER:
                    Console.WriteLine("ERROR: No paper");
                    break;
                case SLIB_ERR_PAPER_JAM:
                    Console.WriteLine("ERROR: Paper jammed");
                    break;
                case SLIB_ERR_FILE_IO_ERROR:
                    Console.WriteLine("ERROR: File I/O error");
                    break;
                case SLIB_ERR_PRINTER_PORT_USED:
                    Console.WriteLine("ERROR: Printer port used");
                    break;
                case SLIB_ERR_OUT_OF_MEMORY:
                    Console.WriteLine("ERROR: Out of memory");
                    break;
            }
            //scanCImage.RotateImage(ImageSource, 90, 1, ModifiedImagePath);

        }

        private void OCRscan()
        {
            Console.WriteLine("Attempting OCR extraction...");
            string data = "";
            int region = scanIdData.AutoDetectState(data);
            // Check for card region
            switch (region)
            {
                case ID_ERR_USA_TEMPLATES_NOT_FOUND:
                    Console.WriteLine("ERROR: No USA templates found");
                    break;
                case INVALID_INTERNAL_IMAGE:
                    Console.WriteLine("ERROR: No internal image loaded");
                    break;
                case ID_ERR_STATE_NOT_SUPORTED:
                    Console.WriteLine("ERROR: State not supported");
                    break;
                case ID_ERR_STATE_NOT_RECOGNIZED:
                    Console.WriteLine("ERROR: State not recognized");
                    break;
                default:
                    Console.WriteLine("Region catch succesful");
                    break;
            }

            // Begin OCR extraction
            string data2 = "";
            Console.WriteLine("Attempting data extraction...");
            switch (scanIdData.ProcState(data2, region))
            {
                case ID_TRUE:
                    Console.WriteLine("Data extraction succesful.");
                    break;
                case LICENSE_INVALID:
                    Console.WriteLine("ERROR: LICENSE_INVALID");
                    break;
                case SLIB_ERR_SCANNER_NOT_FOUND:
                    Console.WriteLine("ERROR: SLIB_ERR_SCANNER_NOT_FOUND. ");
                    break;
                case SLIB_ERR_INVALID_SCANNER:
                    Console.WriteLine("ERROR: SLIB_ERR_INVALID_SCANNER. ");
                    break;
                case ID_ERR_STATE_NOT_SUPORTED:
                    Console.WriteLine("ERROR: ID_ERR_STATE_NOT_SUPORTED. ");
                    break;
                case INVALID_INTERNAL_IMAGE:
                    Console.WriteLine("ERROR: INVALID_INTERNAL_IMAGE. ");
                    break;
                default:
                    Console.WriteLine("ERROR: Uncatched exception in Form1.OCRScan()");
                    break;
            }
            // Data copying to local
            Console.WriteLine("Copying data locally...");
            if (scanIdData.RefreshData() != 0) Console.WriteLine("Data copied succesfully."); else Console.WriteLine("ERROR: Problem while copying data");
        }

        private void GetOCRData()
        {
            //loc* Variables are locally declared global variables, while the scanIdData.* are library variables where OCR scan results are saved
            Console.WriteLine("Saving data locally...");
            locName = scanIdData.Name;
            locNameFirst = scanIdData.NameFirst;
            locNameMiddle = scanIdData.NameMiddle;
            locNameLast = scanIdData.NameLast;
            locNameSuffix = scanIdData.NameSuffix;
            locID = scanIdData.Id;
            locLicense = scanIdData.license;
            locIssueDate = scanIdData.IssueDate;
            locAddress = scanIdData.Address;
            locExperationDate = scanIdData.ExpirationDate;
            locCSC = scanIdData.CSC;
            locCity = scanIdData.City;
            locEyes = scanIdData.Eyes;
            locDup_Test = scanIdData.Dup_Test;
            locState = scanIdData.State;
            locHair = scanIdData.Hair;
            locEndorsements = scanIdData.Endorsements;
            locZip = scanIdData.Zip;
            locHeight = scanIdData.Height;
            locFee = scanIdData.Fee;
            locCounty = scanIdData.County;
            locClass = scanIdData.Class;
            locRestriction = scanIdData.Restriction;
            locDateOfBirth = scanIdData.DateOfBirth;
            locSex = scanIdData.Sex;
            locSigNum = scanIdData.SigNum;
            locType = scanIdData.Type;
            locWeight = scanIdData.Weight;
            locAddress2 = scanIdData.Address2;
            locAddress3 = scanIdData.Address3;
            locAddress4 = scanIdData.Address4;
            locAddress5 = scanIdData.Address5;
            locText1 = scanIdData.Text1;
            locText2 = scanIdData.Text2;
            locText3 = scanIdData.Text3;
            Console.WriteLine("Data saved succesfully.");
        }

        #endregion
    }
}

EDIT

I did what Onur suggested and yes, what's blocking the main UI thread is the global variables scan Lib. I ran the following code and it didn't freeze the main UI thread:

Task debugTask = Task.Run(() =>
        {
            // All 4 variables below are global and used extensively to call scanner methods
            // Main Scan basic functions Library
            NetScanW.SLibEx scanSlibxEx = new NetScanW.SLibEx();
            // Main Scan extended functios Library
            NetScanW.CImage scanCImage = new NetScanW.CImage();
            // Main Scan OCR functions library
            NetScanW.IdData scanIdData = new NetScanW.IdData();
            // Main Scan extended functions 2 library
            NetScanWex.SLibEx scanWexSlibxEx = new NetScanWex.SLibEx();
            string ImageSource = @"C:\Scans\";
            string currentScan = ImageSource + "MyScan.bmp";
            string modifiedScan = ImageSource + "MyEditedScan.bmp";
            InitScanLibraries(scanSlibxEx, scanIdData);
            ScanCard(currentScan, modifiedScan, scanSlibxEx);
        });

Yes, it's very, very messy, but it worked and didn't freeze. All I did was declare the global variables, initialize the libraries AND run the scan in the same thread and of course, it didn't freeze the main UI thread, but it's far from what I want. I need the libraries to stay initialized, running in a secondary thread, and when I need to scan something, let it call the ScanLib method from the ScanLib reference variable which I'm still stumped since I don't know where to put it so it doesn't block the main UI thread. I will try the answer below from Onur and see what happens.

FINAL EDIT

Just to finalize on my question, I would like to add the solved code in case anybody else needs it. Accordingly to Orun's answer, I, instead of declaring the global variables at the top like ScanLib refScanLib = new ScanLib(), I declared them as null objects like so: ScanLib refScanLib = null, and in the Form constructor, I added a new method called InitializeVariables() that does the following:

public void InitializeVariables()
    {
        NetScanW.SLibEx scanSLibExx = null;
        NetScanW.IdData scanIdDataa = null;
        NetScanW.CImage scanCImagee = null;
        NetScanWex.SLibEx scanWexSLibExx = null;
        var th = new Thread(() => 
        {
            scanSLibExx = new NetScanW.SLibEx();
            scanIdDataa = new NetScanW.IdData();
            scanCImagee = new NetScanW.CImage();
            scanWexSLibExx = new NetScanWex.SLibEx();
        });
        th.SetApartmentState(ApartmentState.MTA);
        th.Start();
        th.Join();
        this.scanSlibxEx = scanSLibExx;
        this.scanIdData = scanIdDataa;
        this.scanCImage = scanCImagee;
        this.scanWexSlibxEx = scanWexSLibExx;
    }

After this, everything worked wonderfully. I've yet to understand it fully, but it works, and thank you guys for all the help.


Solution

  • This is the thing that worked for me in a similar case when using a COM library.

    internal static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]            // <= I needed to add this attribute
        private static void Main()
        {
           //...
        }
    }
    
    
    
    
    public partial class MainForm : Form () 
    {    
        // you can call this in the InitializeComponents() for instance
        void someMethodInYourFormIERunningOnTheUIThread()
        {
            ScanLib scanLib  = null;
            var th = new Thread(() =>
            {
                scanLib = new ScanLib();
            });
            th.SetApartmentState(ApartmentState.MTA); // <== this prevented binding the UI thread for some operations
            th.Start();
            th.Join();
            this.scanLibraryReference = scanLib;
        }
        //...
    }