Boost.thread 同步锁详解

来源:互联网 发布:淘宝宝贝详情图片素材 编辑:程序博客网 时间:2024/06/05 06:10

参考 

http://patmusing.blog.163.com/blog/static/1358349602010183920489/ 




标准C++线程即将来临,而且将会从Boost Threads发展而来,研究一下由Boost Threads作者写的这篇文章吧。

 

重要更新

Boost Threads更新方面的情况,请看Anthony Williams的文章What's New in Boost Threads?,他是Boost Threads维护人员,该文发表于Dr. Dobb’s Journal200811月份的刊物上。

 

就在几年前,写一个多线程的程序还是不太常见的事情[1],而今天互联网服务器应用已经可以运行多线程应用,高效地处理多客户端的链接。为了提高吞吐量,交易服务器用独立的线程提供服务,GUI应用程序也将耗时的操作分离开来,以使得界面本身能够更加及时地做出反应,等等。

 

C++标准并未提及线程,这就让程序员们感到疑惑:写多线程的C++程序到底是否可行?尽管不能写符合标准的多线程程序[2],程序员们还是可以通过操作系统提供的库函数来写多线程程序,如果那个操作系统支持线程的话。但这样做至少存在两个问题:这些库几乎全部是用C写的,因此在C++中使用需要格外小心;另外,每个操作系统都仅仅提供自己的多线程处理方面的支持。由此,最终的代码既非标准的,也不可移植。Boost.Threads正是为了解决这两个问题而设计的。

 

Boost是一个由C++标准委员会库函数工作组相关成员发起的一个组织,旨在为C++开发出新的代码库。目前大约有2000名成员。在Boost代码发布版本中可以找到许多有用的库,Boost.Threads的出现,就是为了让这些库更加线程安全。

 

许多C++专家对Boost.Threads的设计提出了宝贵意见。所有接口都是重新设计,而非简单地将C线程的API进行封装。许多C++特性(诸如构造函数/析构函数,函数对象和模板等)被广泛采用,使得接口更加灵活。当前版本支持POSIXWin32Macintosh Carbon等平台。

 

线程创建

boost::thread类代表一个执行线程(a thread of execution),就像std::fstream类代表一个文件一样。缺省构造函数创建一个代表当前执行线程的一个实例。重载构造函数则有一个函数对象[3]作为参数,该函数对象没有实参(argument),也无返回值。重载的构造函数创建一个新的线程,然后调用函数对象。

 

初看起来,这种设计不如用典型的C方法来创建线程那么有用,因为典型的C方法创建线程,可以向新线程调用的函数传入一个void指针,而这个指针用来传递数据。不过,由于Boost.Threads采用了函数对象,而不是函数指针,因此函数对象携带线程要用到的数据是完全可以的。这种方法更加灵活,而且类型安全。如果再和相关的函数库结合起来,比如Boost.Bind,这种方式可以让你轻松地将任意多的数据传递给新创建的线程。

 

目前,我们还不能对用Boost.Threads创建的线程对象做太多的运算。实际上,只可以执行两种操作。一、线程对象可以用==!=来比较两者是否相等或不相等,用以判断两个对象是否指向同一个线程;二、调用boost::thread::join以等待线程结束。其他一些库,或许允许你做另外一些操作(例如,设置或者清除线程的优先级等)。因为这些操作要映射到一个可移植的库界面中并不是一件容易的事情,决定如何将此类操作加入到Boost.Threads中的研究工作,目前正在进行当中。

 

列表1说明了boost::thread类的一个非常简单的用法。其中创建了一个新的线程,该线程简单地向std::cout输出“Hello World”,然后main线程等待它的结束。

#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);                  // 译注:hello前面的&符号,可要可不要

         thrd.join();

 

         return 0;

}

列表1

 

互斥体(Mutexes)

写过多线程程序的人都知道,不能让多个线程同时访问共享的资源是至关重要的。如果一个线程试图改变共享数据的值,而另外一个线程试图去读取该共享数据的值,结果将是未定义的。为了阻止这样的事情发生,需要用到一些特殊的原始数据类型和操作。其中最重的一个就是总所周知的mutex(“mutual exclusion”的缩写。译注:相互排斥的意思,经常被翻译为互斥体”)mutex在同一时间只能允许一个线程访问共享资源。当一个线程需要访问共享资源时,它必须先锁住”mutex,如果任何其他线程已经锁住了mutex,那么本操作将会一直被阻塞,直到锁住了mutex的线程解锁,这就保证了共享资源,在同一时间,只有一个线程可以访问。

 

