你将如何在.NET中实现这个“WorkerChain”功能?

时间:2010-04-24 16:39:44

标签: .net multithreading synchronization

编辑:我觉得太晚了(?)我在第一次更新这个问题时发布的所有代码对于大多数读者而言太多了 。对于任何关心阅读它的人来说,我实际上都是written a blog post about this topic

与此同时,我已经离开了原来的问题,简要介绍了我想解决的问题。

我还要注意到,到目前为止,我发布的代码(在我的博客上)已经很好地进行了测试。但我仍然对任何人都愿意给我的反馈感兴趣,这些反馈是关于它的干净/稳健/高效。

* 我喜欢这个词doesn't really mean what we think,但我们开发人员一直都在使用它。


原始问题

对于模糊的问题标题感到抱歉 - 不确定如何在我的下面简洁地封装我要求的内容。 (如果拥有编辑权限的人可以想到更具描述性的标题,请随时更改它。)

我需要的行为就是这个。我正在设想一个在其构造函数中接受单个委托任务的工作类(为简单起见,我会使其不可变 - 在实例化后不能再添加任务)。我将此任务称为T。该类应该有一个简单的方法,如GetToWork,它将表现出这种行为:

  1. 如果工作人员当前正在运行T,那么它现在就会开始这样做。
  2. 如果工作人员 当前正在运行T,那么一旦完成,它将立即再次启动T
  3. 当工人正在运行时,
  4. GetToWork可以被调用T;简单的规则是,在执行T期间,如果GetToWork至少被调用 T将在完成时再次运行(然后{在GetToWork正在运行 时间时会调用{1}},它会再次重复,等等。)
  5. 现在,使用布尔开关非常简单。但是这个类需要线程安全,我的意思是,上面的步骤1和2需要包含原子操作(至少我认为它们是这样)。

    增加了一层复杂性。我需要一个“工人链”类,其中包括许多连接在一起的工人。第一个工作人员完成后,它基本上会在之后调用上的T;同时,如果调用了自己的GetToWork,它也会自动重启。在上逻辑调用GetToWork与在链中的第一个worker 上调用GetToWork基本相同(我完全打算链的工人不能公开访问。)

    想象这个假设的“工人链”如何表现的一种方法是将其与接力赛中的球队进行比较。假设有四个参与者,GetToWorkW1,并将该链称为W4。如果我致电C,会发生什么:

    1. 如果C.StartWork()处于他的起点(即什么也不做),他将开始向W1跑去。
    2. 如果W2已经已经W1开始(即执行他的任务),那么一旦他到达W2,他就会向{{1}发出信号开始之前,立即回到他的起点,并且自调用W2后,再次开始向W2跑去。
    3. StartWork达到W2的起点时,他会立即回到自己的起点。
      1. 如果W1只是坐在那里,他会立即开始向W2开始。
      2. 如果W2已经偏离W3,那么W2只有在到达W3后才会再次出现并返回到他的起点。
    4. 以上内容可能有点复杂,写得不好。但希望你能得到基本的想法。显然,这些工人将依靠自己的线程运行。

      另外,我猜这个功能可能已存在于某个地方?如果是这样,绝对让我知道!

3 个答案:

答案 0 :(得分:1)

使用信号量。每个worker都是一个包含以下代码的线程(伪代码):

WHILE(TRUE)
    WAIT_FOR_SEMAPHORE(WORKER_ID) //The semaphore for the current worker
    RESET_SEMAPHORE(WORKER_ID)
    /* DO WORK */
    POST_SEMAPHORE(NEXT_WORKER_ID) //The semaphore for the next worker
END

非零信号量意味着有人发信号通知当前线程完成工作。在其输入行中获得非零信号量后,它重置信号量(标记为无信号),完成工作(同时信号量可以再次发布)并发布信号量以供下一个工作人员使用。这个故事在下一个工人中重演。

答案 1 :(得分:1)

一个天真的实施,你可能会得到一些里程。

注意:

我的理解是标量类型,r.e。 bool标志控制执行,具有原子赋值,使它们像在这种情况下需要/想要的那样是线程安全的。

有更复杂的可能性涉及信号量和其他策略,但如果简单有效......

using System;
using System.Threading;

namespace FlaggedWorkerChain
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            FlaggedChainedWorker innerWorker = new FlaggedChainedWorker("inner", () => Thread.Sleep(1000), null);
            FlaggedChainedWorker outerWorker = new FlaggedChainedWorker("outer", () => Thread.Sleep(500), innerWorker);

            Thread t = new Thread(outerWorker.GetToWork);
            t.Start();

            // flag outer to do work again
            outerWorker.GetToWork();

            Console.WriteLine("press the any key");
            Console.ReadKey();
        }
    }

    public sealed class FlaggedChainedWorker
    {
        private readonly string _id;
        private readonly FlaggedChainedWorker _innerWorker;
        private readonly Action _work;
        private bool _busy;
        private bool _flagged;

        public FlaggedChainedWorker(string id, Action work, FlaggedChainedWorker innerWorker)
        {
            _id = id;
            _work = work;
            _innerWorker = innerWorker;
        }

        public void GetToWork()
        {
            if (_busy)
            {
                _flagged = true;
                return;
            }

            do
            {
                _flagged = false;
                _busy = true;
                Console.WriteLine(String.Format("{0} begin", _id));

                _work.Invoke();

                if (_innerWorker != null)
                {
                    _innerWorker.GetToWork();
                }
                Console.WriteLine(String.Format("{0} end", _id));

                _busy = false;
            } while (_flagged);
        }
    }
}

