【问题标题】:Mutex vs. standard function call互斥与标准函数调用
【发布时间】:2016-04-21 04:33:58
【问题描述】:

当我有这样的代码块时:

    mutex mtx;

    void hello(){
        mtx.lock();
        for(int i = 0; i < 10; i++){
           cout << "hello";
        }
        mtx.unlock();
    }
    void hi(){
        mtx.lock();
        for(int i = 0; i < 10; i++){
           cout << "hi";
        }
        mtx.unlock();
    }

    int main(){
       thread x(hello);
       thread y(hi);
       x.join();
       y.join();
    }

What is the difference between just calling `hello()` and `hi()`? (Like so)
   ...
   int main(){
      hello();
      hi();
   }

线程效率更高吗?线程的目的是同时运行,对吧?

有人可以解释为什么我们在线程函数中使用互斥量吗?谢谢!

【问题讨论】:

  • 与顺序相反,它们被并行调用。
  • 整个线程代码被封装在阻止并发执行的锁定机制中,因此在这种非常特殊的情况下,线程并没有提高效率,因为它们被迫顺序执行。您需要为实例化和加入线程支付额外的费用,而您不会通过简单地调用函数来支付这些费用。

标签: c++ function mutex


【解决方案1】:

线程的目的是同时运行,对吧?

是的,线程用于并行执行多个任务,尤其是在不同的 CPU 上。

有人可以解释为什么我们在线程函数中使用互斥量吗?

将多个线程相互序列化,例如当它们正在访问一个不能安全并发访问且需要保护的共享资源时。

【讨论】:

  • 共享资源是指整数、字符等对象吗?
  • 线程彼此共享的任何内容。可以是变量,也可以是硬件资源,也可以是文件等。
【解决方案2】:

线程效率更高吗?

否。但请参阅最后说明(下方)。

在单核上,线程的效率要低得多(比函数/方法调用)。

例如,在我的 Ubuntu 15.10(64) 上,使用 g++ v5.2.1,

a) 通过使用 std::mutex 强制执行的上下文切换(从一个线程到另一个线程)大约需要 12,000 纳秒

b) 但调用 2 个简单方法,例如 std::mutex lock() 和 unlock(),这需要 < 50 纳秒。 3个数量级!所以上下文切换 vx 函数调用是无可争辩的。

线程的目的是同时运行,对吧?

是的……但这不可能发生在单核处理器上。

在多核系统上,上下文切换时间仍然占主导地位。

比如我的Ubuntu系统是双核的。我在上面报告的上下文切换时间的测量使用了 10 个线程链,其中每个线程只是等待其输入信号量被解锁()。当一个线程的输入信号量被解锁时,该线程开始运行……但简短的线程活动只是 1) 增加一个计数并检查一个标志,以及 2) unlock() 下一个线程,以及 3) lock() 它的自己的输入互斥锁,即再次等待上一个任务信号。在该测试中,我们称为 main 的线程使用其中一个线程的 unlock() 启动线程排序,并使用所有线程都可以看到的标志停止它。

在此测量活动期间(大约 3 秒),Linux 系统监视器显示涉及两个内核,并报告两个内核的利用率都在 60% 左右。我预计两个核心都为 100%..不知道为什么不是。

有人可以解释为什么我们在线程函数中使用互斥量吗?感谢 你!

我想 std::mutex 的最常规用途是序列化对内存结构(可能是共享访问存储或结构)的访问。如果您的应用程序具有可由多个线程访问的数据,则每个写访问都必须序列化以防止竞争条件破坏数据。有时,读取和写入访问都需要序列化。 (参见哲学家就餐问题。)

在您的代码中,例如(虽然我不知道您使用的是什么系统), std::cout (共享结构)可能会“交错”文本。也就是说,线程上下文切换可能发生在打印“hello”甚至“hi”的过程中。这种行为通常是不受欢迎的,但可能是可以接受的。

多年前,我与 vxWorks 合作,我的团队学会了在访问 std::cout 时使用互斥锁来消除交错。这种行为可能会分散注意力,而且通常客户不喜欢这种行为。 (最终,对于该应用程序,我们取消了标准三重奏 io(cout、cerr、cin)的使用)

如果允许多个线程“同时”尝试对各种设备进行操作,则各种设备也可能无法正常运行。例如,我为一台设备编写的软件需要 50 us 或更长时间才能完成对我的软件“戳”的反应,然后才能对设备应用任何其他操作。该设备无需等待就直接忽略了我的代码操作。


您还应该知道,有些技术不涉及信号量,而是使用线程和 IPC 来提供序列化(即受保护的)资源访问。

来自维基百科,“在并发编程中,监视器是一种同步构造,它允许线程具有互斥和等待(阻塞)特定条件变为真的能力。”

