在C#中如何获取正在运行的线程列表?

7

我在C#中创建了动态线程,需要获取这些正在运行的线程的状态。

List<string>[] list;
list = dbConnect.Select();

for (int i = 0; i < list[0].Count; i++)
{
    Thread th = new Thread(() =>{
        sendMessage(list[0]['1']);
        //calling callback function
    });
    th.Name = "SID"+i;
    th.Start();
}

for (int i = 0; i < list[0].Count; i++)
{
    // here how can i get list of running thread here.
}

你如何获取正在运行的线程列表?

你需要你创建的线程列表还是所有线程? - sll
5个回答

39

关于线程

我建议避免自己显式地创建线程。

更好的选择是使用ThreadPool.QueueUserWorkItem如果你使用.Net 4.0,则可以使用更强大的任务并行库,它还允许您以更强大的方式使用线程池线程(值得一看的是Task.Factory.StartNew

如果我们选择显式创建线程会怎样?

假设您的list[0].Count返回1000项。我们还假设您正在高端(在撰写本文时)的16核计算机上执行此操作。直接的影响是我们有1000个线程争夺这些有限的资源(16个内核)。

任务越多且每个任务运行时间越长,就会花费更多的时间进行上下文切换。此外,创建线程是昂贵的。如果使用重用现有线程的方法,则可以避免显式创建每个线程的开销。

因此,虽然多线程的初始意图是为了提高速度,但我们可以看到它可能会产生相反的效果。

如何克服“过度”线程?

这就是ThreadPool发挥作用的地方。

线程池是一组线程,可用于后台执行许多任务。

它们是如何工作的:

一旦池中的线程完成其任务,它将返回到等待线程队列中,以便可以重复使用。此重用使应用程序避免为每个任务创建新线程的成本。

线程池通常具有最大线程数。如果所有线程都忙碌,则额外的任务将被放置在队列中,直到线程变得可用。

因此,通过使用线程池线程,我们可以更有效地利用资源。

  • 为了最大化实际工作量,我们不会使处理器过度饱和,这样可以减少线程之间的切换时间,从而更多地执行线程应该完成的代码。
  • 更快的线程启动:每个线程池线程都是立即可用的,而不是等待新线程被构建。
  • 为了最小化内存消耗,线程池将限制线程数量到线程池大小,并排队任何超出线程池大小限制的请求。(请参阅ThreadPool.GetMaxThreads)。这种设计选择的主要原因,当然是为了避免过多的线程请求过度饱和有限数量的核心,从而保持上下文切换到较低水平。

太多理论,让我们把所有这些理论付诸实践!

好的,在理论上了解这一切很好,但让我们将其付诸实践并查看数字告诉我们的结果。我们将使用一个简化的应用程序的基本版本进行比较,以粗略地指示数量级的差异。我们将对比新线程、线程池和任务并行库(TPL)之间的差异。

新线程

    static void Main(string[] args)
    {
        int itemCount = 1000;

        Stopwatch stopwatch = new Stopwatch(); 
        long initialMemoryFootPrint = GC.GetTotalMemory(true);

        stopwatch.Start();
        for (int i = 0; i < itemCount; i++)
        {
            int iCopy = i;  // You should not use 'i' directly in the thread start as it creates a closure over a changing value which is not thread safe. You should create a copy that will be used for that specific variable.
            Thread thread = new Thread(() =>
            {
                // lets simulate something that takes a while
                int k = 0;
                while (true)
                {
                    if (k++ > 100000)
                        break;
                }

                if ((iCopy + 1) % 200 == 0) // By the way, what does your sendMessage(list[0]['1']); mean? what is this '1'? if it is i you are not thread safe.
                    Console.WriteLine(iCopy + " - Time elapsed: (ms)" + stopwatch.ElapsedMilliseconds);
            });

            thread.Name = "SID" + iCopy; // you can also use i here. 
            thread.Start();
        }

        Console.ReadKey();
        Console.WriteLine(GC.GetTotalMemory(false) - initialMemoryFootPrint);
        Console.ReadKey();
    }

结果:

New Thread Benchmark

ThreadPool.EnqueueUserWorkItem

    static void Main(string[] args)
    {
        int itemCount = 1000;

        Stopwatch stopwatch = new Stopwatch(); 
        long initialMemoryFootPrint = GC.GetTotalMemory(true);

        stopwatch.Start();

        for (int i = 0; i < itemCount; i++)
        {
            int iCopy = i; // You should not use 'i' directly in the thread start as it creates a closure over a changing value which is not thread safe. You should create a copy that will be used for that specific variable.
            ThreadPool.QueueUserWorkItem((w) =>
            {
                // lets simulate something that takes a while
                int k = 0;
                while (true)
                {
                    if (k++ > 100000)
                        break;
                }

                if ((iCopy + 1) % 200 == 0) 
                    Console.WriteLine(iCopy + " - Time elapsed: (ms)" + stopwatch.ElapsedMilliseconds);
            });
        }

        Console.ReadKey();
        Console.WriteLine("Memory usage: " + (GC.GetTotalMemory(false) - initialMemoryFootPrint));
        Console.ReadKey();
    }

结果:

ThreadPool基准测试

任务并行库(TPL)

    static void Main(string[] args)
    {
        int itemCount = 1000;

        Stopwatch stopwatch = new Stopwatch(); 
        long initialMemoryFootPrint = GC.GetTotalMemory(true);

        stopwatch.Start();
        for (int i = 0; i < itemCount; i++)
        {
            int iCopy = i;  // You should not use 'i' directly in the thread start as it creates a closure over a changing value which is not thread safe. You should create a copy that will be used for that specific variable.
            Task.Factory.StartNew(() =>
            {
                // lets simulate something that takes a while
                int k = 0;
                while (true)
                {
                    if (k++ > 100000)
                        break;
                }

                if ((iCopy + 1) % 200 == 0) // By the way, what does your sendMessage(list[0]['1']); mean? what is this '1'? if it is i you are not thread safe.
                    Console.WriteLine(iCopy + " - Time elapsed: (ms)" + stopwatch.ElapsedMilliseconds);
            });
        }

        Console.ReadKey();
        Console.WriteLine("Memory usage: " + (GC.GetTotalMemory(false) - initialMemoryFootPrint));
        Console.ReadKey();
    }

结果:

Task Parallel Library result

因此我们可以看到:

+--------+------------+------------+--------+
|        | new Thread | ThreadPool |  TPL   |
+--------+------------+------------+--------+
| Time   | 6749       | 228ms      | 222ms  |
| Memory |300kb     |103kb     |123kb |
+--------+------------+------------+--------+

以上内容符合我们在理论上预期的情况。与线程池相比,新线程需要更高的内存,并且总体性能较慢。线程池和TPL具有相同的性能,但TPL的内存占用略高于纯线程池,但考虑到任务所提供的额外灵活性(例如取消,等待完成查询任务状态),这可能是一个值得付出的代价。

到此为止,我们已经证明使用线程池线程是速度和内存方面更可取的选项。

不过,我们还没有回答你的问题。如何追踪正在运行的线程的状态。

回答您的问题

根据我们收集的见解,以下是我处理它的方式:

        List<string>[] list = listdbConnect.Select()
        int itemCount = list[0].Count;
        Task[] tasks = new Task[itemCount];
        stopwatch.Start();
        for (int i = 0; i < itemCount; i++)
        {
            tasks[i] = Task.Factory.StartNew(() =>
            {
                // NOTE: Do not use i in here as it is not thread safe to do so! 
                sendMessage(list[0]['1']);
                //calling callback function
            });
        }

        // if required you can wait for all tasks to complete
        Task.WaitAll(tasks);
        
        // or for any task you can check its state with properties such as: 
        tasks[1].IsCanceled
        tasks[1].IsCompleted
        tasks[1].IsFaulted 
        tasks[1].Status

最后一点需要注意的是,在Thread.Start中不能使用变量i,因为它会创建一个闭包并共享所有线程中正在更改的变量。如果需要访问i,可以通过创建变量的副本并传递该副本来解决这个问题,这将使每个线程都有一个闭包,从而使其线程安全。

祝你好运!


非常感谢。到目前为止,这是我读过的关于这个话题最好的回答之一。 - sebingel
这个回答在教育意义上告诉 OP 使用 ThreadPool 或 Task.Factory,而不是回答实际提问的问题。我很惊讶这在 SO 上被容忍。此外,答案中表达的观点甚至不一定正确。当然,为短暂任务创建数千个新线程对象是一个坏主意。但有些应用程序可能会受益于创建一个专用于长时间运行任务的线程。 - radfast

13

使用Process.Threads属性:

var currentProcess = Process.GetCurrentProcess();
var threads = currentProcess.Threads;

注意:任何属于当前进程的线程都会在此处显示,包括那些并非明确由您创建的线程。

如果你只想要你自己创建的线程,那么为什么不在创建它们时跟踪它们呢?


1
这可能会有点误导性。在这种情况下,var threads 是一个 ProcessThreadCollection,它纯粹包含线程的信息,而不是 Thread 对象本身。 - Riegardt Steyn

9
创建一个 List<Thread>,并在第一个 for 循环中将每个新线程存储在其中。
List<string>[] list;
List<Thread> threads = new List<Thread>();
list = dbConnect.Select();

for (int i = 0; i < list[0].Count; i++)
{
    Thread th = new Thread(() =>{
        sendMessage(list[0]['1']);
        //calling callback function
    });
    th.Name = "SID"+i;
    th.Start();
    threads.add(th)
}

for (int i = 0; i < list[0].Count; i++)
{
    threads[i].DoStuff()
}

然而,如果你不需要使用i,则可以将第二个循环改为foreach


另外,如果你的sendMessage函数执行时间不长,你应该使用比完整线程更轻量级的东西,例如使用ThreadPool.QueueUserWorkItem或者如果有的话使用Task


1
这可能是解决问题的最佳方案。 - Stefan Paul Noack
是的,不错,但Thread没有DoStuff方法(但我想它可以使用扩展方法来实现)。 - spender
@spender 是的,我不知道 OP 想用它做什么,所以这是我写下它时的思考过程。 - Scott Chamberlain
1
@spender DoStuff 指的是像 StartInterruptJoin 等操作,即做事情 - Kiril

3
Process.GetCurrentProcess().Threads

这会给你一个当前进程中所有运行中的线程列表,但请注意,除了您自己启动的线程之外,还有其他线程。

1
使用 Process.Threads 迭代遍历您的线程。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接