Boost 多线程编程

 

C++标准库没有涉及线程,在C++中,虽然不可能写出标准兼容的多线程程序,程序员可以使用特定操作系统提供的线程库来写出多线程程序来。可是,这至 少导致两个突出的问题:操作系统普遍提供的是C库,在C++中使用要更小心,每个操作系统都有自己的一套支持多线程的库;另外,代码不标准,不可移植。 Boost.Threads 可以解决这些问题。BY firebird32

创建线程

boost::thread 类描述线程的执行。缺省构造器创建一个当前线程的执行的实例。重载构造器调用一个无参也无返回值的函数对象,这个构造器启动一个新线程,其调用函数对象执行线程。

刚 开始好像这样设计不如典型的C实现方式,创建一个线程,一个参数是void指针类型,可传递一个被新线程调用的函数,另一个参数可传递特定的数据到线程。 但是,Boost.Threads 用函数对象代替函数指针,这使得函数对象携带线程需要的数据。这种方法更灵活,并且是线程安全的。对于和函数对象库的组合,比如 Boost.Bind ,这种设计允许你容易地传递任何数量的数据到新创建的线程中。
boost::thread 的 == 和 != 方法用来比较两个线程对象是否在一个线程中;可以调用join()方法等待线程结束。例1是一个使用boost::thread 类的简单例子,创建一个新线程,在新线程中打印"Hello world, I'm a thread!",在main线程中等待这个线程的结束。

例1:
#include <boost/thread/thread.hpp>
#include <iostream>

void hello()
{
std::cout <<
    "Hello world, I'm a thread!"
    << std::endl;
}

int main(int argc, char* argv[])
{
  boost::thread thrd(&hello);
  thrd.join();
  return 0;
}

互斥体

我 们要知道在多线程程序中多个线程同时访问共享资源时如何同步。假如一个线程试图修改共享数据的值,同时另一个线程试图读这个值,结果是不确定的。为防止这 种情况发生,要用专门的原子类型和原子操作。有一种叫做互斥(mutex)。互斥在同一时刻仅允许一个线程访问共享资源。当一个线程要访问共享资源的时 候,它首先要“锁定(lock)”互斥,假如另一个线程先一步锁定了互斥,这个线程要等待直到另一个线程“解除锁定(unlock)”互斥,这就保证了同 时仅有一个线程访问共享资源。

互斥有几个变种。 Boost.Threads 支持两类互斥,简单互斥和递归互斥。简单互斥仅能被锁定一次,假如同一线程试图锁定互斥两次,将造成死锁,导致这个线程永远等待下去。对于递归互斥,单个 线程可以锁定互斥多次,也必须解除锁定相同的次数,这样才能使其他线程锁定互斥。

对于这两种互斥中,一个线程可以通过如下三种途径锁定互斥:
    1.尝试且锁定互斥,等待直到没有其他线程锁定互斥。
    2.尝试且锁定互斥,假如其他线程已经锁定互斥就立即返回。
    3.尝试且锁定互斥,等待直到没有其他线程锁定互斥或直到指定的超时时间到。

Boost.Threads 提供以下六种互斥类型:boost::mutex, boost::try_mutex, boost::timed_mutex, boost::recursive_mutex, boost::recursive_try_mutex, 和 boost::recursive_timed_mutex。

假如一个互斥被锁定,但是没有被解锁,就会导致死锁。这个错误非常普遍,因 此Boost.Threads的设计可以避免这种情况的发生。不直接使用锁定、解锁操作是可行的,使用锁对象,锁对象的构造中锁定,在析构的时候解锁。 C++语言的规则确保析构总是被调用,因此当异常发生时,互斥将被解锁。

虽然这种机制对使用互斥有帮助,但是当异常发生时只能保证互斥被解锁,却可能使共享数据处于无效状态。因此当异常发生时,使数据处于不一致状态使程序员的责任。另外,共享的资源要在几个线程之间共享,确保这些线程都能访问到它。

