这篇博客基于上篇的the buffer cache理论篇,本人利用c++11的多线程机制实现了一个demo,并且由于在网上大多数博客针对双向链表的LRU实现并没有过多的考虑同步问题,特做此记录。
想要直接看项目代码,可以直接移步到我的github:https://github.com/HBKO/the_buffer_cache
闲话少说,在这篇博客中你将看到以下内容:
好戏开始:
- 先来展示一下我们的buffer,freelist,hash queue,bufferpool的设计,相关的数据和接口如下类图所示:
第一部分:关键类设计
1.1.
还是逐个展示头文件说明可能出现的问题,以及设计
下面是CBuffer的头文件,说明对于每一块buffer的头部信息应该有那些:
//
// buffer.hpp
// the_bufer_cache
//
// Created by 何柯文 on 2017/10/14.
// Copyright © 2017年 何柯文. All rights reserved.
//
#ifndef buffer_hpp
#define buffer_hpp
#include<string>
#include<mutex>
using std::string;
using std::mutex;
#define BUSY 0
#define FREE 1
#define DELAY_WRITE 2
#define INVALID 3
class CBuffer
{
private:
//暂时不放设备数进去
int block_num; //block相关数字
int status; //该buffer目前状态
string data; //指该buffer指向的数据块
public:
CBuffer(const int block,string str):block_num(block),data(str),status(FREE),
hash_next(NULL),hash_prev(NULL),freelist_next(NULL),freelist_prev(NULL){} //有给予块值和数据字符串的构造函数
CBuffer(const int block):block_num(block),status(FREE),data("empty"),
hash_next(NULL),hash_prev(NULL),freelist_next(NULL),freelist_prev(NULL)
{} //只给予块号的构造函数
//不允许简单的赋值,含有mutex信号量的对象不允许简单赋值
CBuffer(CBuffer const& other)
{
std::lock_guard<std::mutex> lock(other.mux);
data=other.data;
block_num=other.block_num;
status=other.status;
hash_next=other.hash_next;
hash_prev=other.hash_prev;
freelist_next=other.freelist_next;
freelist_prev=other.freelist_prev;
}
// CBuffer& operator=(const CBuffer&) = delete; // 不允许简单的赋值
//简单的获取
int getblock() const {return block_num;}
void setblock(const int block) {block_num=block;}
int getstatus() const {return status;}
void setstatus(const int sta) {status=sta;}
string read() const {return data;} //最初版的read
void write(const string& str){data=str;} //最初版的write
~CBuffer(){} //析构函数,将new出来的内存全都delete掉
public: //可直接接触到的结点
class CBuffer* hash_next; //hash_queue的下一个
class CBuffer* hash_prev; //hash_queue的上一个
class CBuffer* freelist_next; //freelist的下一个
class CBuffer* freelist_prev; //freelist的上一个
mutable mutex mux; //表示进程信号量,加上mutable的目的是为了这个即使是一个常量对象也可以修改这个mux的锁,
};
#endif /* buffer_hpp */
包含内容如下:两个双向链表的指针即hash_queue以及freelist的next,prev,用来连接链表,block_num用来获取是那一块的信息,status表示对应状态,是busy还是free,映射关系显示在上面的宏定义中(在C++中,这个宏可以考虑用const来替换)。还有block的内容,因为是demo,这里就用string字符串来替代。下面的方法就是各种get和set,这个就不提。
但是这里,关于mutex这个锁对象就要重点提一下了。首先,mutex是c++11针对多线程环境下,共享变量的同步问题而设立的一个类。mutex就是同步锁,基本目的就是只允许一个线程拥有这个对象实例,其他线程想要再拥有就会进入睡眠,下面在对双向链表说明的时候还会祥谈。
接下来谈一下这个mutex锁,mutex锁的设计原则就是一个类实例对应一个mutex锁,这就意味着mutex不允许简单赋值,即不允许拷贝构造函数和移动构造函数。因为如果出现简单赋值的话,就会出现两个对象实例同时拥有同一个锁,也就是一个buffer对象实例被拥有的时候,另一个buffer也不允许被拥有,这是明显不合逻辑的。于是,在多线程编程中,mutex锁和thread线程类都不允许简单赋值(浅拷贝,只是将类成员变量一个个赋值,c++primer中也提到对于指针进行浅拷贝会出现重复使用同一个内存空间的问题,但是这个不用浅拷贝的理由有点不一样)。
有心的读者肯定也注意到了,我在mutex锁前面加了一个mutable易变的,事实上,如果对mutex不说明是mutable数据是无法编译过的,错误的报错是没有对变量进行初始化。要解决这个问题,先来了解一下mutable.
mutable 易变的,与它相反的关键词是const。大家了解的更多肯定是const不变的,即你不能对声明为const的变量进行改变变量值(除了通过指针访问改变)。那么,mutable的意思就是即使你声明这个类或者这个结构体是常量(const)对象或者结构体,这个用mutable声明的变量也可以进行改变。
可是,为什么mutex要设计mutable呢?因为mutex的使用,常常要调用其方法lock(),unlock()对mutex进行修改。所以,即使用常量对象,其成员的mutex也要能够随时改变,所以声明为mutable。
由于不允许简单拷贝和赋值,我们只能自己重载一个拷贝构造函数,拷贝函数中我直接不对mux锁进行操作:
//不允许简单的赋值,含有mutex信号量的对象不允许简单赋值
CBuffer(CBuffer const& other)
{
std::lock_guard<std::mutex> lock(other.mux);
data=other.data;
block_num=other.block_num;
status=other.status;
hash_next=other.hash_next;
hash_prev=other.hash_prev;
freelist_next=other.freelist_next;
freelist_prev=other.freelist_prev;
}
1.2 DoublyList的设计
#ifndef DoublyLink_hpp
#define DoublyLink_hpp
#define BUFFERHEAD -1
#include<string>
#include<mutex>
#include<iostream>
#include"buffer.hpp"
const std::string status[4]={"BUSY","FREE","DELAY_WRITE","INVALID"}; //存储string数组的四种状态
class DoublyLink
{
protected:
class CBuffer* header; //队列头
class CBuffer* tail; //队列尾
mutable std::mutex m; //用来对链表的操作进行保护,加入mutable表示易变的,就不会出现未初始化的问题
public:
DoublyLink(); //带有模数的构造函数
DoublyLink(const int block); //模数和block数的构造函数
class CBuffer* getbuffer(const int block) const; //根据block数来寻找对应的buffer,如果找不到就返回NULL
// 尽量类成员中像get这类获取内容的函数,用const修饰,即不能修改得到的内容,但是得到的内容如果是指针,则可以修改指针指向的内容
bool deletebuffer(const int block); //根据block数,对指定结点进行删除,0,1判断是否成功
bool deletebuffer(class CBuffer *q); //直接根据传入的结点直接删除,省去遍历链表的过程
int addbuffer(class CBuffer *buf); //根据传入的结点,添加结点,返回值为1说明正常添加,返回值为-1,说明该结点已经存在,
//返回值为0,说明传入的空结点
~DoublyLink();
};
作为双向链表的基类,主要的数据成员也就是head(头),tail(尾),主要实现的功能也就是增,删,改,查,至于如何实现,放在下面环节具体说明可能出现的问题。
1.3 FreeList的设计
#ifndef FreeList_hpp
#define FreeList_hpp
#include <iostream>
#include "DoublyLink.hpp"
class FreeList:public DoublyLink
{
private:
mutable std::mutex fm;
public:
FreeList();
CBuffer* alloc(); //提供申请一个结点的功能,没有就返回NULL
int addfreebuffer(CBuffer* buf,int isfirst); //提供从链表头添加还是从链表尾添加的功能
bool removefreebuffer(CBuffer* q); //提供删除结点的功能
void printallfreenode(); //打印所有空闲结点
~FreeList();
};
针对于FreeList,由于要实现LRU(最近最早访问),利用双向链表来实现LRU的主要有两种思路:
思路一:从头申请,从尾回收,这样使得不常使用的buffer能够最早被申请出去获取其他进程使用。也就是文中所使用的设计。
思路二:每当freelist里面的某个节点(buffer)被使用过后,直接添加到链表头,然后freelist如果过长,从尾删除。
这两种方式都能实现LRU,但是这两个方式的适用场景不同。思路一是使用freelist的长度没有受限,只是作为维护free的buffer的手段,思路二是用在freelist的总长度受限,你必须把多余的结点删除。
1.4
BufferPool的设计,具体头文件如下所示:
#ifndef BufferPool_hpp
#define BufferPool_hpp
#include <string>
#include <condition_variable>
#include "HashQueue.hpp"
#include "FreeList.hpp"
#include <vector>
#include <iostream>
#include <sstream>
class BufferPool
{
private:
std::vector<HashQueue* > Queue_temp; //存储四个hash_queue的vector
FreeList* freelist; //一个freelist,存储空闲的列表(LRU)
HashQueue* hashqueue_1; //存储链表的hashqueue_1
HashQueue* hashqueue_2; //hashqueue_2
HashQueue* hashqueue_3; //hashqueue_3
HashQueue* hashqueue_4; //hashqueue_4
int num; //链表中要存储的buffer数量
std::condition_variable data_cond; //表示等待事件的变量
mutable std::mutex mut;
public:
/* block_num表示要创建多少块buffer,这个函数的功能:
1.创建buffer.2.将buffer插入到hash_queue中。3.遍历hash_queue将空闲的buffer增加到freelist中
*/
BufferPool(int block_num);
bool Brelse(CBuffer* buf); //释放一个buf的资源
CBuffer* getblk(const int block); //根据block号来申请对应的buffer
CBuffer* bread(const int block_number); //将buf内的内容读取出来
void bwrite(CBuffer* buf); //将buf内的内容写入
void readcontext(); //用于测试用,输出所有节点的block值
~BufferPool(); //析构函数,主要把申请的buffer全都释放掉
};
BufferPool的设计重点就是在getblk和Brelse这两个函数上,数据成员有4个hash_queue(这里可能用7个hash_queue更好),一个freelist来管理申请出来的buffer, condition_variable表示控制同步问题的一个条件变量(即遇到什么样的条件进行解锁,唤醒线程)。
第一部分设计篇结束。
第二部分关键函数设计
2.1
DoublyLink(双向链表的增,删,改,查)
双向链表的增,删主要在于要分类,对于删除一个结点来说,删除的位置要分类讨论,删除的时候只有一个结点,还是有多个结点,对于多个结点的情况,还要考虑该结点是头结点,尾结点,还是都不是。下面是代码表示:
//直接根据传入的结点直接删除,省去遍历链表的过程
bool DoublyLink::deletebuffer(class CBuffer* q)
{
//利用mutex进行保护链表的操作
std::lock_guard<std::mutex> l(m);
if(q==NULL)
{
return false;
}
else
{
//只有一个结点的情况
if(tail==header)
{
tail=header=NULL;
}
//不只有一个结点
else
{
//该结点为头结点
if(q==header)
{
q->hash_next->hash_prev=NULL;
header=q->hash_next;
q->hash_next=NULL;
}
//该结点为尾结点
else if(q==tail)
{
q->hash_prev->hash_next=NULL;
tail=q->hash_prev;
q->hash_prev=NULL;
}
//该结点不为头结点也不为尾结点
else{
q->hash_prev->hash_next=q->hash_next;
q->hash_next->hash_prev=q->hash_prev;
q->hash_prev=q->hash_next=NULL;
}
}
}
return true;
}
没有谈论同步问题的双向链表都是耍流氓,对于双向链表这类不变量,一旦其中一个指针被破环就会导致整个链表被破坏,这不是我们想看到的。但是在多线程的环境下,如果两个线程通过访问同一个链表并对链表进行操作,在没有锁的保护下就很容易破环链表,请看下面的例子(例子来自the_design_of_the_unix_operating_system,例子中的上下文切换针对于系统中的进程,这里用线程表示):
上面的第一个图说明的是对一个链表添加一个结点的操作,在最后一步之前出现了上下文切换(这里可以理解为切换线程),结果表示为上面一个链表,在这个时候发生切换乍看之下好像没什么问题,但如果在切换到另一个线程的时候执行的是对bp1后面的结点的删除呢?会出现什么情况,先放上删除一个结点的链表代码:
在另一个线程执行完对bp1后面的结点删除的操作得到的链表结果如下所示:
链表明显被破坏了,这时候如果再执行bp1->forp->backp=bp1,就会出现非法内存访问了。
为了防止多线程对同一个链表进行操作 ,解决办法就是加锁,上面提到了c++11提供了各种加锁的工具。
最简单的加锁方式就是:
std::mutex mux;
mux.lock();
do_something();
mux.unlock();
但是这种加锁方式,需要在每次函数返回的时候都要mux.unlock(),这是相当麻烦的。更重要的是,如果线程因为异常的原因而出来,无法走到mux.unlock()处,锁就永远无法归还,想要得到这个资源的线程就会straving,这不是我们想看到的。庆幸的是,c++11提供了lock_guard<> ,这样一个模版类对mutex进行封装,如果函数返回或者程序中途退出,自动调用lock_guard的析构函数,进行unlock操作,我们就不用担心何处解锁的问题啦,具体的代码就是我上面的代码所写:
//利用mutex进行保护链表的操作
std::lock_guard<std::mutex> l(m);
当然C++11提供各种锁,比如想要更加灵活的锁(比如想进行在线程等待的时候解锁互斥量,并在这之后对互斥量继续上锁)就要使用unique_guard。
那么,上了锁是不是就万事大吉了呢?
答案当然不是的,锁多了,一方面是造成性能的问题(线程都在睡眠,谁来进行操作)
关于性能问题:这边要谈到关于锁的颗粒范围的问题,就是一个锁应该锁住的范围的问题。还是对上面的双向链表为例。其实,我们没必要在一个线程访问的时候对整个链表上锁,我们很多时候只需要锁住操作结点的前后两个结点,即一共三个结点就可以避免大部分的破坏不变量问题,这样将锁的颗粒将会减少,即减少性能的损失以及减少对应的竞争条件。
另一方面更重要的一点,容易造成死锁的问题。
还是以上面的双向链表为例,将锁的颗粒减少之后,一次只需要获取三个结点的锁就能保证链表不会被破环。那么,这时候如果有一个线程从head开始向尾遍历,另一个线程从tail开始向头遍历,这样会出现什么问题。就是在两个线程达到中间的时候,如果有两个buffer A和B,就会出现线程1手握A 想要B,线程2手握B想要A的经典死锁问题,这就是要求线程的访问必须要有顺序可以作为减少死锁问题的一个手段。不过,我们可以基于mutex实现一个层次锁,相应的代码如下:
hierarchical_mutex high_level_mutex(10000); // 1 hierarchical_mutex low_level_mutex(5000); // 2
int do_low_level_stuff();
int low_level_func() {
std::lock_guard<hierarchical_mutex> lk(low_level_mutex); // 3
return do_low_level_stuff(); }
void high_level_stuff(int some_param);
void high_level_func() {
std::lock_guard<hierarchical_mutex> lk(high_level_mutex); // 4
high_level_stuff(low_level_func()); // 5 }
void thread_a() { high_level_func(); }
// 6
hierarchical_mutex other_mutex(100); // 7 void do_other_stuff();
void other_stuff() {
high_level_func();
// 8
do_other_stuff(); }
void thread_b() // 9 {
std::lock_guard<hierarchical_mutex> lk(other_mutex); // 10
other_stuff(); }
不过,最简单解决死锁问题的办法还是尽可能的让线程只能拥有一把锁,也就是减少锁的数量。
c++11还提供了各种锁,想了解相关锁的问题,可以参考材料
Cpp_Concurrency_In_Action
下面依照同步锁的操作,很容易就写出添加结点和遍历链表的函数
下面是DoublyLink的添加结点的代码(在尾添加,添加前要先查看链表中是否已经有该数字,因为不能有两个相同block_number的buffer,因为查询操作也有锁,如果把锁加在开始,会出现两次上锁的情况,所以在addbuffer的时候把锁加在查询操作的后面)
//根据传入的结点,添加结点,返回值为1说明正常添加,返回值为-1,说明该结点已经存在,
//返回值为0,说明传入的空结点,添加结点直接添加在hash queue 尾巴
int DoublyLink::addbuffer(class CBuffer *buf)
{
//传入的buf为空,返回0
if(buf==NULL)
{
cout<<"the buf is NULL!"<<endl;
return 0;
}
//判断添加的结点是否存在
if(getbuffer(buf->getblock())!=NULL)
{
cout<<"the buf is exist!"<<endl;
return -1;
}
//利用mutex进行保护链表的操作
std::lock_guard<std::mutex> l(m);
if(header==NULL)
header=buf;
else
{
tail->hash_next=buf;
buf->hash_prev=tail;
}
//buf成为尾巴
tail=buf;
return 1;
}
下面是DoublyLink查询结点的函数
class CBuffer* DoublyLink::getbuffer(const int block) const
{
//利用mutex进行保护链表的操作
std::lock_guard<std::mutex> l(m);
//如果是一个空的双向链表进行处理
if(header==NULL)
{
return NULL;
}
class CBuffer* buffer=this->header;
//遍历寻找结点
while(buffer!=NULL)
{
if(buffer->getblock()==block)
{
return buffer;
}
else
{
buffer=buffer->hash_next;
}
}
//到达最终结点,查看寻找的是否是尾巴结点
if(buffer==tail && buffer->getblock()!=block)
{
return NULL;
}
else
{
return buffer;
}
}
2.3 bufferpool的关键函数设计
bufferpool的两个关键函数在于getblk和brelse,这两个函数的伪代码在上一篇理论篇给出,这里我就直接上实际代码。
这个是getblk的代码:
//根据block号来申请对应的buffer,返回NULL说明程序出错了。
CBuffer* BufferPool::getblk(const int block)
{
int flag_1=0; //该flag表示对应的buffer是否寻找到,0表示未寻找到
while(!flag_1)
{
CBuffer* buf=Queue_temp[block%4]->getbuffer(block);
if(buf!=NULL)
{
if((buf->getstatus())==BUSY) //情况五
{
std::unique_lock<std::mutex> lk(mut); //使用unique_lock建立互斥锁,对应取的锁应该是buffer的锁
data_cond.wait(lk,[&buf]{return buf->getstatus()==FREE;}); //对于lambdas函数表达式,要么将其设置成全局变量(全局函数),或者通过[]里面的值传入
lk.unlock();
continue;
}
buf->setstatus(BUSY); //情况一
freelist->removefreebuffer(buf);
return buf;
}
else // block not on hash queue //
{
CBuffer* freebuf=freelist->alloc();
if(freebuf==NULL) //在freelist里面已经没有可以申请出来的块了,情况四
{
std::unique_lock<std::mutex> bk(mut);
data_cond.wait(bk,[this]{return freelist->alloc()!=NULL;}); //对于想要在lambda表达式中传对象成员(函数或者变量,需要把this指针传进去)
bk.unlock();
continue;
}
freelist->removefreebuffer(freebuf);
if(freebuf->getstatus()==DELAY_WRITE) //情况三
{
bwrite(buf); //进行延迟写的操作
continue;
}
//情况二,找到了一个空闲的buffer
//将buf从旧的hash_queue中移除
int old_block=freebuf->getblock();
Queue_temp[old_block%4]->deletebuffer(old_block);
freebuf->setblock(block);
//将buf添加到新的hash_queue中
Queue_temp[block%4]->addbuffer(freebuf);
// flag_1=1;
return freebuf;
}
}
return NULL;
}
这个是brelse函数
//对对应的block的进行释放
bool BufferPool::Brelse(CBuffer* buf)
{
buf->setstatus(FREE);
//将使用完的buffer添加到freelist的尾巴,实现LRU
freelist->addfreebuffer(buf, 0);
data_cond.notify_all();
return true;
}
为什么把两个函数结合在一起讲呢,是因为这两个函数涉及到同步问题中的等待一个事件或者其他条件。
首先,先讲一下BufferPool如何实现LRU,对于getblk遇到的情况大体上是两个类,一个是在hashqueue里面找到了buffer,另一个是没有找到,需要在freelist申请一块出来。那么,每次申请的时候,freelist直接把链表头申请出来。最后在brelse释放内存的时候,直接把释放的buffer添加到freelist的尾巴。实现了,最不常使用的buffer最优先被申请出来的效果。
接下来,另一个问题就是情况四和情况五了,情况四因为在向freelist申请buffer的时候,发现freelist是空的。这个时候就要等待其他线程归还buffer,让freelist不为空。这时候,条件变量就有用了,请看下面的几行代码:
if(freebuf==NULL) //在freelist里面已经没有可以申请出来的块了,情况四
{
std::unique_lock<std::mutex> bk(mut);
data_cond.wait(bk,[this]{return freelist->alloc()!=NULL;}); //对于想要在lambda表达式中传对象成员(函数或者变量,需要把this指针传进去)
bk.unlock();
continue;
}
bool BufferPool::Brelse(CBuffer* buf)
{
buf->setstatus(FREE);
//将使用完的buffer添加到freelist的尾巴,实现LRU
freelist->addfreebuffer(buf, 0);
data_cond.notify_all();
return true;
}
上面当出现情况四的时候,先对bufferpool进行上锁,然后调用条件变量data_cond的wait方法,wait方法传递的是一个锁,一个lambda函数表达式,这个lambda相当于是一个匿名函数,简单来说就是直接执行这个函数表达式后面的
return freelist->alloc()!=NULL;
测试freelist是否不为空这个判断语句,但如果想要向这个lambda函数表达式传参的话,需要往[]这个框框里面写参数,如果你要调用的参数是全局变量则不需要传参。但是,如果不是全局变量都需要传参,特别是你如果在一个类中调用了这个lambda表达式,想要调用类成员的话,还是需要传递一个this指针进去,即[this]{lambda表达式}。
那么,这个语句的意思就是当执行data_cond.wait()的时候,如果lambda表达式为false,这个时候wait函数解锁互斥量,该线程进入休眠。当有其他线程调用brelse函数。返回一块buffer为free时候,并调用notify_all()(通知全部线程,notify_one()通知一个线程)该线程会再次调用wait,检查lambda表达式的条件,如果符合(freelist中有可用的buffer)就从休眠状态苏醒,并重新获得互斥量。
关于lambda判别式的问题,你当然可以传入一个更加复杂的函数进行判断。但是实际上,当线程重新检查互斥量并不是直接响应另一个线程的通知,这个就称作伪唤醒。由于伪唤醒的次数和频率都不确定,所以可以会多次调用wait里面的函数。因此,尽量少在wait的判别式中添加对程序有副作用的函数或者步骤。
至于情况五,也就是当在hashqueue中找到的buffer发觉是忙碌的,这个时候就要等待其他线程用完了这个buffer让它变成free,所使用的期望条件和上面那个data_cond.wait()同理,这里就不赘述了。
第二部分关键函数设计结束
第三部分:多线程测试
well, well。我们谈了这么多同步问题。那么,我们该怎么验证我们上面说的锁,条件变量是否正确呢?那当然就是测试啦,一谈到多线程测试,我们首先就要先把线程给建起来。
3.1
c++11自带的thread库也使得建线程相当容易,只需要执行以下代码:
std::thread t2(thread03,10);
std::thread t3(thread04,10);
std::thread t1(func);
这个是截取我的头文件测试函数的一段。很简单,建立线程就只需一行代码:
std::thread t2(传入函数(可以是函数对象或者函数),函数参数)
但如果这里要传入一个函数对象的话,可能就会引发c++里面”令人头疼的语法解析”问题,详细可参考wiki:
https://en.wikipedia.org/wiki/Most_vexing_parse
这里摘自《cpp concurrency in action》 一段代码:
class background_task
{
public:
void operator()() const
{
do_something();
do_something_else();
}
};
std::thread my_thread(background_task());
下面这段:
std::thread my_thread(background_task());
就会被C++编译器解析成一个函数声明,这个函数是一个返回值为std::thread 名字叫mythread 含有一个参数的函数指针,这个函数的返回值为background_task,没有参数的函数。
避免这个问题的方法如下所示:(用多组括号,或者用新统一的初始化语法)
std::thread my_thread((background_task())); // 1
std::thread my_thread{background_task()}; // 2
3.2 如何向线程函数传递参数
一共有两种方式:
1.利用thread的构造函数直接传参,具体体现在:
std::thread my_thread(执行的函数,函数参数1,函数参数2,函数参数3)
3.3
接下来,要说明的就是线程控制权的转移。大家可以看到,每当我们建立一个thread的时候,我们都会实例化thread类。那么,很容易能理解到一个thread实例类对应一个线程,那么就代表了thread不允许拷贝构造函数以及赋值构造函数,也就是不能让同一个线程的控制权在两个thread实例类的手里。但是,一个线程的控制权可以转移,也就c++11新加的移动构造函数,利用std::move可以将线程的控制权进行转移:
void some_function();
void some_other_function();
std::thread t1(some_function);
std::thread t2=std::move(t1);
t1=std::thread(some_other_function);
std::thread t3;
t3=std::move(t2);
t1=std::move(t3);
如果一个thread实例已经拥有一个线程,这时候再通过移动构造函数转移线程的话,就会把原来的线程结束掉,然后换新线程(本质就是替换线程)。
3.4
前台线程和后台线程
前台线程可以简单理解为,主线程(main函数开始的那个线程)必须等待其它线程结束之后,才能继续线程。
代码表示就是为:
t2.join();
t3.join();
t2,t3表示为两个线程实例,即为两个线程。
在主函数中放置。
后台线程就是,主线程不用等待其它线程结束,就相当于这几个线程各干各的,互不等待,对应的代码就是:
t2.detach();
t3.detach();
t2,t3表示为两个线程实例,即为两个线程。
但是后台线程可能会有潜在的问题,如果在主线程声明了一个局部变量,那么可能主线程结束之后,新线程还在执行,但是主线程的局部变量已经被释放掉了,那么新线程就会出现非法访问的情况。
具体的错误代码如下所示:
3.5 如何最大可能的触发竞争条件
我们想要测试the_buffer_cache的可能触发的同步问题主要有三:
1. 双向链表因为多线程访问而被破坏。
2. 如果freelist空的话,要等待其它线程释放出free的buffer出来
3. 想要的block处于busy状态,要等待其它线程用完这个block
对于这三种情况,我们应该设立三种测试样例:
1. 建两个线程,每个线程都在不停的add结点然后马上删除结点,这样就会造成两个线程交替对链表进行删除和添加的操作,看最后链表是否为空。
//定义多线程函数
void thread01(int num)
{
vector<CBuffer> res;
for(int i=0;i<num;++i)
{
string str="the"+int2str(i)+"th buffer in the thread01";
CBuffer test((i),str);
res.push_back(test);
}
for(int i=0;i<num;++i)
{
link_2.addbuffer(&(res[i]));
freelist.addfreebuffer(&(res[i]), 1);
CBuffer* showbuffer=link_2.getbuffer(i);
CBuffer* showbuffer_2=freelist.alloc();
if(showbuffer!=NULL)
cout<<showbuffer->read()<<endl;
if(showbuffer_2!=NULL)
cout<<showbuffer_2->read()<<endl;
link_2.deletebuffer(showbuffer);
freelist.removefreebuffer(showbuffer_2);
}
}
- 设定bufferpool的buffer数为30,刚开始申请掉29块,剩下一块,然后建两到三个线程,不断的申请和释放buffer,相当于几个线程不断的抢占剩下的最后一块buffer,最后bufferpool还剩一块buffer为free.
3.还是设定bufferpool的buffer数为30,刚开始申请掉28块,然后一个线程来申请这28块中10个buffer的使用权,另一个线程不断释放这28个buffer,最后bufferpool中的buffer全为free。
//开第三个线程也一直申请hash_queue中没有的buffer
void thread04(int num)
{
vector<CBuffer *> res;
for(int i=0;i<num;++i)
{
CBuffer* test=pool.bread(i+40);
res.push_back(test);
cout<<"alloc the block is: "<<i+40<<endl;
pool.bwrite(test);
}
}
//开第四个线程申请
void thread05(int num)
{
for(int i=0;i<num;++i)
{
CBuffer* test=pool.bread(i);
bufblock.push_back(test);
}
}
//开第五个线程进行释放,用来测试第五种情况
void thread06()
{
for(auto k:bufblock)
{
pool.bwrite(k);
}
}
int main(int argc, const char * argv[]) {
// insert code here...
myfun func(29);
thread05(29);
pool.readcontext();
std::thread t2(thread03,10);
std::thread t3(thread04,10);
std::thread t1(func);
// std::thread t4(thread05,28);
t2.join();
t3.join();
std::thread t4(thread06);
t4.join();
t1.join();
// t4.join();
pool.readcontext();
return 0;
}
具体的代码由于关联性强,我直接放在了github上
项目代码源:https://github.com/HBKO/the_buffer_cache
(之后会添加一个makefile,因为之前的使用xcode编写的项目)
参考书目:《the_design_of_unix_operating_system》
《C++ Concurrency in Action》
《C++ Primer Plus》