65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.78/5 (33投票s)

2013年2月5日

CPOL

4分钟阅读

viewsIcon

125996

downloadIcon

1465

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() 类似于 LeaveCriticalSectiontry_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_mutexstd::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_variablestd::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 日:首次发布。

© . All rights reserved.