例2是使用boost::mutex类的简单例子。创建两个线程,循环10次,写ID到std::cout。main线程等待这两个线程结束。std::cout对象是共享资源,因此每个线程使用全局互斥来保证每次只有一个线程写它。

例2:

#include <boost/thread/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <iostream>

boost::mutex io_mutex;

struct count
{
  count(int id) : id(id) { }

  void operator()()
  {
    for (int i = 0; i < 10; ++i)
    {
      boost::mutex::scoped_lock
        lock(io_mutex);
      std::cout << id << ": "
        << i << std::endl;
    }
  }

  int id;
};

int main(int argc, char* argv[])
{
  boost::thread thrd1(count(1));
  boost::thread thrd2(count(2));
  thrd1.join();
  thrd2.join();
  return 0;
}

例3:
// 这个例子和例2一样,除了使用Boost.Bind来简化创建线程携带数据,避免使用函数对象

#include <boost/thread/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/bind.hpp>
#include <iostream>

boost::mutex io_mutex;

void count(int id)
{
  for (int i = 0; i < 10; ++i)
  {
    boost::mutex::scoped_lock
      lock(io_mutex);
    std::cout << id << ": " <<
      i << std::endl;
  }
}

int main(int argc, char* argv[])
{
  boost::thread thrd1(
    boost::bind(&count, 1));
  boost::thread thrd2(
    boost::bind(&count, 2));
  thrd1.join();
  thrd2.join();
  return 0;
}

条件变量

有时候锁定共享资源且使用它满足不了应用。有时共享资源在能够使用前需要满足一些特定状态。例如,某线程试图把数据从栈中取出,要是数据没有到达它需要等待。互斥不能满足这种同步。另一种同步类型,叫做条件变量,能够满足这种情况。

条 件变量总是和互斥、共享资源一起使用。一个线程首先锁定互斥,然后坚持共享资源是否处于特定状态,假如不在所需要的状态,线程在条件变量上等待。在等待期 间这个操作引发互斥解锁,因此另一线程可以改变共享资源的状态。当等待操作结束会确保互斥又被锁定。当另一线程改变了共享资源的状态,其需要通知等待条件 变量的线程,确使等待线程从等待操作返回。

例4是使用boost::condition类的简单例子。

#include <boost/thread/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/thread/condition.hpp>
#include <iostream>

const int BUF_SIZE = 10;
const int ITERS = 100;

boost::mutex io_mutex;

class buffer
{
public:
  typedef boost::mutex::scoped_lock
    scoped_lock;
   
  buffer()
    : p(0), c(0), full(0)
  {
  }
   
  void put(int m)
  {
    scoped_lock lock(mutex);
    if (full == BUF_SIZE)
    {
      {
        boost::mutex::scoped_lock
          lock(io_mutex);
        std::cout <<
          "Buffer is full. Waiting..."
          << std::endl;
      }
      while (full == BUF_SIZE)
        cond.wait(lock);
    }
    buf[p] = m;
    p = (p+1) % BUF_SIZE;
    ++full;
    cond.notify_one();
  }

  int get()
  {
    scoped_lock lk(mutex);
    if (full == 0)
    {
      {
        boost::mutex::scoped_lock
          lock(io_mutex);
        std::cout <<
          "Buffer is empty. Waiting..."
          << std::endl;
      }
      while (full == 0)
        cond.wait(lk);
    }
    int i = buf[c];
    c = (c+1) % BUF_SIZE;
    --full;
    cond.notify_one();
    return i;
  }
   
private:
  boost::mutex mutex;
  boost::condition cond;
  unsigned int p, c, full;
  int buf[BUF_SIZE];
};

buffer buf;

void writer()
{
  for (int n = 0; n < ITERS; ++n)
  {
    {
      boost::mutex::scoped_lock
        lock(io_mutex);
      std::cout << "sending: "
        << n << std::endl;
    }
    buf.put(n);
  }
}

