在另一个线程中独立计时 For 循环的每个滴答声的最佳方法

Posted

技术标签:

【中文标题】在另一个线程中独立计时 For 循环的每个滴答声的最佳方法【英文标题】:Best approach to Independently Timing each Tick of a For Loop in another Thread 【发布时间】:2021-10-28 01:06:40 【问题描述】:

假设我有一个客户端线程和一个服务器线程。客户端线程必须执行昂贵的 for 循环操作,这很容易挂起。这样,服务器就可以独立判断for循环的每个tick是否超过了最大时间。这背后的背景是,如果完成一个滴答的时间过长,服务器将使客户端超时。

我最初的想法是在客户端和服务器线程中有两个 for 循环。服务器线程将有一个等待 1 秒的条件变量。如果客户端在每个tick的1秒内没有通知条件变量,服务端会超时:

服务器

 bool success;
 for (int i = 0; i < 10; i++) 
     std::unique_lock<std::mutex> lock(CLIENT_MUTEX);
     success = CLIENT_CV.wait_for(lock, std::chrono::seconds(1));
     if (!success) 
         std::cout << "timed out during tick " << i << std::endl;
         break;
     
  

客户

for (int i = 0; i < 10; i++) 
    std::unique_lock<std::mutex> lock(CLIENT_MUTEX);

    //do work

    CLIENT_CV.notify_one();


但是,考虑到客户端的相同工作,我的实现尝试是不可靠的,并且会随机超时。如何改进设计以使其更可靠?

旁注:

一个简单的解决方案是让服务器对整个 for 循环而不是每个滴答计时。但是,如果 for 循环在 10 次中的第 1 次失败,并且计时器正在等待 10 秒,则将在 10 秒后通知客户端。但是,如果服务器对每个滴答(10x1sec = 10 秒)施加 1 秒超时,则客户端将被通知超时,而无需等待整整 10 秒。

编辑。

整个客户端/服务器/超时的类比只是将问题置于上下文中。我完全对从不同线程计时 for 循环的最佳方法感兴趣。

【问题讨论】:

不可靠到底是什么意思?如果它随机超时,这可能只是表明工作需要大约 1 秒并且触发超时的次数相当多(即对于需要 0.8+-0.1 秒的任务,您预计大约 50 分之一会超时如果超时是 1 秒),或者它没有计时 1 秒但有时会在 0.1 秒后超时? @RichardCritten 超时部分不是问题的一部分。我只是对为 for 循环的每个滴答计时的最佳方法感兴趣。在我真正的问题中,它没有超时。我只是想表明对这个过程进行计时很重要。 这是一个 XY 问题,真正的问题是如何通知/终止客户端线程?如果通知客户端线程关闭,则客户端线程必须循环和轮询(在工作期间),并且它可以根据作为工作包的一部分传递给它的超时来关闭自己。如果客户端线程是硬循环(而不是轮询),那么在客户端线程完成工作之前,无法彻底关闭它。无论哪种方式,都不需要从服务器线程监视客户端线程。 @RichardCritten 我只是想把问题放到上下文中。我的真正问题不涉及服务器或客户端。标题为“在另一个线程中独立计时 For 循环的每个 Tick 的最佳方法”。我现在已经在问题中澄清了这一点。 你可以实现一个账本,它在每个滴答声(以及整个循环之后)接收一个std::chrono::time_point,它会为每个滴答声计时。您可以将此分类帐提供给定期检查此分类帐的其他线程并休眠,以便在它期望下一个条目时唤醒它,而当它没有时它可以设置一些标志,但我不确定这是否是“最好的办法。不过,我也认为不需要其他线程。 【参考方案1】:

这样做的一种方法可能是:

共享变量:

std::vector<std::chrono::time_point<std::chrono::high_resolution_clock>> ledger;
std::mutex ledger_mtx;

客户:

for (int i = 0; i < 10; i++) 
    
    std::scoped_lock lock(ledger_mtx);
    ledger.push_back(std::chrono::high_resolution_clock::now());
    
    // Do work


std::scoped_lock lock(ledger_mtx);
ledger.push_back(std::chrono::high_resolution_clock::now());

服务器:

size_t id = 0;
std::this_thread::wait_for(1s); // Some time so that initial write to ledger is made
while(true) 
    
    std::scoped_lock lock(ledger_mtx);
    if(ledger.size()==id)  /* Do something if the thread hangs */ 
    id = ledger.size();
    std::chrono::time_point<std::chrono::high_resolution_clock> last_tick = ledger.back();
    
    if(id == 11) break;
    std::this_thread::sleep_for(1s - (std::chrono::high_resolution_clock::now() - last_tick));

通过这种方式,您可以对线程进行计时,同时从外部对其进行监控。这是最好的方法吗?可能不会,但它确实为您提供了所需的时间。

【讨论】:

问题始于“...用挂起的线程做点什么...” - 因为没有办法干净地终止挂起或不合作的线程。由于在不使进程处于未定义状态的情况下无法终止线程,因此为线程计时是没有意义的。如果线程正在协作(轮询等),则无需对其计时,因为客户端线程可以处理自己的超时。 @RichardCritten 我明白这一点,但 OP 说 “整个客户端/服务器/超时的类比只是将问题置于上下文中。我纯粹对最佳计时方式感兴趣来自不同线程的 for 循环。”。现在你为什么想要这个也超出了我的范围。 (即使线程挂起,也许仍然可以获得一些基准数据?)。我把这个评论表述得很糟糕,如果线程挂起,我打算做点什么

以上是关于在另一个线程中独立计时 For 循环的每个滴答声的最佳方法的主要内容,如果未能解决你的问题,请参考以下文章

显示给定系统滴答计数的时间字符串

C# - 计时器滴答时间比指定的间隔更短,导致重复作业并行运行

为啥我的计时器停止滴答作响?

为啥有时在计时器滴答事件中它会连续两次调用该方法?

计时器、线程和编译器不当行为

OpenMP:在每个线程中都有一个完整的“for”循环