C++ 11 线程:让您的(多任务)生活更轻松。






4.78/5 (33投票s)
C++ 11 线程。
介绍
本文旨在帮助经验丰富的 Win32 程序员理解 C++ 11 线程和同步对象与 Win32 线程和同步对象之间的区别和相似之处。
在 Win32 中,所有同步对象句柄都是全局句柄。它们可以共享,甚至可以在进程之间复制。在 C++ 11 中,所有同步对象都是栈对象,这意味着它们必须被“分离”(如果支持分离)才能被栈帧析构。如果您不分离许多对象,它们将撤销其操作,并可能破坏您的计划(如果您是 Win32 程序员,则您的计划是“全局句柄”导向的)。
所有 C++ 11 同步对象都有一个 native_handle() 成员,它返回实现特定的句柄(在 Win32 中是 HANDLE)。
在我所有的示例中,我都提供了 Win32 伪代码。玩得开心!
背景颜色
0x00000000。也就是说,什么都没有。我也是 C++ 11 线程的新手。您需要了解 Win32 同步的方方面面。这不是一个关于正确同步技术的教程,而是一个关于 C++11 实现您心中已有的计划的机制的快速介绍。
简洁使之完美
简单的例子:启动一个线程,然后等待它完成。
void foo() { } void func() { std::thread t(foo); // Starts. Equal to CreateThread. t.join(); // Equal to WaitForSingleObject to the thread handle. }
与 Win32 线程不同的是,在这里您可以拥有参数。
void foo(int x,int y) { // x = 4, y = 5. } void func() { std::thread t(foo,4,5); // Acceptable. t.join(); }
通过将隐藏的 'this' 指针传递给 std::thread,可以轻松地将成员函数用作线程。
如果 std::thread 被析构并且您没有调用 join(),它将调用 abort。要让线程在没有 C++ 包装器的情况下运行:
void foo() { } void func() { std::thread t(foo); t.detach(); // C++ object detached from Win32 object. Now t.join() would throw std::system_error(). }
除了 join() 和 detach(),还有 joinable()、get_id()、sleep_for()、sleep_until()。它们的使用应该是不言自明的。
使用互斥体
std::mutex 类似于 Win32 的临界区。lock() 类似于 EnterCriticalSection,unlock() 类似于 LeaveCriticalSection,try_lock() 类似于 TryEnterCriticalSection。
std::mutex m; int j = 0; void foo() { m.lock(); j++; m.unlock(); } void func() { std::thread t1(foo); std::thread t2(foo); t1.join(); t2.join(); // j = 2; }
和以前一样,您必须在锁定 std::mutex 后解锁它,并且如果您已经锁定了一个 std::mutex,则不能再次锁定它。这与 Win32 不同,在 Win32 中,当您已经在临界区内时,EnterCriticalSection 不会失败,而是会增加一个计数器。
嘿,别走。还有 std::recursive_mutex(谁发明的这些名字?)它就像一个临界区一样工作:
std::recursive_mutex m; void foo() { m.lock(); m.lock(); // now valid j++; m.unlock(); m.unlock(); // don't forget! }
除了这些类之外,还有 std::timed_mutex 和 std::recursive_timed_mutex,它们也提供了 try_lock_for/ try_lock_until。这些允许您等待锁直到特定的超时或特定时间。
线程局部存储
与 TLS 类似,此功能允许您使用 thread_local 修饰符声明一个全局变量。这意味着每个线程都有自己的变量实例,并且有一个通用的全局名称。再次考虑之前的示例
int j = 0; void foo() { m.lock(); j++; m.unlock(); } void func() { j = 0; std::thread t1(foo); std::thread t2(foo); t1.join(); t2.join(); // j = 2; }
但现在看看这个:
thread_local int j = 0; void foo() { m.lock(); j++; // j is now 1, no matter the thread. j is local to this thread. m.unlock(); } void func() { j = 0; std::thread t1(foo); std::thread t2(foo); t1.join(); t2.join(); // j still 0. The other "j"s were local to the threads }
Visual Studio 尚不支持线程本地存储。
神秘变量
条件变量是使线程等待特定条件的对象。在 Windows 中,这些对象是用户模式的,不能与其他进程共享。在 Windows 中,条件变量与临界区相关联以获取或释放锁。 std::condition_variable 与 std::mutex 相关联是出于同样的原因。
std::condition_variable c; std::mutex mu; // We use a mutex rather than a recursive_mutex because the lock has to be acquired only and exactly once. void foo5() { std::unique_lock lock(mu); // Lock the mutex c.notify_one(); // WakeConditionVariable. It also releases the unique lock } void func5() { std::unique_lock lock(mu); // Lock the mutex std::thread t1(foo5); c.wait(lock); // Equal to SleepConditionVariableCS. This unlocks the mutex mu and allows foo5 to lock it t1.join(); }
这个看起来并不像它看起来那么无害。c.wait() 即使在未调用 c.notify_one() 的情况下也可能返回(这种情况称为 **虚假唤醒** - http://msdn.microsoft.com/en-us/library/windows/desktop/ms686301(v=vs.85).aspx)。通常,您会将 c.wait() 放在一个 while 循环中,该循环还检查一个外部变量以验证通知。
条件变量仅在 Vista 或更高版本中受支持。
承诺未来
考虑这种情况。您希望一个线程执行一些工作并返回一个结果。与此同时,您希望执行其他工作,这些工作可能需要一些时间,也可能不需要。您希望在某个时候得到另一个线程的结果。
在 Win32 中,您会这样做:
- 使用 CreateThread() 启动线程。
- 在线程内部,执行工作并在准备好时设置一个事件,同时将结果存储到全局变量中。
- 在主代码中,执行其他工作,然后当您想要结果时调用 WaitForSingleObject。
在 C++ 11 中,这可以通过使用 std::future 来轻松完成,并且由于它是模板,因此可以返回任何类型。
int GetMyAnswer() { return 10; } int main() { std::future<int> GetAnAnswer = std::async(GetMyAnswer); // GetMyAnswer starts background execution int answer = GetAnAnswer.get(); // answer = 10; // If GetMyAnswer has finished, this call returns immediately. // If not, it waits for the thread to finish. }
您还拥有 std::promise。此对象可以提供 std::future 稍后会请求的内容。如果您在向 promise 中添加任何内容之前调用 std::future::get(),get 会等待直到 promised 的值可用。如果调用了 std::promise::set_exception(),std::future::get() 会抛出该异常。如果 std::promise 被销毁并且您调用了 std::future::get(),您将获得一个 broken_promise 异常。
std::promise<int> sex; void foo() { // do stuff sex.set_value(1); // After this call, future::get() will return this value. sex.set_exception(std::make_exception_ptr(std::runtime_error("broken_condom"))); // After this call, future::get() will throw this exception } int main() { future<int> makesex = sex.get_future(); std::thread t(foo); // do stuff try { makesex.get(); hurray(); } catch(...) { // She dumped us :( } }
代码
所附的 CPP 文件包含了我们到目前为止所说的一切,可以在 Visual Studio 12 中使用 2012 年 11 月的 CTP 编译器进行编译(不包括 TLS 机制)。
接下来呢?
还有很多值得包含的内容,例如
- 信号量
- 命名对象
- 跨进程共享对象。
- [...]
您应该怎么做?总的来说,在编写新代码时,如果标准库对您来说足够了,就首选标准库。对于现有代码,我会保留我的 Win32 调用,当我需要将它们移植到另一个平台时,我将使用 C++ 11 函数来实现 CreateThread、SetEvent 等。
祝你好运。
历史
- 2013 年 2 月 5 日:首次发布。