隐式类型转换未定义

1
2
3
4
5
6
7
8
9
void f(int i, std::string const& s);

void oops(int param)
{
char buffer[1024];
sprintf(buffer, "%i", param);
std::thread t(f, 3, buffer);
t.detach();
}

在这种情况下,正是局部变量 buffer 的指针被传递给新线程,还有一个重要的时机,即函数 oops 会在缓冲在新线程上呗转换为 std::string 之前退出,从而导致未定义的行为。解决之道是在将缓冲传递给 std::thread 的构造函数之前转换为 std::string

1
2
3
// 先转换,避免悬浮指针
std::thread t(f, 3, std::string(buffer));
t.detach();

引用对象错误

也有可能的得到相反的情况,对象被复制,而你想要的是引用。这可能发生在当线程正在更新一个通过引用传递来的数据结构时,例如:

1
2
3
4
5
6
7
8
9
10
void updateWidgetData(Widget w, Widget& data); // 引用

void oops(Widget w)
{
WidgetData data;
std::thread t(updateWidgetData, w, data); // 不知道需要引用,会先复制参数data,让复制后的变量被引用
displayStatus();
t.join();
processWidgetData(data);
}

尽管 updateWidgetData 希望通过引用传递第二个参数,但 std::thread 的构造函数却并不知道;它无视函数所期望的类型,并且盲目地复制了所提供的值。实际修改的是 data 在线程内部的副本的应用,随着线程的销毁,这些改动都将被舍弃。最后调用 processWidgetData,将会传递一个未改变的 data

对于熟悉 std::bind 的人来说,解决方案也是显而易见的,需要用 std::ref 来包装确实需要被引用的参数。

1
std::thread t(updateWidgetData, w, std::ref(data);

新线程调用成员函数

你可以传递一个成员函数的指针作为函数,前提是提供一个合适的对象指针作为第一个参数。

1
2
3
4
5
6
7
8
class X
{
public:
void doWork();
}

X x;
std::thread t(&X::doWork, &x);

这段代码将在新线程上调用 x.doWork(),因为 x 的地址是作为对象指针提供的。你也可以提供参数给这样的成员函数调用:std::thread 构造函数的第三个参数将作为成员函数的第一个参数等等。

转移std::unique_ptr对象所有权

一个有趣的场景是,参数不能被复制但只能被移动:一个对象内保存的数据被转移到另一个对象,使原来的对象变成“空壳”。这种类型的一个例子是 std::unique_ptr。移动构造函数和移动赋值运算符允许一个对象的所有权在 std::unique_ptr 实例之间进行转移,这种转移给源对象留下一个 NULL 指针。

在源对象是临时的场合,这种移动是自动的;但在源是一个命名值的地方,此转移必须直接通过调用 std::move 来请求。

1
2
3
4
5
6
void processObject(std::unique_ptr<Object>);

std::unique_ptr<Object> p(newObject);
p->prepareData(42);

std::thread t(processObject, std::move(p));

这种所有权可以在实例之间进行转移,因为 std::thread 的实例是可移动的,即使他们不是可复制的。这确保了在允许程序员选择在对象之间转换所有权的时候,在任意时刻只有一个对象与某个特定的执行线程相关联。

笔记摘录自:《C++并发实战》Anthony Williams,章节2.2