以下是我的代码结构的高度简化的演示:
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永不返回,为什么?
答案 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_ReportDead
或TS_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)
在您的示例中,您将添加到队列,然后然后启动一个线程来处理它。我想指出,这毫无用处;您可以将其添加到队列,然后继续在同一线程上一一处理它们。或者,您可以完全取消队列,并对其进行简单处理而不是对它们进行排队。
因此,出于我的回答目的,我将假设您不想要那个;也许检索要处理的项目的操作很昂贵,并且您希望能够与工作线程并发添加队列,这样它就不必等待队列准备就绪就可以开始。因此,我的示例在添加到队列之前启动了工作线程,从而允许它在队列设置完成之前启动。
这种处理项目队列的常规方法是使用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.
}
这将设置一个队列
阻止任何工作Take()
直到可用。因此,您无需在添加新项目时发出信号,也无需担心并发性。阻塞是轻量级的;在通知队列之前,操作系统不会安排线程执行。
允许线程安全添加
允许主线程在完成添加时发出信号。
允许工作线程继续处理,直到完成主线程添加为止。
不使用Join()
同步处理。
使Main线程保持活动状态,但不进行计划,直到Worker完成为止。这避免了担心退出主线程是否会产生不良影响。
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)