【问题标题】:Thread static class methods vs global scope线程静态类方法与全局范围
【发布时间】:2012-03-08 10:21:14
【问题描述】:

想象一个应用程序的功能需要多达 5 个线程处理数据,这些线程使用缓冲区、互斥体和事件相互交互。性能至关重要,语言是 C++。

该功能可以作为一个(编译)单元和一个类来实现,并且只能为应用程序实例化该类的一个实例。该类本身在 run() 方法中实现了 1 个线程,该方法产生其他 4 个线程,管理它们并在用户关闭应用程序时收集它们。

选择以下方法之一比另一种方法有什么优势(请告诉我任何更好的方法)?

  1. 向类中添加 5 个静态方法,每个方法运行一个线程、互斥锁和其他数据作为静态类变量共享。
  2. 添加5个全局函数(无作用域)并使用全局变量、事件和互斥锁(好像是C)
  3. 完全改变模式,再添加 4 个类,每个类实现一个线程并通过全局变量共享数据。

以下是一些需要考虑的想法和问题(如有错误请更正):

  1. 将线程作为类成员(当然是静态的),它们可以依靠单例来访问非静态成员函数,它还为它们提供了一个命名空间,这本身似乎是个好主意。
  2. 使用静态类方法,类头文件很快将包含许多静态变量(和其他辅助静态方法)。必须在类头文件中声明变量可能会给包含头文件的其他单元带来额外的依赖关系。如果变量在全局范围内声明,它们可以隐藏在单独的头文件中。
  3. 静态类变量应该在代码的某个地方定义,所以它会加倍输入声明的东西。
  4. 编译器可以利用命名空间解析来优化代码(与可能在不同单元中的全局变量相反)。
  5. 单个单元可能会得到更好的优化,而整个程序的优化速度很慢并且可能效果不佳。
  6. 如果单元增长,我必须将部分代码移动到一个单独的单元,所以我将有一个包含多个(编译)单元的类,这是否是反模式?
  7. 如果使用多个类,每个类处理一个线程,同样的问题可以在静态方法和全局函数之间做出决定来实现线程。此外,这需要更多的代码留置权,这不是一个真正的问题,但值得额外的开销吗?

请在没有 Qt 等库的情况下回答这个问题,然后假设我们可以依赖 QThread 并为每个 run() 方法实现一个线程。

Edit1:每个设计的线程数是固定的,数字 5 只是一个示例。请分享您对方法/模式的想法,而不是细节。

Edit2:我发现this answer(对另一个问题)非常有帮助,我猜第一种方法滥用类作为命名空间。如果与命名空间结合使用,则可以减轻第二种方法。

【问题讨论】:

  • 我不喜欢计划 1、2 或 3。它们似乎都有神奇的数字“5”(或 5-1)。这开始散发出线程微管理和缺乏灵活性的味道。如果性能至关重要,那么如何拼凑代码并不重要——重要的是如何拼凑数据。你在操作什么数据?是否可以简单地在线程之间进行划分以减少锁定和错误共享?输入数据从哪里来,输出到哪里去?
  • +1 @MartinJames - Martin 是对的:您以错误的方式思考问题。您需要决定“如何以独立计算的方式划分数据”?考虑输入和输出也很重要:如果你做对了,你甚至可能不需要锁。我强烈建议您构建一个简化模型并对其进行彻底测试,以确保您在真正实施之前了解线程和锁定的工作原理。
  • @MartinJames - 感谢您的回答,实际上在一个项目中我正在使用方法 1。它具有网络、视频捕获、编码、解码、跟踪和渲染功能。假设一个 4 核处理器,我已经划分了任务,它是如何神奇数字 5 来的。同时,我使用 IPP、OpenMP 和 OpenGL 直接渲染,我可以得到低于 3ms 的帧,这意味着一个很好的 fps。我有环形缓冲区,只有当其中一个线程落后时才使用锁,所以它几乎是无锁的。
  • @kfmfe04 - 应用程序工作正常,只是现在我有一个胖 cpp 文件,一个头文件,正如我所说,包括它会给其他单元带来一些依赖性,减少类的相互依赖性。我还有其他一些使用第二种方法的项目,但对我来说,它们是伪装成 C++ 的 C。我也有一些 Java 项目,其方法类似于第三种方法。所以我想知道这些模式背后的基本原理是什么,以及其他人使用的是什么。

标签: c++ multithreading qt static-methods


【解决方案1】:

来源

首先,您应该阅读 Herb Sutter 的全部并发文章:

http://herbsutter.com/2010/09/24/effective-concurrency-know-when-to-use-an-active-object-instead-of-a-mutex/

这是上一篇文章的链接,里面包含了之前所有文章的链接。

你的情况是什么?

根据以下文章:您拥有或需要多少可扩展性? (http://drdobbs.com/parallel/201202924),您属于O(K): Fixed 案例。也就是说,您有一组固定的任务要同时执行。

根据您的应用程序的描述,您有 5 个线程,每个线程都在做非常不同的事情,因此您必须有 5 个线程,也许希望其中一个或一些线程仍然可以将他们的任务划分为多个线程(因此,使用线程池),但这将是一个奖励。

我让你阅读这篇文章以了解更多信息。

设计问题

关于单例

忘记单例。这是dumb, overused pattern。

如果您真的想限制您的类的实例数量(而且说真的,您没有比这更好的事情吗?),您应该将设计分成两部分:一个用于数据类,一个用于类将前一个类包装到单例限制中。

关于编译单元

使您的标题和来源易于阅读。如果您需要将一个类实现到多个源中,那就这样吧。我相应地命名了来源。例如,对于 MyClass 类,我会:

  • MyClass.hpp:标题
  • MyClass.cpp : 主要来源(带有构造函数等)
  • MyClass.Something.cpp : 用某物处理源代码
  • MyClass.SomethingElse.cpp : 用其他东西处理源代码
  • 等

关于编译器优化

最近的编译器能够内联来自不同编译单元的代码(我在 Visual C++ 2008,IIRC 上看到了该选项)。我不知道整个全局优化是否比“一个单元”编译更糟糕,但即使是这样,您仍然可以将代码分成多个源,然后让一个全局源包含所有内容。例如:

  • MyClassA.header.hpp
  • MyClassB.header.hpp
  • MyClassA.source.hpp
  • MyClassB.source.hpp
  • global.cpp

然后相应地进行包含。但是您应该确保这实际上可以使您的性能更好:除非您确实需要它并且您已针对它进行了分析,否则不要优化。

你的情况,但更好?

您的问题和 cmets 更多地谈论单体设计而不是性能或线程问题,所以我可能是错的,但您需要的是简单的重构。

我会使用第三种方法(每个线程一个类),因为类具有私有/公共访问权限,因此,您可以使用它来保护一个线程拥有的数据,只需将其设为私有即可。

以下指南可以帮助您:

1 - 每个线程都应该隐藏在一个非静态对象中

您可以使用该类的私有静态方法,也可以使用匿名命名空间函数(我会使用该函数,但在这里,我想访问该类的私有函数,所以我会满足于静态方法)。

通常,线程构造函数允许您将指针传递给带有void * 上下文参数的函数,因此使用它将this 指针传递给主线程函数:

每个线程有一个类可以帮助您隔离该线程,从而将该线程的数据与外部世界隔离:没有其他线程能够访问该数据,因为它是私有的。

这里有一些代码:

// Some fictious thread API
typedef void (*MainThreadFunction)(void * p_context) ;
ThreadHandle CreateSomeThread(MainThreadFunction p_function, void * p_context) ;

// class header
class MyClass
{
   public :
      MyClass() ;
      // etc.

      void         run() ;

   private :
      ThreadHandle m_handle ;

      static void  threadMainStatic(void * p_context) ;
      void         threadMain() ;
}

.

// source
void MyClass::run()
{
   this->m_handle = CreateSomeThread(&MyClass::threadMainStatic, this) ;
}

void MyClass::threadMainStatic(void * p_context)
{
   static_cast<MyClass *>(p_context)->threadMain() ;
}

void MyClass::threadMain()
{
   // Do the work
}

Displaimer:这没有在编译器中测试。将其视为伪 C++ 代码而不是实际代码。 YMMV。

2 - 识别未共享的数据。

这些数据可以隐藏在拥有对象的私有部分中,如果它们受到同步保护,那么这种保护是多余的(因为数据不共享)

3 - 识别共享的数据

...并验证其同步性(锁、原子访问)

4 - 每个类都应该有自己的标题和来源

...并在必要时通过同步保护对其(共享)数据的访问

5 - 尽可能保护访问

如果一个函数被一个类使用,并且只是一个类,并且不需要访问类内部,那么它可以隐藏在匿名命名空间中。

如果一个变量仅由一个线程拥有,则将其作为私有变量成员隐藏在类中。

等等

【讨论】:

    猜你喜欢
    • 2019-06-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-05-12
    • 2010-11-08
    相关资源
    最近更新 更多