mutex的概念有几个变种。Boost.Threads支持两大类型的mutex:简单mutex和递归mutex。一个简单的mutex只能被锁住一次,如果同一线程试图两次锁定mutex,将会产生死锁。对于递归mutex,一个线程可以多次锁定一个mutex,但必须以同样的次数对mutex进行解锁,否则其他线程将无法锁定该mutex

 

在上述两大类mutex的基础上,一个线程如何锁定一个mutex也有些不同变化。一个线程有3种可能方法来锁定mutex

1. 等待并试图对mutex加锁,直到没有其他线程锁定mutex

2. 试图对mutex加锁,并立即返回,如果其他线程锁定了mutex

3. 等待并试图对mutex加锁,直到没有其他线程锁定mutex或者直到规定的时间已过。

 

看起来最好的mutex类型是递归的mutex了,因为上述3种加锁的方式它都支持。不过,不同的加锁方式有不同的消耗,因此对于特定的应用,Boost.Threads允许你挑选最有效率的mutex。为此,Boost.Threads提供了6中类型的mutex,效率由高到低排列:boost::mutexboost::try_mutexboost::timed_mutexboost::recursive_mutexboost::recursive_try_mutexboost::recursive_timed_mutex

 

如果一个线程锁定一个mutex后,而没有解锁,就会发生死锁,这也是最为常见的错误了,为此,Boost.Threads专门进行了设计,可不直接对mutex加锁或者解锁操作,以使这种错误不可能发生(或至少很难发生)。取而代之地,mutex类定义了内嵌的typedef来实现RAII(Resource Acquisition In Initialization,译注:在初始化时资源获取)[4]用以对一个mutex进行加锁或者解锁,这就是所谓的Scoped Lock模式。要构建一个这种类型的锁,需要传递一个mutex引用,构造函数将锁定mutex,析构函数将解锁mutexC++语言规范确保了析构函数总是会被调用,所以即使有异常抛出,mutex也会被正确地解锁。

 

这种模式确保了mutex的正确使用。不过必须清楚,尽管Scoped Lock模式保证了mutex被正确解锁,但它不能保证在有异常抛出的时候,所有共享资源任然处于有效的状态,所以,就像进行单线程编程一样,必须确保异常不会让程序处于不一致的状态。同时,锁对象不能传递给另外一个线程,因为他们所维护的状态不会受到此种用法的保护。

 

列表2举例说明了boost::mutex类的一个简单的用法。其中两个线程被创建,每个循环10次,将id和当前循环计数输出到std::coutmain线程等待着两个线程结束。std::cout对象是一个共享资源,所以每个线程均使用全局mutex,以确保在同一时刻,只有一个线程输出到它。

#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;

}

列表2

 

也许你已经注意到在列表2的代码中,需要手工写一个函数对象,才能向线程传递数据。尽管代码很简单,但每次都要写这样的代码也会让人有单调沉闷之感。有另外一种更容易的解决办法,Functional库可以让你通过将需要传入的数据绑定到另外一个函数对象的方式,来创建一个新的函数对象。列表3展示了Boost.Bind库如何不写函数对象,而简化列表2中的代码。

// This program is identical to listing2.cpp except that it uses

// Boost.Bind to simplify the creation of a thread that takes data.

#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;

}

列表3

 

条件变量

有时候仅仅锁定一个共享资源去使用它还是不够的。共享资源在被使用之前,有时候它必须处在某种特殊的状态。例如,一个线程有可能试图从一个栈里面取数据,如果栈中没有数据的话,它要等待新的数据的到来。mutex处理这种同步问题就显得力不从心了。另外一种同步方式,即所谓的条件变量,正好适用于这种情形。

 

条件变量总是和mutex、共享资源联合使用。线程首先锁定mutex,然后验证共享资源是否处于一种可以被安全使用的状态,如果没有处在所需要的状态,那么线程将等待条件变量。这个操作会导致在等待的过程中,mutex被解锁,从而让另外一个线程可以改变共享资源的状态。线程从等待操作返回时,mutex将肯定是被锁定的。如果另外一个线程改变了共享资源的状态,它必须通知其他正在等待条件变量的线程,从而可以使他们从等待操作中返回。

