【问题标题】:std::thread management: usage and best practicestd::thread 管理:用法和最佳实践
【发布时间】:2013-10-17 09:18:54
【问题描述】:

在 Java 中使用了一些线程之后,我试图找出线程,但我有点困惑。 两个问题:

  • 我可以从线程扩展我的类还是必须通过处理程序从类中管理线程?
  • 如何保存上述线程处理程序? std::thread 本身似乎没有命名类型。

我们将不胜感激。

我如何解读这条信息?

src/CHandler.h:27:9: error: 'thread' in namespace 'std' does not name a type
         std::thread _thread;
         ^

这是我扩展线程的尝试:

src/CHandler.h:17:30: error: expected class-name before '{' token
 class CHandler : std::thread {
                              ^

完整但麻烦的标题:

#ifndef __projectm__CHandler__
#define __projectm__CHandler__

#include <set>
#include <vector>
#include <thread>

#include "CListener.h"

class CHandler {
    public:
        virtual bool subscribe(std::shared_ptr<CListener> aListener);
        virtual bool unsubscribe(std::shared_ptr<CListener> aListener);

        virtual bool hasSubscriber(std::shared_ptr<CListener> aListener);

        virtual ~CHandler() {}

    protected:
        std::thread _thread;
        std::vector<std::weak_ptr<CListener> > _subscribers;
        std::set<const CListener *> _subscribersSet;

        virtual void run();
};

#endif /* defined(__projectm__CDefaultHandler__) */

编译器版本:

bash-3.1$ g++ --version
g++.exe (GCC) 4.8.1

makefile(一团糟,我知道 - 还在学习这个该死的东西):

CC=g++

OUTFILE=game

BINDIR=bin
SRCDIR=src
OBJDIR=obj

CFLAGS=
LDFLAGS=-std=c++0x



all: core

# Ядро проекта.
core: $(OBJDIR)/main.o $(OBJDIR)/CGame.o $(OBJDIR)/CHandler.o $(OBJDIR)/CListener.o
    $(CC) $(CFLAGS) $(wildcard $(OBJDIR)/*.o) -o $(BINDIR)/$(OUTFILE)

$(OBJDIR)/main.o: $(OBJDIR)
    $(CC) $(LDFLAGS) $(SRCDIR)/main.cpp -c -o $(OBJDIR)/main.o

$(OBJDIR)/CGame.o: $(OBJDIR)
    $(CC) $(LDFLAGS) $(SRCDIR)/CGame.cpp -c -o $(OBJDIR)/CGame.o

$(OBJDIR)/CHandler.o: $(OBJDIR)
    $(CC) $(LDFLAGS) $(SRCDIR)/CHandler.cpp -c -o $(OBJDIR)/CHandler.o

$(OBJDIR)/CListener.o: $(OBJDIR)
    $(CC) $(LDFLAGS) $(SRCDIR)/CListener.cpp -c -o $(OBJDIR)/CListener.o

# Создаем директорию для объектов, если ее нет.
$(OBJDIR):
    mkdir $(OBJDIR)

main.o: $(SRC)/main.cpp

【问题讨论】:

  • std::thread 是一个类。我不明白你的问题。
  • @BjörnPollex,对不起,我的意思是说“没有命名类型”。尝试将线程对象存储在变量中不起作用。
  • 它是一种类型。类是一种类型。
  • @juanchopanza,我想,但编译器仍然抛出非类型错误。
  • 这不是问题,但是包含两个连续下划线 (__projectm__CHandler__) 的名称和以下划线后跟大写字母的名称保留给实现。不要使用它们。

标签: c++ multithreading c++11


【解决方案1】:

使用std::thread 作为一个简单的局部变量的问题之一是它不是异常安全的。我承认我自己在演示小的 HelloWorlds 时经常犯这个错误。

不过,最好能准确地知道你要做什么,所以这里有一个关于使用std::thread 的异常安全方面的更详细说明:

#include <iostream>
#include <thread>

void f() {}
void g() {throw 1;}

int
main()
{
    try
    {
        std::thread t1{f};
        g();
        t1.join();
    }
    catch (...)
    {
        std::cout << "unexpected exception caught\n";
    }
}

在上面的示例中,我有一个“大型”程序,它“偶尔”会引发异常。通常我想在异常冒泡到main 之前捕获并处理它们。然而,作为最后的手段,main 本身被包裹在一个 try-catch-all 中。在这个例子中,我只是简单地打印出发生了非常糟糕的事情并退出。在一个更现实的例子中,您可以让您的客户有机会节省工作、释放内存或磁盘空间、启动一个不同的进程来提交错误报告等。

看起来不错,对吧?可惜错了。当你运行它时,输出是:

libc++abi.dylib: terminating
Abort trap: 6

在从main 正常返回之前,我没有通知我的客户出现问题。我期待这个输出:

unexpected exception caught

而不是 std::terminate() 被调用。

为什么?

事实证明,~thread() 看起来像这样:

thread::~thread()
{
    if (joinable())
        terminate();
}

所以当g() 抛出时,t1.~thread() 在堆栈展开期间运行,而t1.join() 不会被调用。因此t1.~thread() 调用std::terminate()

别问我为什么。这是一个很长的故事,我缺乏客观公正地讲述它。

无论如何,你必须了解这种行为,并加以防范。

一种可能的解决方案是回到包装设计,可能使用 OP 首次提出并在其他答案中警告不要使用的私有继承:

class CHandler
    : private std::thread
{
public:
    using std::thread::thread;
    CHandler() = default;
    CHandler(CHandler&&) = default;
    CHandler& operator=(CHandler&&) = default;
    ~CHandler()
    {
        if (joinable())
            join();  // or detach() if you prefer
    }
    CHandler(std::thread t) : std::thread(std::move(t)) {}

    using std::thread::join;
    using std::thread::detach;
    using std::thread::joinable;
    using std::thread::get_id;
    using std::thread::hardware_concurrency;

    void swap(CHandler& x) {std::thread::swap(x);}
};

inline void swap(CHandler& x, CHandler& y) {x.swap(y);}

目的是创建一个新类型,比如CHandler,它的行为就像std::thread,除了它的析构函数。 ~CHandler() 应该在其析构函数中调用 join()detach()。我在上面选择了join()。现在可以在我的示例代码中简单地将CHandler 替换为std::thread

int
main()
{
    try
    {
        CHandler t1{f};
        g();
        t1.join();
    }
    catch (...)
    {
        std::cout << "unexpected exception caught\n";
    }
}

现在的输出是:

unexpected exception caught

如预期的那样。

为什么在~CHandler() 中选择join() 而不是detach()

如果您使用join(),则主线程的堆栈展开将阻塞,直到f() 完成。这可能是您想要的,也可能不是。我不能为你回答这个问题。只有您可以为您的应用程序决定这个设计问题。考虑:

// simulate a long running thread
void f() {std::this_thread::sleep_for(std::chrono::minutes(10));}

main() 线程在g() 下仍然会抛出异常,但现在它会在展开时挂起,并且仅在 10 分钟后打印出来:

unexpected exception caught

然后退出。也许是因为f() 中使用的引用或资源,这就是您需要发生的事情。但如果不是,那么您可以:

    ~CHandler()
    {
        if (joinable())
            detach();
    }

然后您的程序将立即输出“意外捕获异常”并返回,即使f() 仍在忙于处理(在main() 返回后f() 将作为应用程序正常关闭的一部分被强制取消)。

也许您的某些线程需要join()-on-unwinding,而其他线程需要detach()-on-unwinding。也许这会引导您使用两个类似CHandler 的包装器,或者一个基于策略的包装器。委员会无法就解决方案达成共识,因此您必须决定什么适合您,或者接受terminate()

这直接使用了std::thread 非常非常低级的行为。对 Hello World 来说还可以,但在真正的应用程序中,最好封装在中级处理程序中,通过私有继承或作为私有数据成员。好消息是,在 C++11 中,现在可以可移植地编写中级处理程序(在 std::thread 之上),而不是像 C++98/ 中所必需的那样写入操作系统或第三方库。 03.

【讨论】:

    【解决方案2】:

    建议不要std::thread 继承:反正它没有virtual 方法。我什至建议不要使用组合。

    std::thread 的主要问题是它会在构建后立即启动一个线程(除非您使用其默认构造函数)。因此,许多情况都充满危险:

    // BAD: Inheritance
    class Derived: std::thread {
    public:
        Derived(): std::thread(&Derived::go, this), _message("Hello, World!") {}
    
        void go() const { std::cout << _message << std::endl; }
    
    private:
        std::string _message;
    };
    

    线程可能会在_message 构建之前执行go,从而导致数据争用。

    // BAD: First Attribute
    class FirstAttribute {
    public:
        FirstAttribute(): _thread(&Derived::go, this), _message("Hello, World!") {}
    
        void go() const { std::cout << _message << std::endl; }
    
    private:
        std::thread _thread;
        std::string _message;
    };
    

    同样的问题,线程可能会在_message 构建之前执行go,从而导致数据争用。

    // BAD: Composition
    class Safer {
    public:
        virtual void go() const = 0;
    
    protected:
        Safer(): _thread(&Derived::go, this) {}
    
    private:
        std::thread _thread;
    };
    
    class Derived: Safer {
        virtual void go() const { std::cout << "Hello, World!\n"; }
    };
    

    同样的问题,线程可能会在Derived 构建之前执行go,从而导致数据争用。


    如您所见,无论是继承还是组合,都非常容易在不知不觉中引发数据竞争。使用std::thread 作为类的last 属性 会起作用...如果你能确保没有人从这个类派生。

    因此,在我看来,现在建议只使用std::thread 作为局部变量。请注意,如果您使用async 设施,您甚至不必自己管理std::thread

    【讨论】:

      【解决方案3】:

      Bjarne Stroustrup 在他的C++11 FAQ 中显示some examples of using std::thread。最简单的示例如下所示:

      #include<thread>
      
      void f();
      
      struct F {
          void operator()();
      };
      
      int main()
      {
          std::thread t1{f};  // f() executes in separate thread
          std::thread t2{F()};    // F()() executes in separate thread
      }
      

      一般来说,std::thread 不打算继承自。在构造函数中传递一个异步执行的函数。

      如果您的编译器不支持std::thread,您可以改用Boost.Thread。它是fairly compatible,因此一旦您的编译器支持它,您就可以将其替换为std::thread

      【讨论】:

      • 这看起来不错,但声明 std::thread _thread; 会返回 error: 'thread' in namespace 'std' does not name a type
      • 这可能意味着你有一些包含搞砸了。如果没有完整的源代码,就不可能诊断出错误。您是否尝试过编译我发布的示例?如果它有效,那么你知道它不是你的编译器。您可能应该阅读一下 C++ 中包含的工作原理,因为它们与 Java 中的包导入完全不同。
      • 好问题。我将尝试编译该示例,并查看线程本身是否有效。
      • 不,它不起作用。 g++ -std=c++0x -o test test.cpp \\ test.cpp: In function 'int main()': \\ test.cpp:11:5: error: 'thread' is not a member of 'std'
      • 没有什么比临时解决方案更持久的了。但我不认为 Boost 作为传统的线程解决方案会成为问题。我会试一试。感谢您的帮助。
      【解决方案4】:

      首先,您使用的是什么编译器和编译器版本? std::thread 是相当新的,直到最近才在其中一些中实现。那可能是你的问题。

      第二个是你

      #include <thread> 
      

      第三(这不是你的直接问题)这不是如何在 c++ 中使用线程。你不继承它,你创建一个实例传递你希望它运行的函数。

      std::thread mythread = std::thread(my_func);
      

      (虽然您可以传递的不仅仅是一个简单的函数)

      【讨论】:

      • 我通过自定义 makefile 使用 mingw 包中的 g++。 LDFLAGS=-std=c++0x。包含线程没有帮助。简单地声明 std::thread mythread 仍然会引发错误。
      • @MaximKumpan 你用的是哪个版本的g++?
      • @PeterT,向问题添加了 g++ 版本。
      • 这就是问题所在。据我所知,mingw 目前不支持 Windows 上的 std::thread 。最近肯定没有。 windows上当前版本的visual c++可以。
      • 不回答这个问题,但这里有一些 cmet programmers.stackexchange.com/questions/195639/…
      【解决方案5】:

      确保在编译和链接时使用:

      g++ -std=c++11 your_file.cpp -o your_program 
      

      弄乱 LDFLAGS 只会帮助链接,而不是编译。

      【讨论】:

      • 没有冰,将-std=c++11 添加到 CFLAGS 没有帮助。
      • 对于 linux 中的 g++ (v4.9.2),我需要添加两个标志来支持 和 std::thread 程序:-std=c++11 -pthread
      猜你喜欢
      • 1970-01-01
      • 2019-06-07
      • 2015-09-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-11-11
      • 1970-01-01
      • 2015-07-29
      相关资源
      最近更新 更多