C#线程无法休眠?

54
我有这段代码:
void Main()
{
    System.Timers.Timer t = new System.Timers.Timer (1000);
    t.Enabled=true;
    t.Elapsed+= (sender, args) =>c();
    Console.ReadLine();

}

int h=0;
public void c()
{
    h++;
    new Thread(() => doWork(h)).Start();
}

public void doWork(int h)
{
    Thread.Sleep(3000);
    h.Dump();
}

我想知道如果时间间隔为1000毫秒,作业处理需要3000毫秒会发生什么。

但是我看到了奇怪的行为 - 3000毫秒的延迟只在开始时发生!

如何让每个doWork都睡眠3000毫秒?

正如你在这里看到的,在开始时有3秒的延迟,然后每次迭代1秒。

enter image description here


5
我看不到问题。发生的情况正是多线程场景下所期望的。 - leppie
4
顺便说一句,创建大量线程不是一个好主意;线程相对来说比较耗费资源。 - Marc Gravell
7
您期望的行为是什么?您每秒开始一个3秒的工作,因此第一份工作在3秒后完成,第二份工作在4秒后完成,第三份在5秒后等等。所以一切都按预期进行,这里没有什么异常情况。 - Dirk Vollmar
3
请注意,您正在修改一个被捕获的变量。这很少有效,虽然在这里您不会得到最糟糕的副作用。但是从技术上讲,使用您目前的代码可能会出现奇怪的间隙和重复的数字。 - Marc Gravell
6
太棒了,非常赞!给这个动态图片点赞。 - Alex Gordon
显示剩余8条评论
6个回答

65

每次计时器滴答作响,您就会启动一个线程来进行一些睡眠;该线程完全隔离,并且定时器将继续每秒钟触发。 实际上,即使将 Sleep(3000) 移入 c() 中,计时器每秒都会触发

您目前拥有的是:

1000 tick (start thread A)
2000 tick (start thread B)
3000 tick (start thread C)
4000 tick (start thread D, A prints line)
5000 tick (start thread E, B prints line)
6000 tick (start thread F, C prints line)
7000 tick (start thread G, D prints line)
8000 tick (start thread H, E prints line)
...

不清楚您想要做什么。您可以在不需要触发计时器时禁用它,然后在准备好后再恢复它,但是这里Sleep()的目的不清楚。另一种选择是使用带有Sleep()while循环。简单,不涉及大量线程。


2
感谢您实际解释正在发生的事情! - aukaost
@MarcGravell 谢谢(我知道使用线程是昂贵的...但仍然只是为了学习。) - Royi Namir
6
如果您是为了学习而这么做的话,那么请注意,这种方法完成定期工作的成本非常高昂。;p - Marc Gravell
4
@MarcGravell是谁说我在进行定期工作?写代码并观察结果并不意味着我正在建造某些东西。 - Royi Namir
1
@Royi 嗯,除非有一个目标行为,否则很难进行调试。 - Marc Gravell

16

每秒钟会启动一个新线程,具有3秒的延迟。它的执行方式如下:

  1. 线程1开始执行
  2. 线程2开始执行,线程1休眠
  3. 线程3开始执行,线程2休眠,线程1休眠
  4. 线程4开始执行,线程3休眠,线程2休眠,线程1休眠
  5. 线程5开始执行,线程4休眠,线程3休眠,线程2休眠,线程1退出
  6. 线程6开始执行,线程5休眠,线程4休眠,线程3休眠,线程2退出
  7. 线程7开始执行,线程6休眠,线程5休眠,线程4休眠,线程3退出

可以看出,每个线程都会休眠3秒,但是每秒会发生一次“退出”操作。

如何使用线程?类似这样:

void Main()
{
    new Thread(() => doWork()).Start();
    Console.ReadLine();
}

public void doWork()
{
    int h = 0;
    do
    {
        Thread.Sleep(3000);
        h.Dump();
        h++;
    }while(true);
}

9

你的示例非常有趣,展示了并行处理的副作用。为了回答你的问题,并更容易地看到这些副作用,我稍微修改了你的示例:

using System;
using System.Threading;
using System.Diagnostics;

public class Program
{
    public static void Main()
    {
        (new Example()).Main();
    }
}

public class Example
{
    public void Main()
    {
        System.Timers.Timer t = new System.Timers.Timer(10);
        t.Enabled = true;
        t.Elapsed += (sender, args) => c();
        Console.ReadLine(); t.Enabled = false;
    }

    int t = 0;
    int h = 0;
    public void c()
    {
        h++;
        new Thread(() => doWork(h)).Start();
    }

    public void doWork(int h2)
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        try
        {
            t++;
            Console.WriteLine("h={0}, h2={1}, threads={2} [start]", h, h2, t);
            Thread.Sleep(3000);
        }
        finally
        {
            sw.Stop();
            var tim = sw.Elapsed;
            var elapsedMS = tim.Seconds * 1000 + tim.Milliseconds;
            t--;
            Console.WriteLine("h={0}, h2={1}, threads={2} [end, sleep time={3} ms] ", h, h2, t, elapsedMS);
        }
    }
}

