线程/进程阻止未按预期工作

时间:2018-07-16 17:17:33

标签: c# multithreading

以下是我的代码结构的高度简化的演示:

using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static ConcurrentQueue<string> lst = new ConcurrentQueue<string>();
        static bool bExit = false;

        static void Main(string[] args)

    {
        for (int i = 0; i < 1000; i++)
        {
            lst.Enqueue("LINE " + i.ToString());
        }

        Thread threadCurrent = Thread.CurrentThread;

        Task.Run(() =>
        {
            // #1 this block causes the process to exit at about line 556 on my PC

            //while (threadCurrent.IsAlive)
            //{
            //  Thread.Sleep(500);
            //}

            /////

            // #2 this block works as expected and exits after line 999 is printed

            //while (threadCurrent.IsAlive || lst.Count > 0)
            //{
            //  Thread.Sleep(500);
            //}

            /////

            // #3 this block never exits the process

            threadCurrent.Join();

            // forced queue flush would go here

            /////

            bExit = true;
        });

        Thread threadWorker = new Thread(new ThreadStart(Worker));

        threadWorker.SetApartmentState(ApartmentState.STA);
        threadWorker.Start();
    }

    static private void Worker()
    {
        while (true)
        {
            string s;

            if (lst.TryDequeue(out s))
            {
                System.Diagnostics.Debug.WriteLine(s);
            }
            else
            {
                Thread.Sleep(100);
            }

            if (bExit)
                break;
        }
    }
}

}

请参阅Task.Run()部分中的3个块。在调试器中运行它。使用方法1,在我的PC上,该过程大约退出一半。 #2正常工作,#3永久挂起。

这个想法是,主线程将消息发布到队列中,并在工作线程对其进行处理时立即返回。我想确保在队列退出之前,即使主线程已经退出,也要处理队列中的所有消息。方法2完成了此任务,但我只是不喜欢睡眠,但是如果我必须这样做,我将与之共处...:)。

方法1证明主线程实际上正在退出,不是吗?因此,我认为方法3无需睡眠即可完成相同的工作?但是。Join永不返回,为什么?

2 个答案:

答案 0 :(得分:1)

这是一个令人惊讶的行为,但这可以解释。

首先,您必须了解进程的生存期与主线程(aka primary thread)的生存期相关。我以为是Windows的东西,but according to Raymon Chen this is a C thing

无论如何,最重要的是:.NET运行时必须使主线程保持活动状态。

那么当您退出程序的Main方法时会发生什么?根据合同,.NET运行时必须等待未标记为后台的任何线程。它是在RunMainPost方法中执行的,该方法将依次调用WaitForOtherThreads。 这解释了为什么在所有前台线程都完成工作之前在主线程上调用Thread.Join不会成功的原因。

但是,为什么mainThread.IsAlive返回假?

首先,您必须知道IsAlive没有直接绑定到线程。而是调用ThreadIsRunning。如果基础线程未死,则返回true;如果未用TS_ReportDeadTS_Dead进行标记,则该线程将返回true(这是因为运行时将线程保留得更长一些)向托管代码报告已死亡,以进行一些簿记/整理)。

返回WaitForOtherThreads,如果您检查代码,则可以看到以下内容:

if (!OtherThreadsComplete())
{
    TSLockHolder.Release();

    FastInterlockOr((ULONG *) &pCurThread->m_State, Thread::TS_ReportDead);

    DWORD ret = WAIT_OBJECT_0;
    while (CLREventWaitWithTry(&m_TerminationEvent, INFINITE, TRUE, &ret))
    {
    }
    _ASSERTE(ret == WAIT_OBJECT_0);
}

m_TerminationEvent由要死的最后一个线程设置。但是重要的一点是FastInterlockOr((ULONG *) &pCurThread->m_State, Thread::TS_ReportDead);主线程在等待其他线程之前将其自身标记为已死。它说明了为什么IsAlive是错误的。

我通过构建自定义版本的.NET Core并注释掉这一行来证实了这一假设。之后,IsAlive报告为真。