void reader()
{
  for (int x = 0; x < ITERS; ++x)
  {
    int n = buf.get();
    {
      boost::mutex::scoped_lock
        lock(io_mutex);
      std::cout << "received: "
        << n << std::endl;
    }
  }
}
   
int main(int argc, char* argv[])
{
  boost::thread thrd1(&reader);
  boost::thread thrd2(&writer);
  thrd1.join();
  thrd2.join();
  return 0;
}

线程局部存储

许多函数被实现成不可重入。这意味着它是非线程安全的。就是说不能被多个线程同时调用。一个不可重入的函数通常保存静态数据,或返回值指向静态数据。例如,std::strtok是不可重入的,以为它用静态数据保存字符串来分解成标记。

有 两种途径可使一个不可重入函数变成可重入函数。一种途径使修改函数接口,譬如可以传递一个指向数据的指针或引用来代替静态数据。这种方案简单且性能最佳, 但是需要改变公共接口,导致应用代码的大量修改。另一种途径是不改变公共接口,用线程局部存储(有时也叫线程特定存储)代替静态数据。

线 程局部存储时数据和特定线程相关联。多线程库给出存取线程局部存储的接口,可存取当前线程的数据实例。每个线程提供其自己的数据实例,因此对同时存取就不 会有任何问题。但是,线程局部存储比静态数据或局部数据要慢。因此这也不总是最好的方案。但是它是不改变公共接口的唯一方案。

Boost.Threads 通过智能(smart)指针 boost::thread_specific_ptr 来存取线程局部存储。首先每个线程试图获取这个智能指针的一个实例,其有一个 NULL 值,因此第一次使用的时候检查指针要是为 NULL,就需要初始化。Boost.Threads确保线程局部存储的数据在线程退出的时候被清除。

例5是使用 boost::thread_specific_ptr 的简单例子。

例5:

#include <boost/thread/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/thread/tss.hpp>
#include <iostream>

boost::mutex io_mutex;
boost::thread_specific_ptr<int> ptr;

struct count
{
  count(int id) : id(id) { }

  void operator()()
  {
    if (ptr.get() == 0)
      ptr.reset(new int(0));

    for (int i = 0; i < 10; ++i)
    {
      (*ptr)++;
      boost::mutex::scoped_lock
        lock(io_mutex);
      std::cout << id << ": "
        << *ptr << std::endl;
    }
  }

  int id;
};

int main(int argc, char* argv[])
{
  boost::thread thrd1(count(1));
  boost::thread thrd2(count(2));
  thrd1.join();
  thrd2.join();
  return 0;
}

仅运行一次的例程

一 般初始化例程仅执行一次,不能被多次执行,这就需要保证初始化例程是线程安全的,也就是说仅有一个线程能调用执行。这种情况叫做“仅运行一次的例程”。一 个“仅运行一次的例程”在应用程序中只能被调用一次。假如多个线程试图在同时执行这个例程,仅有一个实际能执行,其他都将等待这个线程执行完毕后才返回。 Boost.Threads 通过boost::call_once 函数和boost::once_flag 标志类型和一个特定的宏定义BOOST_ONCE_INIT来支持“仅运行一次的例程”。

例6 展示了 boost::call_once 的简单使用。

例6:


#include <boost/thread/thread.hpp>
#include <boost/thread/once.hpp>
#include <iostream>

int i = 0;
boost::once_flag flag =
  BOOST_ONCE_INIT;

void init()
{
  ++i;
}

void thread()
{
  boost::call_once(&init, flag);
}

int main(int argc, char* argv[])
{
  boost::thread thrd1(&thread);
  boost::thread thrd2(&thread);
  thrd1.join();
  thrd2.join();
  std::cout << i << std::endl;
  return 0;

}

你可能感兴趣的:(thread,多线程,c,IO,存储,buffer)