本文的主要参考资料为 BOOST线程完全攻略 - 基础篇 这里主要是对其中的例程进行学习,相关说明还请参考原文。
1 创建一个简单的多线程示例
在boost中创建一个 boost::thread
类的对象就代表一个可执行的线程。该类的定义在boost/thread/thread.hpp
中,最简单的使用方式是直接传递给其一个函数对象或函数指针,创建thread后会自动新建一个线程并立刻执行函数对象或指针的()
操作。boost::thread
类提供了join
方法来等待线程对象的结束。
下面这个示例演示了线程的基本创建方法,并且主程序在等待线程结束之后再退出,为了方便观察这里还加上一个额外的等待语句。
#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();
int i;
std::cin>>i;
return 0;
}
2 临界资源的互斥访问
所谓的临界资源就是那些每次只允许一个线程访问的资源,硬件的临界资源有打印机等,软件的临界资源有缓冲区等。标准输出流中的缓冲区就是一个临界资源,对应的线程中访问临界资源的代码我们就称之为临界区,如cout<<"Hello!"<<endl;
这段代码就是临界区,它向标准输出流的缓冲区中写入一段字符并用endl来刷新缓冲区。很显然如果有多个线程都同时向标准输出设备的缓冲区中写数据,那么打印出来的结果有可能是无效的。这时候就需要互斥锁来保证临界资源的互斥访问了。一般在访问临界区之前都要上锁,退出临界区后再解锁。
互斥锁根据所需的功能不同,执行效率有所区别。boost::mutex
就是一种简单高效的互斥锁类型。为了保证互斥锁总是能够解锁,boost将加锁和解锁的动作分别封装到一个类的构造函数和析构函数中,由于类再离开其作用域或抛出异常后总是能执行其析构函数,所有不会出现死锁的事情,这个封装的类就是boost::mutex::scoped_lock
。互斥锁类定义在boost/thread/mutex.hpp
中。下例就是一个对cout
互斥访问的例子:
#include <boost/thread/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <iostream>
class MyCout
{
private:
int Id;
static boost::mutex IoMutex;
public:
MyCout(int id):Id(id){}
void operator()()
{
for(int i = 0;i < 5;++i)
{
boost::mutex::scoped_lock lock(IoMutex);
std::cout<<"线程"<<Id<<": "<<i<<std::endl;
}
}
};
boost::mutex MyCout::IoMutex;
int main(int argc,char* argv[])
{
boost::thread thrd1(MyCout(1));
boost::thread thrd2(MyCout(2));
thrd1.join();
thrd2.join();
int i;
std::cin>>i;
return 0;
}
上例定义的函数对象MyCout
中有两个成员变量,一个是用来表示线程的ID,另一个为静态成员的互斥锁用来保证所有通过该函数类所产生的函数对象都会互斥的使用临界资源。注意静态成员必须要在类的外部进行定义,在类内部只是进行了声明。
3 使用boost::bind给函数绑定数据
前面使用函数对象来执行线程,使得其不但能保存程序而且还能保存相关的数据。我们使用boost::bind
将函数和数据绑定到一起也能实现相同的功能,该类的定义位于boost/bind.hpp
之中。
#include <boost/thread/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/bind.hpp>
#include <iostream>
boost::mutex IoMutex;
void MyCout(int id)
{
for(int i = 0;i < 5;++i)
{
boost::mutex::scoped_lock lock(IoMutex);
std::cout<<"线程"<<id<<": "<<i<<std::endl;
}
}
int main(int argc,char* argv[])
{
boost::thread thrd1(boost::bind(&MyCout,1));
boost::thread thrd2(boost::bind(&MyCout,2));
thrd1.join();
thrd2.join();
int i;
std::cin>>i;
return 0;
}
4 条件变量
有时候仅仅是保证临界资源的互斥方式是不能满足我们的要求的,比如对于一个缓冲区操作的线程,除了保证缓冲区的互斥访问之外,对于写入的线程还必须要保证缓冲区不满,对于从缓冲区中读取的线程还必须要保证缓冲区非空。当然我们可以使用一个变量来判断缓冲区是否可操作,对于读取缓冲区我们可用判断DataNum != 0
来进行操作。但是这样做会增加程序的判断逻辑使程序结构变得复杂。boost提供了这种等待条件变量的机制。在代码(GetData线程)进入临界区前,首先锁住互斥变量,然后检验临界资源是否可用,如果不可用(缓冲区为空),那么线程就会等待(调用wait
方法),等待的过程中会解锁互斥量以便其他线程能够运行以改变临界资源的状态,同时也会阻塞当前线程防止当前线程和其他线程同时访问临界区。如果某个线程(PutData线程)改变了临界资源使其对该线程来说可用了,那么在那个线程(PutData线程)运行完毕后应该调用notify_one
来取消wait
方法阻塞的一个线程,在从wait
方法返回之前,wait
会再次给互斥量加锁。
下面这个示例是一个使用条件变量的例子,其中全局互斥量IoMutex
和之前的例子一样,使用来保证标准输出流的缓冲区进行互斥访问。而类中的互斥量就是用来保证类中的方法是互斥的使用的。条件变量有多种这里我们使用的是condition
,这个类型是condition_variable_any
的别名,定义在boost/condition.hpp
中。
#include <boost/thread/thread.hpp>
#include <boost/thread/condition.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/bind.hpp>
#include <iostream>
typedef boost::mutex::scoped_lock scoped_lock;
boost::mutex IoMutex;
const int BufferSize = 10;
const int Iters = 100;
class Buffer
{
public:
Buffer():P(0),C(0),DataNum(0){}
void Write(int m)
{
scoped_lock lock(Mutex);
if(DataNum == BufferSize)
{
{
scoped_lock ioLock(IoMutex);
std::cout<<"Buffer is full.Waiting..."<<std::endl;
}
while(DataNum == BufferSize)
Cond.wait(lock);
}
Buf[P] = m;
P = (P+1)%BufferSize;
++DataNum;
Cond.notify_one();
}
int Read()
{
scoped_lock lock(Mutex);
if(DataNum == 0)
{
{
scoped_lock ioLock(IoMutex);
std::cout<<"Buffer is empty.Waiting..."<<std::endl;
}
while(DataNum == 0)
Cond.wait(lock);
}
int i = Buf[C];
C= (C+1)%BufferSize;
--DataNum;
Cond.notify_one();
return i;
}
private:
int Buf[BufferSize];
int P;
int C;
int DataNum;
boost::mutex Mutex;
boost::condition Cond;
};
Buffer buf;
void Writer()
{
for(int n = 0;n < Iters;++n)
{
{
scoped_lock ioLock(IoMutex);
std::cout<<"sending:"<<n<<std::endl;
}
buf.Write(n);
}
}
void Reader()
{
for(int i = 0;i < Iters;++i)
{
int n = buf.Read();
{
scoped_lock ioLock(IoMutex);
std::cout<<"received:"<<n<<std::endl;
}
}
}
int main(int argc,char* argv[])
{
boost::thread thrd1(&Writer);
boost::thread thrd2(&Reader);
int i;
std::cin>>i;
return 0;
}
5 线程局部存储
一般对于含有静态变量或者返回指向静态数据的指针的函数是不可重入的,即同时只能有一个线程可以使用该函数,如std::strtok
就是不可重入的函数。Boost线程库提供了智能指针boost::thread_spacific_ptr
可以保证对于不同的线程这个指针指向的数据是相互独立的。由于不同线程指向的数据不同,所以每一个线程在使用这个指针之前都必须要对其进行赋值。该指针的定义位于boost/thread/tss.hpp
中,下面是一个使用示例:
#include <boost/thread/thread.hpp>
#include <boost/thread/tss.hpp>
#include <boost/thread/mutex.hpp>
#include <iostream>
boost::mutex IoMutex;
boost::thread_specific_ptr<int> Ptr;
struct MyCout
{
MyCout(int id):Id(id){}
void operator()()
{
if(Ptr.get() == 0)
Ptr.reset(new int(0));
for(int i = 0;i < 5;++i)
{
(*Ptr)++;
boost::mutex::scoped_lock IoLock(IoMutex);
std::cout<<Id<<": "<<*Ptr<<std::endl;
}
}
int Id;
};
int main(int argc,char* argv[])
{
boost::thread thrd1(MyCout(1));
boost::thread thrd2(MyCout(2));
int i;
std::cin>>i;
return 0;
}
# 6 仅运行一次的例程
多线程编程中还存在一个问题,那就是我们有一个函数可能被很多个线程调用,但是我们却只想它执行一次。即如果该函数已经执行过了那么再次调用时并不会执行。我们可用通过手动添加全局标志位的方式来实现。也可以使用`boost::call_once`来实现。`call_once`同样需要借助于一个全局标志位`boost::once_flag`并且需要使用宏`BOOST_ONCE_INIT`来初始化这个标志位。函数和宏定义在`boost/thread/once/hpp`中,下面是一个使用示例:
```c++
#include <boost/thread/thread.hpp>
#include <boost/thread/once.hpp>
#include <iostream>
int GlobalVal = 0; //只想初始化这个变量一次
boost::once_flag Flag= BOOST_ONCE_INIT;
void Init()
{
++GlobalVal;
}
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<<GlobalVal<<std::endl;
int i;
std::cin>>i;
return 0;
}
上面程序执行完毕后,GlobalVal
值为1,说明确实只执行了一遍初始化函数。
参考文章:
关于在类中创建线程的多种方式可以参考:
boost::thread线程创建方式总结