为什么要这样做?我只能猜测,但是我认为这只是为了简化运行时代码。通过将主线程标记为死线程,其他实际上死掉的线程可以检查(在上述簿记代码中)所有其他线程是否都死了,在这种情况下,请设置m_TerminationEvent。这比检查“除了一个线程之外的所有其他线程是否都死了”,它要优雅得多。只是一个猜测而已。

答案 1 :(得分:0)

为什么,OP?

在您的示例中,您将添加到队列,然后然后启动一个线程来处理它。我想指出,这毫无用处;您可以将其添加到队列,然后继续在同一线程上一一处理它们。或者,您可以完全取消队列,并对其进行简单处理而不是对它们进行排队。

也许是吗?

因此,出于我的回答目的,我将假设您想要那个;也许检索要处理的项目的操作很昂贵,并且您希望能够与工作线程并发添加队列,这样它就不必等待队列准备就绪就可以开始。因此,我的示例在添加到队列之前启动了工作线程,从而允许它在队列设置完成之前启动。

使用BlockingCollection的功能

这种处理项目队列的常规方法是使用BlockingCollection,它可以为您处理几乎所有的细节。

您的工作方法可能很简单:

static private void Worker(BlockingCollection<string> queue)
{
    while (queue.Count > 0 || !queue.IsAddingCompleted)  //Exit if there are no items and no more are on the way
    {
        var item = queue.Take();        //Wait until the queue has at least one item, then take it
        Foo(item);                      //Whatever processing you need to do
    }
}

您会这样称呼它:

static public void Main(string[] args)
{
    var queue = new BlockingCollection<string>();

    var task = Task.Run(() => Worker(queue));  //OK to start it first, it'll run in parallel
    for (int i = 0; i < 100; i++)
    {
        var item = "LINE " + i.ToString();
        queue.Add(item);
    }
    queue.CompleteAdding();  //Tell the Worker the queue is complete.
    task.Wait();             //Wait for Worker to finish. 
}

这将设置一个队列

  1. 阻止任何工作Take()直到可用。因此,您无需在添加新项目时发出信号,也无需担心并发性。阻塞是轻量级的;在通知队列之前,操作系统不会安排线程执行。

  2. 允许线程安全添加

  3. 允许主线程在完成添加时发出信号。

  4. 允许工作线程继续处理,直到完成主线程添加为止。

  5. 不使用Join()同步处理。

  6. 使Main线程保持活动状态,但不进行计划,直到Worker完成为止。这避免了担心退出主线程是否会产生不良影响。

  7. Wait()完成任务时捕获任何异常(如果使用线程,则未处理的异常会炸毁整个事情)

请参见complete working example on DotNetFiddle。我添加了延迟和日志记录语句,以便您可以看到线程如何交互。括号内的数字是线程ID。

示例输出:

2018-07-17 01:45:40.785 [150] {Main} Adding item 'LINE 0'
2018-07-17 01:45:40.785 [2C1] {Worker} Processing item 'LINE 0'
2018-07-17 01:45:40.832 [150] {Main} Adding item 'LINE 1'
2018-07-17 01:45:40.863 [150] {Main} Adding item 'LINE 2'
2018-07-17 01:45:40.894 [150] {Main} Adding item 'LINE 3'
2018-07-17 01:45:40.894 [2C1] {Worker} Processing item 'LINE 1'
2018-07-17 01:45:40.926 [150] {Main} Adding item 'LINE 4'
2018-07-17 01:45:40.957 [150] {Main} Adding item 'LINE 5'
2018-07-17 01:45:40.988 [150] {Main} Adding item 'LINE 6'
2018-07-17 01:45:41.004 [2C1] {Worker} Processing item 'LINE 2'
2018-07-17 01:45:41.019 [150] {Main} Adding item 'LINE 7'
2018-07-17 01:45:41.051 [150] {Main} Adding item 'LINE 8'
2018-07-17 01:45:41.082 [150] {Main} Adding item 'LINE 9'
2018-07-17 01:45:41.113 [2C1] {Worker} Processing item 'LINE 3'
2018-07-17 01:45:41.113 [150] {Main} Adding item 'LINE 10'
2018-07-17 01:45:41.144 [150] {Main} Adding item 'LINE 11'
2018-07-17 01:45:41.176 [150] {Main} Adding item 'LINE 12'
(truncated)

另请参阅producer-consumer pattern