在主线程上处理大型全局对象时如何不阻止来自工作线程的主 UI 线程

Posted

技术标签:

【中文标题】在主线程上处理大型全局对象时如何不阻止来自工作线程的主 UI 线程【英文标题】:How to not block main UI thread from a worker thread while working with a large global object on the main thread 【发布时间】:2014-07-31 16:37:10 【问题描述】:

我正在使用一个用于卡片扫描仪的外部库,为了简单起见,我将其称为 ScanLib。扫描过程很长,当然,在调用 ScanLib.Scan() 之类的方法时,它往往会阻塞 UI 线程,因此我认为这将是使用 Tasks 的好时机。这里是上下文的简化(或 TLDR)代码(下面是完整代码以获取更多详细信息,但为了节省时间,这里对其进行了总结):

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()); 
   

问题是,当我尝试扫描卡片时,它会冻结主 UI 线程,即使我在其他任务中同时运行 ScanLib.InitializeScanLib() 和 ScanLib.ScanCard() 方法以尽量不阻塞主 UI 线程UI 线程,因为后者很耗时。我已经阅读了主 UI 线程阻塞的原因,我认为这可能是两件事之一:

    我正在使用全局声明的 scanLibraryReference 变量来使用该库,即使我在任务中使用它,它也可能会在使用它时阻塞主 UI 线程,因为在其上声明了变量。 根据文档,所有 ScanLib 方法都可能引发许多错误,这些错误以数字形式出现(错误 1001、1002、1003 等),为了简化错误记录,文档要求我声明大约 +100 个常量,类似于:public const int SCANLIB_ERR_SCANFAILED = 1001;。所有这些“错误常量”都在 MainForm 的公共部分类类型的另一个文件中声明,因此从另一个任务(线程)使用这些常量可能会冻结主 UI 线程

这些是我的主要嫌疑人,但话虽如此,你会认为我会解决它,但我没有,这就是我的问题所在: 在整个程序的整个过程中,我将不断需要一个 ScanLib 引用,但如果我在主 UI 线程上创建它,它就会被阻止。我可以尝试创建一个新任务,例如: 任务 backgroundWorker = Task.Run(() => ScanLib scanLibRef = new ScanLib(); scanLibRef.InitializeLibrary() ); 但据我了解,这个变量现在将存在于这个线程上,并且不能从另一个线程中使用,或者可以吗?或者即使我创建一个简单的线程来存放变量,一旦完成声明变量,该线程也会死掉。我一直坚持只使用 Thread 函数来执行此操作,但是当我按下按钮时如何将其恢复为操作,并为其提供运行扫描仪的函数时,问题就来了。谁能提出一个解决方案,说明如何声明一个需要经常使用的全局变量而不阻塞主 UI 线程?

请求完整代码(对不起,如果它很长,所有 Console.WriteLines 都用于调试)

    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
    

编辑

我按照 Onur 的建议做了,是的,阻塞主 UI 线程的是全局变量扫描库。我运行了以下代码,它并没有冻结主 UI 线程:

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);
        );

是的,它非常非常混乱,但它有效并且没有冻结。我所做的只是声明全局变量、初始化库并在同一个线程中运行扫描,当然,它并没有冻结主 UI 线程,但这与我想要的相差甚远。我需要库保持初始化,在辅助线程中运行,当我需要扫描某些东西时,让它从 ScanLib 引用变量中调用 ScanLib 方法,因为我不知道把它放在哪里所以我仍然很难过它不会阻塞主 UI 线程。我将尝试 Onur 的以下答案,看看会发生什么。

最终编辑

为了最终确定我的问题,我想添加已解决的代码以防其他人需要它。根据 Orun 的回答,我没有像 ScanLib refScanLib = new ScanLib() 那样在顶部声明全局变量,而是将它们声明为 null 对象,如下所示:ScanLib refScanLib = null,并且在 Form 构造函数中,我添加了一个新方法调用 InitializeVariables() 执行以下操作:

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;
    

在此之后,一切都很顺利。我还没有完全理解它,但它确实有效,感谢大家的帮助。

【问题讨论】:

全局声明的变量应该不是问题,它们是否在 UI 线程上声明都没关系。我认为我们缺少您的更多代码,因为从这里看起来不错。后台还有什么你没有发布的吗?也许发布你的 ScanCard 方法正在做什么 我尽可能地简化了代码,因为我不想混淆大家。大多数 ScanLib 方法都是隐藏的,所以我所能做的就是向它提供数据。我将在下面发布完整代码以获取更广泛的上下文 在 UI 被阻止时暂停调试器。当前堆栈显示谁在阻止 UI。 知道什么窗口/工具栏会显示这个吗? 曾经我在使用 COM 的库中遇到类似问题。问题是由于scanLibraryReference 是在UI 线程上创建的,与COM 相关的东西以某种方式绑定到了UI 线程。在不同的线程/任务中创建scanLibraryReference 可能会有所帮助。我还必须将[STAThread] 属性添加到Program.Main 方法中。 【参考方案1】:

这是在使用 COM 库时在类似情况下对我有用的东西。

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;
    
    //...

【讨论】:

我按照你之前的建议做了,并用测试结果编辑了我的帖子。是的,你是对的,阻塞 UI 线程的是那些全局变量,尽管将它们全部放在一个新的任务中进行演示,但它仍然让我想知道如何保持它活着以访问这些变量。无论哪种方式,我现在都会尝试这个并希望看看会发生什么,我不知道 STAthread 或 SetApartmentState() 做了什么,但让我们看看我通过示例学到了什么。谢谢。 哇……它成功了……我还是不敢相信。我不知道 SetApartment 或 STAthread 到底做了什么,但如果我要继续使用 COM 对象,我一定会认真阅读。非常感谢您的帮助。 据我所知,他们决定了如何实现同步。

以上是关于在主线程上处理大型全局对象时如何不阻止来自工作线程的主 UI 线程的主要内容,如果未能解决你的问题,请参考以下文章

本机调用阻塞主线程

来自dispatch_async全局崩溃的C函数调用,但在主队列上工作

如何让工作线程在主 UI 线程上执行回调?

在主线程和工作线程中加载动态库(内部调用COM dll)有什么区别?

在主线程上启动对话框等待工作线程的结果

解决定时器在主线程不工作问题