我所做的修改如下:
  • 计时器间隔现在为10毫秒,线程仍然有3000毫秒。效果是,当线程正在休眠时,新线程将被创建
  • 我添加了变量t,它计算当前活动线程的数量(当线程启动时增加,线程结束前减少)
  • 我添加了2个转储语句,打印出线程开始和线程结束
  • 最后,我给函数doWork的参数一个不同的名称(h2),这允许查看底层变量h的值
现在很有趣看到这个修改后的程序在LinqPad中的输出(请注意,这些值并不总是相同的,因为它们取决于已启动线程的竞争条件):
    h=1, h2=1, threads=1 [start]
    h=2, h2=2, threads=2 [start]
    h=3, h2=3, threads=3 [start]
    h=4, h2=4, threads=4 [start]
    h=5, h2=5, threads=5 [start]
    ...
    h=190, h2=190, threads=190 [start]
    h=191, h2=191, threads=191 [start]
    h=192, h2=192, threads=192 [start]
    h=193, h2=193, threads=193 [start]
    h=194, h2=194, threads=194 [start]
    h=194, h2=2, threads=192 [end]
    h=194, h2=1, threads=192 [end]
    h=194, h2=3, threads=191 [end]
    h=195, h2=195, threads=192 [start]

我认为这些数值本身就说明了问题:每10毫秒就会启动一个新线程,而其他线程仍在睡眠。有趣的是,h并不总是等于h2,特别是当更多的线程在睡眠时启动时。线程数(变量t)在一段时间后稳定下来,即大约在190-194之间运行。
你可能会认为,我们需要在变量t和h上加锁,例如。
readonly object o1 = new object(); 
int _t=0; 
int t {
       get {int tmp=0; lock(o1) { tmp=_t; } return tmp; } 
       set {lock(o1) { _t=value; }} 
      }

虽然这种方式更加简洁,但它并没有改变本例中所显示的效果。

为了证明每个线程确实会休眠3000毫秒(即3秒),让我们在工作线程doWork中添加一个Stopwatch

public void doWork(int h2) 
{ 
    Stopwatch sw = new Stopwatch(); sw.Start();
    try 
    {
        t++; string.Format("h={0}, h2={1}, threads={2} [start]", 
                            h, h2, t).Dump();                               
        Thread.Sleep(3000);         }
    finally {
        sw.Stop(); var tim = sw.Elapsed;
        var elapsedMS = tim.Seconds*1000+tim.Milliseconds;
        t--; string.Format("h={0}, h2={1}, threads={2} [end, sleep time={3} ms] ", 
                            h, h2, t, elapsedMS).Dump();
    }
} 

为了正确清理线程,请在ReadLine之后禁用定时器,代码如下:
    Console.ReadLine(); t.Enabled=false; 

这使你能够看到在你按下ENTER键后不再启动更多线程时会发生什么:
    ...
    h=563, h2=559, threads=5 [end, sleep time=3105 ms] 
    h=563, h2=561, threads=4 [end, sleep time=3073 ms] 
    h=563, h2=558, threads=3 [end, sleep time=3117 ms] 
    h=563, h2=560, threads=2 [end, sleep time=3085 ms] 
    h=563, h2=562, threads=1 [end, sleep time=3054 ms] 
    h=563, h2=563, threads=0 [end, sleep time=3053 ms] 

如预期的那样,您可以看到它们一个接一个地被终止,而且它们睡了约3秒(或3000毫秒)。


6
你看到这种行为的原因很简单:每秒调度一个新线程,结果在三秒后变得可见。前四秒你什么也看不到;然后,已经启动了三秒钟的线程会被转储;到那时,另一个线程将已经睡眠两秒钟,还有另一个线程将已经睡眠一秒钟。下一秒线程#2会被转储;然后是线程#3、#4等等-每秒都会打印输出。
如果您想每三秒打印输出一次,请每三秒安排一个新的线程,并设置任何延迟:初始线程将在三秒加上延迟后输出;随后的所有线程都将在三秒间隔内触发。

2

看起来你每秒钟都在运行一个新的线程,这不是一个好主意。使用BackgroundWorker,在事件BackgroundWorker Completed完成时再次调用C函数,这样就不需要计时器了。


2
我不想使用BackgroundWorker,我想使用普通的线程命令... :) - Royi Namir
然后只需删除计时器代码,并在线程完成睡眠1秒后,以递归的方式/模式调用C函数/方法。 - JohnnBlade
2
“我不想使用正确的工具,我想使用错误的工具”这句话对于解决问题并没有什么帮助。 - H H
1
@HenkHolterman,虽然代码很有用,但认为它是解决方案可能有些牵强。 - Jodrell
2
@HenkHolterman 我认为原始代码是一个试验,旨在弄清楚事物的运作方式。显然,这不是生产代码,因此,它并不是做任何事情的错误方式。 - Chris Cudmore

2
每个 doWork 函数都会睡眠三秒钟,但由于您在一秒的时间间隔内创建线程,它们的睡眠会重叠。

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