#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)

                   {

                            // 译注:下面是块语句,或者叫复合语句,规定了lock的作用域,可以将它们看做是一条语句

                            {

                                     boost::mutex::scoped_lock lock(io_mutex);

                                     std::cout << "Buffer is full. Waiting..." << std::endl;

                            }

                            // 译注:由于scoped_lock使用了RAII,程序运行到此,已经超出了lock的作用域,因此

                            //       其析构函数会被自动调用,而在其析构函数中又调用了io_mutexunlock,故而

                            //       程序运行到此,io_mutex已经被解锁了。

                            while (full == BUF_SIZE)   // 如果buffer满了

                                     cond.wait(lock);          // 导致lock解锁,并阻塞当前线程,直到被notify_one()

                   }                                                       // notify_all()唤醒,才会解除阻塞,并重新锁定lock

                  

                   // 如果缓冲区没有满

                   buf[p] = m;                                     // 想缓冲区加入数据,p为缓冲区的位置变量

                   p = (p+1) % BUF_SIZE;    

                   ++full;                                             // full加,full为缓冲区中元素的数目

                   cond.notify_one();                       // 唤醒由于调用wait而被阻塞的所有线程中的一个

         }

 

         int get()

         {

                   scoped_lock lk(mutex);

                   if (full == 0)

                   {

                            // 译注:下面是块语句,或者叫复合语句,规定了lock的作用域,可以将它们看做是一条语句

                            {

                                     boost::mutex::scoped_lock lock(io_mutex);

                                     std::cout << "Buffer is empty. Waiting..." << std::endl;

                            }

                            // 译注:由于scoped_lock使用了RAII,程序运行到此,已经超出了lock的作用域,因此

                            //       其析构函数会被自动调用,而在其析构函数中又调用了io_mutexunlock,故而

                            //       程序运行到此,io_mutex已经被解锁了。

 

                            while (full == 0)           // 如果buffer中已经没有数据了

                                     cond.wait(lk);      // 导致lk解锁,并阻塞当前线程,直到被notify_one()

                   }                                              // notify_all()唤醒,才会解除阻塞,并重新锁定lk

 

                   int i = buf[c];                          // 取出缓冲区中的一个数据

                   c = (c+1) % BUF_SIZE;              

                   --full;                                      // full减,full为缓冲区中元素的数目

                  cond.notify_one();                // 唤醒由于调用wait而被阻塞的所有线程中的一个

 

                   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;

}

列表4[5]

 

列表4举例说明了boost::condition类的一个比较简单的应用。所定义的类实现了一个大小固定的FIFO容器 – 一个大小固定的缓冲区,通过使用boost::mutex,缓冲区内部是线程安全的。其中的putget方法使用了条件变量,这可以保证线程等待缓冲区处于有效状态时完成相关操作。例子中创建了2个线程,一个线程将100个整数放入缓冲区,另一个线程则将这些数据取出,由于缓冲区一次只能保存10个整数,因此两个线程需要周期性地等待彼此的操作完成。为了验证,putget方法将诊断信息输出到std::cout上。最后,main线程等待着两个线程结束。

 

 

线程本地存储

大部分函数都被实现成不可重新进入。在调用该函数时,如果已有其他线程正在调用该函数,那么这意味着不安全。持有被连续访问的静态数据或者返回一个指向静态数据的指针这样的函数,都是不可重新进入函数 (non-reentrant function)。例如,std::strtok就是一个不可重复进入函数,因为它使用静态数据来保存要被用标记(token)分割的字符串。

 

有两种办法可以将不可重复进入函数转换成可重复进入函数。一种方法就是改变函数的接口,让函数接收一个某种数据类型的指针或者引用作为参数,用以取代前面提及的静态数据。例如,POSIX定义了strtok_r,就是std::strtok函数的可重复进入版本的变体,它接收了一个额外的char**参数,该参数替代了原来的静态数据。这种解决办法很简单,而且提供了最可能好的性能。不过,改变了公共接口,就潜在地意味着要改动很多代码;另外一种办法就是保持函数的公共接口不变,用线程本地存储(有时也称为线程有关存储,thread-specific storage)来取代静态数据。

 

线程本地存储和某个特定的线程(当前线程)相关联。多线程库提供了访问线程本地存储的接口,该接口可以访问当前线程实例的数据。对于该数据,每个线程拥有其自身的实例,因此并发访问不会存在任何问题。不过,访问线程本地存储要比访问静态数据或者本地数据慢,因此它永远也不会是最好的解决方案,但在不能改变公共接口的前提下,它是唯一可行的解决方案。

 