当 os 提供合适的 IPC 时,我更喜欢使用 Hoare monitor。在我的解释中,监视器只是一个通过 IPC 接受命令的线程,并且是只要线程访问共享结构或设备。当只有 1 个线程访问结构时,不需要互斥体。所有其他线程必须发送消息(通过 IPC)以请求(或可能命令)另一个结构更改。监控线程一次处理一个请求,从 IPC 中按顺序处理。


定义:碰撞

在“线程上下文切换”和“互斥信号量”的上下文中,当线程必须阻塞并等待访问资源时会发生“冲突”,因为该资源已经在“使用中”(即“占用”) . 这是一个强制的上下文切换. 另请参见术语“临界区”。

当共享资源当前未被使用时,没有冲突。 lock() 和 unlock() 几乎没有成本(与上下文切换相比)。

当发生冲突时,上下文切换会使事情变慢“一堆”。但这“一群”可能仍然是可以接受的...考虑与关键部分内的活动持续时间相比,“束”何时较小。


最后说明......有了这个“碰撞”的新想法:

a) 面对许多冲突,多线程的效率可能低得多。

对于意想不到的例子,函数“new”访问一个线程共享资源,我们可以称之为“动态内存”。在一次体验中,每个线程在启动时生成 1000 个新线程。一个线程可以在 0.5 秒内完成这项工作。四个线程,连续快速启动,用了 40 秒来完成 4 次启动。上下文切换!

b) 当你有多个内核并且没有/或很少有冲突时,多线程可以更有效。本质上,如果线程很少交互,它们可以(大部分)同时运行。

当多核和冲突时,线程效率可以是 a 或 b 之间的任何位置。

例如,我的基于 ram 的“日志”机制似乎运行良好——每个日志条目一个互斥锁访问。通常,我有意使用最少的日志记录。在调试“发现的”挑战时,我添加了额外的日志记录(可能稍后删除)以确定出了什么问题。通常,调试器优于一般的日志记录技术。但有时,添加多个日志条目效果很好。

【讨论】:

    【解决方案3】:

    与纯串行代码相比,线程至少有两个优势。

    1. 便于分离逻辑上独立的指令序列。即使在单核机器上也是如此。这为您提供了逻辑并发性,但不一定是并行性。

      • 拥有多个线程允许操作系统或用户级线程库在较少数量的 CPU 内核上多路复用多个逻辑线程,而应用程序开发人员不必担心其他线程和进程。
    2. 利用多核/处理器。线程允许您将执行扩展到您拥有的 CPU 内核数量,从而实现并行性。

      你的例子有点做作,因为整个线程的执行都被锁定了。通常,线程独立执行许多操作,并且仅在访问共享资源时使用互斥量。

      更具体地说,在您的场景下,您不会获得任何性能。但是,如果您的整个线程不在互斥锁之下,那么您可能会提高效率。我说可能是因为运行多个线程会产生开销,这可能会抵消您获得的任何效率增益。

    【讨论】:

    • 并发性和并行性是相关的,但不能互换。问题是关于并行性。例如。我通过编写函数来分离逻辑上独立的指令序列。很方便。
    • @knivil,并行是同时执行,而并发是在逻辑上运行简单交错的线程。差异描述为here
    • Downvoter 请更正此答案。我有兴趣了解我所缺少的东西。
    • 很多人把线程和“任务”混在一起,引入逻辑线程或逻辑并发并不能改善这种情况。最后你会混淆自己:锁定执行与独立指令序列相互排斥。是的,你提到它。此外,您获得效率的假设也值得怀疑。
    • @knivil,我谈到了最后一点,虽然我不确定如何让第一点更清楚,因为互联网上已经有多少关于这个主题的混乱。
    【解决方案4】:

    线程理论上同时运行,这意味着线程可以同时写入同一个内存块。例如,如果您有一个全局变量int i;,并且两个线程试图同时写入不同的值,那么哪一个值保留在i中?

    Mutex 强制同步访问内存,在互斥锁块(mutex.lock 和 mutex.unlock)内,您保证同步内存访问并避免内存损坏。

    当您调用 mtx.lock() 时,只有一个线程保持运行,并且任何其他调用相同 mtx.lock() 的线程停止,等待 mtx.unlock 调用。

    【讨论】:

    • 当调用mtx.lock()时,只有在同一个mtx对象上调用lock()的线程才会被阻塞,直到unlock()被调用。其他线程将愉快地保持畅通无阻地运行。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-04-21
    • 1970-01-01
    • 1970-01-01
    • 2011-11-13
    • 2015-12-20
    • 1970-01-01
    • 2013-11-12
    相关资源
    最近更新 更多