只有当可选的主线程任务和工作线程完成时,我如何保证代码的执行?

Posted

技术标签:

【中文标题】只有当可选的主线程任务和工作线程完成时,我如何保证代码的执行?【英文标题】:How do I guarantee execution of code only if and when optional main thread task and worker threads are finished? 【发布时间】:2021-06-22 21:53:09 【问题描述】:

背景:

我正在开发一个应用程序,它处理另一个应用程序的大量插件。一个如果它的主要用途是安全地修改具有较少记录的文件中的文件记录,以便它们可以被视为一个文件(几乎就像它将文件组合成一组记录一样。为了安全地做到这一点,它会跟踪重要的有关这些文件和对其所做更改的信息,以便在它们未按预期工作时撤消这些更改。

当我的应用程序启动时,它会分析这些文件并将基本属性保存在缓存中(以减少加载时间)。如果缓存中缺少文件,则检索最重要的内容,然后后台工作人员必须处理该文件以获取更多信息。如果先前修改过的文件已更新为文件的新版本,则 UI 必须与用户确认这一点并删除其修改数据。所有这些信息,包括有关其修改的信息都存储在缓存中。

我的问题:

我的问题是这些进程都不能保证运行(确认窗口或后台文件处理器)。如果其中任何一个运行,则缓存必须由主线程更新。我对工作线程以及哪个线程运行 BackgroundWorker.RunWorkerCompleted 事件处理程序了解不够,以便有效地决定如何确保缓存更新程序在一个(或两个)进程完成后运行。

总而言之:如果任一进程运行,它们都必须完成并(可能)等待另一个进程完成,然后再运行缓存更新代码。我该怎么做?

ADJUNCT INFO(我目前的干预似乎效果不佳):

我在 RunWorkerCompleted 处理程序中有一行等待表单引用为空,然后再继续并退出,但这可能是一个错误,因为它有时会锁定我的程序。

SpinWait.SpinUntil(() => overwriteForm == null);

我没有包含更多代码,因为我预计这更像是一个概念问题,而不是代码问题。不过,如果有需要,我可以提供代码,如果有帮助的话。

【问题讨论】:

你可能想看看Task-based asynchronous programming。 Task 是比 Thread 更好的后台工作抽象,因为它可以(a)同步和异步等待,还可以存储执行结果,或者(如果执行失败)异常发生了。 【参考方案1】:

我认为CountDownTask是你需要的

using System;
using System.Threading;

public class Program


    public class AtomicInteger
    
        protected int value = 0;

        public AtomicInteger(int value)
        
            this.value = value;
        

        public int DecrementAndGet()
        
            int answer = Interlocked.Decrement(ref value);
            return answer;
        
    

    public interface Runnable
    
        void Run();
    

    public class CountDownTask
    
        private AtomicInteger count;
        private Runnable task;
        private Object lk = new Object();
        private volatile bool runnable;
        private bool cancelled;

        public CountDownTask(Int32 count, Runnable task)
        
            this.count = new AtomicInteger(count);
            this.task = task;
            this.runnable = false;
            this.cancelled = false;
        

        public void CountDown()
        
            if (count.DecrementAndGet() == 0)
            
                lock (lk)
                
                    runnable = true;
                    Monitor.Pulse(lk);
                
            
        

        public void Await()
        
            lock (lk)
            
                while (!runnable)
                
                    Monitor.Wait(lk);
                
                if (cancelled)
                
                    Console.WriteLine("Sorry! I was cancelled");
                
                else 
                    task.Run();
                
            
        

        public void Cancel()
        
            lock (lk)
            
                runnable = true;
                cancelled = true;
                Monitor.Pulse(lk);
            
        
    

    public class HelloWorldTask : Runnable
    
        public void Run()
        
            Console.WriteLine("Hello World, I'm last one");
        
    

    public static void Main()
    
        Thread.CurrentThread.Name = "Main";
        Console.WriteLine("Current Thread: " + Thread.CurrentThread.Name);
        CountDownTask countDownTask = new CountDownTask(3, new HelloWorldTask());
        Thread worker1 = new Thread(() => 
            Console.WriteLine("Worker 1 run");
            countDownTask.CountDown();
        );
        Thread worker2 = new Thread(() => 
            Console.WriteLine("Worker 2 run");
            countDownTask.CountDown();
        );
        Thread lastThread = new Thread(() => countDownTask.Await());
        lastThread.Start();
        worker1.Start();
        worker2.Start();
        //countDownTask.Cancel();
        Console.WriteLine("Main Thread Run");
        countDownTask.CountDown();
        Thread.Sleep(1000);
    

让我解释一下(但你可以参考Java CountDownLatch)

1.为了确保一个任务必须在另一个任务之后运行,我们需要创建一个Wait函数来等待它们完成,所以我使用了
while(!runnable) 
    Monitor.Wait(lk);

2.当有任务完成时,我们需要倒计时,如果倒计时到零(这意味着所有的任务都完成了)我们需要通知阻塞线程唤醒并处理任务
if(count.decrementAndGet() == 0) 
    lock(lk) 
        runnable = true;
        Monitor.Pulse(lk);
    

让我们了解更多关于volatile的信息,谢谢

【讨论】:

能否请您对您提出的解决方案也提供一些解释? 我知道避免无休止的“CountDownTask”的一种方法是在没有必要时不启动它。但是,是否可以取消 CountDownTask? 这并不能真正解决问题。原因是因为任务的创建是由运行在不同线程上的两个单独的代码部分完成的。使用您的解决方案,所有任务都可以提前知道。如果一个线程在另一个线程有机会运行之前完全完成而不需要更新怎么办?或者如果它运行并且确实需要更新缓存然后另一个也需要更新缓存怎么办?但是,您的方法给了我一个想法。 可以,可以取消倒计时任务,不好意思,我现在很忙,明天更新代码。 我在想:如果一个线程在另一个线程有机会运行之前完全完成而不需要更新怎么办?我想你总是知道你需要先运行多少个任务,所以你可以用它来输入倒计时任务【参考方案2】:

虽然 dung ta van 的“CountDownTask”答案不是我所需要的,但它极大地启发了下面的解决方案(有关更多信息,请参阅它)。基本上我所做的只是添加了一些额外的功能,最重要的是:让每个任务对结果“投票”(真或假)。谢谢粪大面包车!

公平地说,dung ta van 的解决方案确实可以保证执行,但事实证明这并不是我所需要的。我的解决方案增加了使执行有条件的能力。

这是我的有效解决方案:

public enum PendingBool

    Unknown = -1,
    False,
    True

public interface IRunnableTask

    void Run();


public class AtomicInteger

    int integer;
    public int Value  get  return integer;  
    public AtomicInteger(int value)  integer = value; 
    public int Decrement()  return Interlocked.Decrement(ref integer); 
    public static implicit operator int(AtomicInteger ai)  return ai.integer; 


public class TaskElectionEventArgs

    public bool VoteResult  get; private set; 
    public TaskElectionEventArgs(bool vote)  VoteResult = vote; 


public delegate void VoteEventHandler(object sender, TaskElectionEventArgs e); 

public class SingleVoteTask

    private AtomicInteger votesLeft;
    private IRunnableTask task;
    private volatile bool runTask = false;
    private object _lock = new object();

    public event VoteEventHandler VoteCast;
    public event VoteEventHandler TaskCompleted;

    public bool IsWaiting  get  return votesLeft.Value > 0;  
    
    public PendingBool Result 
    
        get
        
            if (votesLeft > 0)
                return PendingBool.Unknown;
            else if (runTask)
                return PendingBool.True;
            else
                return PendingBool.False;
        
    

    public SingleVoteTask(int numberOfVotes, IRunnableTask taskToRun) 
    
        votesLeft = new AtomicInteger(numberOfVotes);
        task = taskToRun; 
    

    public void CastVote(bool vote)
    
        votesLeft.Decrement();
        runTask |= vote;
        VoteCast?.Invoke(this, new TaskElectionEventArgs(vote));
        if (votesLeft == 0)
            lock (_lock)
            
                Monitor.Pulse(_lock);
            
    

    public void Await()
    
        lock(_lock)
        
            while (votesLeft > 0)
                Monitor.Wait(_lock);
            if (runTask)
                task.Run();
            TaskCompleted?.Invoke(this, new TaskElectionEventArgs(runTask));
        
    

实现上述解决方案就像在 UI 线程中创建 SingleVoteTask 一样简单,然后让影响结果的每个线程投票。

【讨论】:

以上是关于只有当可选的主线程任务和工作线程完成时,我如何保证代码的执行?的主要内容,如果未能解决你的问题,请参考以下文章

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

主线程和worker分离

线程池怎么保证线程按次序执行任务?

Python之多任务编程线程

JAVA 线程池 其中一个线程执行失败 则线程重新执行或者重新提交任务 急

多任务-线程