答案 2 :(得分:1)

在我看来,你过度复杂了。我以前写过这些“管道”课程;您只需要一个工作队列,每个工作队员都有一个等待句柄,在操作完成后会发出信号。

public class Pipeline : IDisposable
{
    private readonly IEnumerable<Stage> stages;

    public Pipeline(IEnumerable<Action> actions)
    {
        if (actions == null)
            throw new ArgumentNullException("actions");
        stages = actions.Select(a => new Stage(a)).ToList();
    }

    public Pipeline(params Action[] actions)
        : this(actions as IEnumerable<Action>)
    {
    }

    public void Dispose()
    {
        foreach (Stage stage in stages)
            stage.Dispose();
    }

    public void Start()
    {
        foreach (Stage currentStage in stages)
            currentStage.Execute();
    }

    class Stage : IDisposable
    {
        private readonly Action action;
        private readonly EventWaitHandle readyEvent;

        public Stage(Action action)
        {
            this.action = action;
            this.readyEvent = new AutoResetEvent(true);
        }

        public void Dispose()
        {
            readyEvent.Close();
        }

        public void Execute()
        {
            readyEvent.WaitOne();
            action();
            readyEvent.Set();
        }
    }
}

这是一个测试程序,您可以使用它来验证操作是否始终以正确的顺序执行,并且只能同时执行同一个操作中的一个:

class Program
{
    static void Main(string[] args)
    {
        Action firstAction = GetTestAction(1);
        Action secondAction = GetTestAction(2);
        Action thirdAction = GetTestAction(3);
        Pipeline pipeline = new Pipeline(firstAction, secondAction, thirdAction);
        for (int i = 0; i < 10; i++)
        {
            ThreadPool.QueueUserWorkItem(s => pipeline.Start());
        }
    }

    static Action GetTestAction(int index)
    {
        return () =>
        {
            Console.WriteLine("Action started: {0}", index);
            Thread.Sleep(100);
            Console.WriteLine("Action finished: {0}", index);
        };
    }
}

简短,简单,完全线程安全。

如果出于某种原因,您需要开始在链中的特定步骤开始工作,那么您只需为Start添加重载:

public void Start(int index)
{
    foreach (Stage currentStage in stages.Skip(index + 1))
        currentStage.Execute();
}

修改

基于评论,我认为内部Stage类的一些小改动应足以获得您想要的行为。我们只需要在“就绪”事件之外添加“排队”事件。

    class Stage : IDisposable
    {
        private readonly Action action;
        private readonly EventWaitHandle readyEvent;
        private readonly EventWaitHandle queuedEvent;

        public Stage(Action action)
        {
            this.action = action;
            this.readyEvent = new AutoResetEvent(true);
            this.queuedEvent = new AutoResetEvent(true);
        }

        public void Dispose()
        {
            readyEvent.Close();
        }

        private bool CanExecute()
        {
            if (readyEvent.WaitOne(0, true))
                return true;
            if (!queuedEvent.WaitOne(0, true))
                return false;
            readyEvent.WaitOne();
            queuedEvent.Set();
            return true;
        }

        public bool Execute()
        {
            if (!CanExecute())
                return false;
            action();
            readyEvent.Set();
            return true;
        }
    }

如果某个阶段无法执行(即已经排队),还要更改管道的Start方法以中断:

public void Start(int index)
{
    foreach (Stage currentStage in stages.Skip(index + 1))
        if (!currentStage.Execute())
            break;
}

这里的概念非常简单,再次:

  • 阶段首先尝试立即获取就绪状态。如果成功,则开始运行。
  • 如果它未能获得就绪状态(即任务已在运行),则它尝试获取排队状态。
    • 如果它处于排队状态,则等待就绪状态变为可用,然后释放排队状态。
    • 如果它也无法获得排队状态,那么它就会放弃。

我已经再次阅读了您的问题和评论,我非常确定这正是您要做的,并且在安全性,吞吐量和限制之间进行了最佳权衡。

由于ThreadPool有时可能需要一段时间才能回复,因此如果你想真正看到“跳过”,你应该将测试程序的延迟提升到1000而不是100发生。