确保多个线程之间的执行顺序

时间:2012-07-24 16:20:06

标签: c# multithreading deadlock race-condition

我有一个恼人的问题,主要是因为我在C#多线程方面的技能水平较低/经验不足。

这是背景。在我的框架中,我有一个名为WaitFormHelper的静态类,它有两个静态方法(嗯......实际上更多但我们在这里并不关心),Start()Close() Start()方法初始化并启动一个线程,该线程将获取locker对象上的锁并创建WaitForm(这是一个带有自定义消息和进度条的小型加载控件)< / p>

在我当前的项目中,我有一个方法,它启动WaitForm,执行计算,然后关闭WaitForm。没什么好看的。 该方法如下所示,我尽可能地简化了它:

public void PerformCalculations()
{
   try
   {
      WaitFormHelper.Start("Title", "message", false);
      if (this.CalculationsParameters.IsInvalid)
      {
         return;
      }
      // Perform all those lengthy calculations here
   } 
   // catch whatever exception I may have to catch, we don't care here
   finally
   {
      WaitFormHelper.Close();
   }
}

以下是Start()Close()方法以及相关方法&amp;属性,也简化了:

private static Thread instanceCaller;
private static WaitForm instance;

private static AutoResetEvent waitFormStarted = new AutoResetEvent(false);
private static object locker = new object();

/// <summary>
/// Initializes WaitForm to start a single task
/// </summary>
/// <param name="header">WaitForm header</param>
/// <param name="message">Message displayed</param>
/// <param name="showProgressBar">True if we want a progress bar, else false</param>
public static void Start(string header, string message, bool showProgressBar)
{
    InitializeCallerThread(showProgressBar, header, message);
    instanceCaller.Start();
}

/// <summary>
/// Initializes caller thread for executing a single command
/// </summary>
/// <param name="showProgressBar"></param>
/// <param name="header"></param>
/// <param name="message"></param>
private static void InitializeCallerThread(bool showProgressBar, string header, string message)
{
    waitFormStarted.Reset();

    instanceCaller = new Thread(() =>
    {
        lock (locker)
        {
            instance = new WaitForm()
            {
                Header = header,
                Message = message,
                IsProgressBarVisible = showProgressBar
            };
            waitFormStarted.Set();
        }
        instance.ShowDialog();
    });
    instanceCaller.Name = "WaitForm thread";
    instanceCaller.SetApartmentState(ApartmentState.STA);
    instanceCaller.IsBackground = true;
}

/// <summary>
/// Closes current form
/// </summary>
public static void Close()
{
    lock (locker)
    {
        if (instance != null && !instance.IsClosed)
        {
            waitFormStarted.WaitOne();
            instance.FinalizeWork();
            instance.Dispatcher.Invoke(
                new Action(() =>
                {
                    instance.Close();
                }));
        }
    }
}

现在让我们来解决问题

这通常可以正常工作,除非在这种情况下: 如果this.CalculationsParameters.IsInvalid为真(即,您可能已经理解,我无法出于某种原因执行计算,例如“用户在我的表单中输入垃圾”),执行将直接关闭WaitForm。 但是在这种情况下,主线程将到达Close方法并获取locker对象之前锁定 Start()方法触发的线程。< / p>

会发生什么:Close获取锁定,尝试关闭表单,但instance仍然为空,因为Thread中创建的InitializeCallerThread仍在等待锁定实际创建它。 Close发布了锁定,InitializeCallerThread获取了它,并且...显示了一个不会关闭的WaitForm

现在我知道我可以通过在实际启动WaitForm之前测试计算参数是否无效来解决这个问题,但问题是这个WaitForm应该被我们框架的所有应用程序使用(其中包括在4个国家/地区使用和维护的40多个不同的应用程序),所以理想情况下我更愿意看到WaitForm在所有情况下工作。

您是否知道如何同步这一点以确保首先调用并执行启动程序线程? 正如您所看到的,我已经使用AutoResetEvent来解决此问题,但如果我在测试之前听取它if (instance != null),我将最终陷入困境。

希望这很清楚!谢谢!

2 个答案:

答案 0 :(得分:1)

您需要一些机制来进行“队列控制”,根据您希望它们发生的顺序协调步骤。

最近我需要实现类似这样的操作,在单元测试中强制执行多个线程的特定顺序。

这是我的建议:

<强> QueueSynchronizer:

/// <summary>
/// Synchronizes steps between threads.
/// </summary>
public class QueueSynchronizer
{
    /// <summary>
    /// Constructor.
    /// </summary>
    /// <param name="minWait">Minimum waiting time until the next try.</param>
    /// <param name="maxWait">Maximum waiting time until the next try.</param>
    public QueueSynchronizer(Int32 minWait, Int32 maxWait)
    {
    }

