std :: thread management:用法和最佳实践

时间:2013-10-17 09:18:55

标签: c++ multithreading c++11

我试图在Java中使用它们之后弄清楚线程并且我有点困惑。 两个问题:

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

任何正确方向的产品都将受到高度赞赏。

如何解释此消息?

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

5 个答案:

答案 0 :(得分:8)

建议继承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;
};

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

// 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;
};

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

// 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"; }
};

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


正如您所看到的,无论您是继承还是撰写,都很容易在不知不觉中导致数据竞争。使用std::thread作为类的最后一个属性将有效...如果您可以确保没有人从此类派生。

因此,对我来说,建议仅使用std::thread作为局部变量似乎更好。请注意,如果您使用async工具,则您甚至不必自己管理std::thread

答案 1 :(得分:8)

使用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

按预期。

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

如果使用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 :(得分:4)

Bjarne Stroustrupsome examples of using std::thread中显示C++11 FAQ。最简单的例子如下:

#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替换它。

答案 3 :(得分:0)

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

其次你是

#include <thread> 

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

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

(虽然你可以传递更多的功能)

答案 4 :(得分:0)

确保编译和链接时使用:

g++ -std=c++11 your_file.cpp -o your_program 
弄乱LDFLAGS只会帮助链接,而不是编译。

相关问题