Boost.Threads 通过智能指针boost::thread_specific_ptr提供访问线程本地存储的办法。每个线程第一次试图访问这个智能指针的一个实例时,其初始值是NULL,因此必须要有相关的代码检查这种情形,并且在第一次使用时初始化该指针。Boost.Threads库确保在线程退出时,会清理存储在线程本地存储中的数据。

 

#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;

}

列表5

 

列表5给出了一个简单的如何使用boost::thread_specific_ptr的例子。其中新创建了两个线程来初始化线程本地存储,然后循环10次,增加智能指针所指的整数,并输出到std::cout(用一个mutex进行了同步,因为它是一个共享资源)。然后main线程等待这两个线程结束。本例的输出结果清楚地表明,尽管它们使用了同一个boost::thread_specific_ptr,每个线程均仅处理属于自己的数据实例。

 

单次执行函数(Once Routines)

还剩下一个问题需要处理:怎样使得初始化函数(比如构造函数)线程安全?比如,某个应用要将某个对象的全局实例创建为单态(singleton[6])。由于存在初始化顺序问题,一个函数用来返回一个静态实例,并保证静态实例是在方法第一次被调用的时候创建的。现在的问题就是,如果有多个线程同时调用该函数,那么构建静态实例的构造函数也会被同时调用多次,这样就会产生灾难性的后果。[7]

 

这个问题的解决办法就是所谓的单次执行函数。一个单次执行函数仅被调用一次。如果多个线程试图同时调用该函数,只有其中一个线程能够真正调用它,而其他的线程均处于等待状态,直到该线程结束对那个函数的调用。为了保证只执行一次,该函数必须是被另外一个函数间接调用,即向另外一个函数传入该函数的函数指针,和一个用于检查该函数是否已经被调用过的特殊标志的引用。这个标志使用静态初始化的方式进行初始化,这确保它是在编译时而非运行时初始化的。因此,它不受限于多线程初始化的问题。Boost.Thread通过boost::call_once来提供调用单次执行函数的办法的,同时也定义了一个标志类型boost::once_flag和一个特殊的用于静态初始化标志的宏BOOST_ONCE_INIT

#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;

}

列表6

 

列表6举例说明了boost::call_once的简单用法。一个全局的整型变量被静态地初始化为0boost_flag也被静态地用BOOST_ONCE_INIT初始化。main启动两个线程,向boost::call_once传入了一个用来将该全局整型变量加1的函数的指针,这两个线程都试图通过调用boost::call_once初始化那个全局整形变量。然后main等待这两个线程结束,并将结果输出到std::cout。输出结果表明,该函数确实仅被调用了一次,因为该整型变量的值仅仅为1

 

Boost.Threads的未来

有几个新的特性被计划加入Boost::Threads,包括boost::read_write_mutex,它允许多个线程同时读取共享资源,但只允许一个线程排他性地写共享资源[8]。还有boost::thread_barrier,它使一组线程处于等待状态,直到所有线程进入障碍(barrier)状态。此外,boost::thread_pool也被计划加入,它允许每次在无需创建和销毁线程的情况下,异步地执行一些短小的函数(short routines)

 

Boost.Threads已经被提交到C++标准委员会函数库工作组,以期被纳入标准的下一个函数库技术报告中,并最终成为C++标准的一部分。标准委员当然也会考虑其他的线程库,不过,他们看好初次展示的Boost.Threads,并且他们也颇有兴趣向标准加入多线程支持。因此,C++多线程编程,前景一片光明。

 

译者注:

[1]    本文发表于2002

[2]    因为根本没有标准可依

[3]  function object,即函数对象。所谓函数对象,简单地说,就是一个类重载了调用操作符(),该类的对象就是函数对象

[4]    RAII的原理,详见:http://patmusing.blog.163.com/blog/static/13583496020101824142699/

[5]    代码中的注释为译者所加

[6]    单态是设计模式中的概念,如果一个类最多只能有一个实例,那么就可以说这个类是单态的。

[7]    这段文字实际上就是讲在C++中,如何让singleton线程安全。本文中讨论的内容比较简单,复杂的情形请见:

         http://patmusing.blog.163.com/blog/static/135834960201002322226231/

[8]    目前的最新的版本已经支持该功能。在某个线程写的时候,其它线程也不能读,否则就可能读到脏数据;如果没有线程拥有写锁,那么多个线程可以拥有读锁,即多个线程可以读。


0 0
原创粉丝点击