    private Mutex mx = new Mutex();
    /// <summary>
    /// Minimum waiting time until the next try.
    /// </summary>
    private Int32 minWait = 5;
    /// <summary>
    /// Maximum waiting time until the next try.
    /// </summary>
    private Int32 maxWait = 500;

    int currentStep = 1;

    /// <summary>
    /// Key: order in the queue; Value: Time to wait.
    /// </summary>
    private Dictionary<int, int> waitingTimeForNextMap = new Dictionary<int, int>();

    /// <summary>
    /// Synchronizes by the order in the queue. It starts from 1. If is not 
    /// its turn, the thread waits for a moment, after that, it tries again, 
    /// and so on until its turn.
    /// </summary>
    /// <param name="orderInTheQueue">Order in the queue. It starts from 1.</param>
    /// <returns>The <see cref="Mutex"/>The mutex that must be released at the end of turn.
    /// </returns>
    public Mutex Sincronize(int orderInTheQueue)
    {
        do
        {
            //while it is not the turn, the thread will stay in this loop and sleeping for 100, 200, ... 1000 ms
            if (orderInTheQueue != this.currentStep)
            {
                //The next in queue will be waiting here (other threads).
                mx.WaitOne();
                mx.ReleaseMutex();

                //Prevents 100% processing while the current step does not happen
                if (!waitingTimeForNextMap.ContainsKey(orderInTheQueue))
                {
                    waitingTimeForNextMap[orderInTheQueue] = this.minWait;
                }
                Thread.Sleep(waitingTimeForNextMap[orderInTheQueue]);
                waitingTimeForNextMap[orderInTheQueue] = Math.Min(waitingTimeForNextMap[orderInTheQueue] * 2, this.maxWait);
            }
        } while (orderInTheQueue != this.currentStep);

        mx.WaitOne();
        currentStep++;
        return mx;
    }
}

Start()Close()QueueSynchronizer

//synchronizer
private static QueueSynchronizer queueSynchronizer;

private static Thread instanceCaller;
private static WaitForm instance;

private static AutoResetEvent waitFormStarted = new AutoResetEvent(false);
private static object locker = new object();

/// <summary>
/// Initializes WaitForm to start a single task
/// </summary>
/// <param name="header">WaitForm header</param>
/// <param name="message">Message displayed</param>
/// <param name="showProgressBar">True if we want a progress bar, else false</param>
public static void Start(string header, string message, bool showProgressBar)
{
    queueSynchronizer = new QueueSynchronizer();
    InitializeCallerThread(showProgressBar, header, message);
    instanceCaller.Start();
}

/// <summary>
/// Initializes caller thread for executing a single command
/// </summary>
/// <param name="showProgressBar"></param>
/// <param name="header"></param>
/// <param name="message"></param>
private static void InitializeCallerThread(bool showProgressBar, string header, string message)
{
    waitFormStarted.Reset();

    instanceCaller = new Thread(() =>
    {
        lock (locker)
        {
            //Queuing to run on first.
            Mutex mx = queueSynchronizer.Sincronize(1);
            try
            {
                instance = new WaitForm()
                        {
                            Header = header,
                            Message = message,
                            IsProgressBarVisible = showProgressBar
                        };
            }
            finally
            {
                //I think is here that ends the first step!?
                mx.ReleaseMutex();
            }

            waitFormStarted.Set();
        }
        instance.ShowDialog();
    });
    instanceCaller.Name = "WaitForm thread";
    instanceCaller.SetApartmentState(ApartmentState.STA);
    instanceCaller.IsBackground = true;
}

/// <summary>
/// Closes current form
/// </summary>
public static void Close()
{
    //Queuing to run on second.
    Mutex mx = queueSynchronizer.Sincronize(2);
    try
    {
        lock (locker)
        {
            if (instance != null && !instance.IsClosed)
            {
                waitFormStarted.WaitOne();
                instance.FinalizeWork();
                instance.Dispatcher.Invoke(
                    new Action(() =>
                    {
                        instance.Close();
                    }));
            }
        }
    }
    finally
    {
        mx.ReleaseMutex();
    }
}

答案 1 :(得分:0)

您需要加入您创建的主题。通过连接一个线程,你将在那个时阻塞,直到线程完成执行。

public static void Close()
{
    lock (locker)
    {
        instanceCaller.Join();

        if (instance != null && !instance.IsClosed)
        {
            waitFormStarted.WaitOne();
            instance.FinalizeWork();
            instance.Dispatcher.Invoke(
                new Action(() =>
                {
                    instance.Close();
                }));
        }
    }
}