五、银行家算法的分析和实现
问题描述:研究一个银行家如何将总数一定的资金,安全地借给若干个客户,是客户既能满足资金需求,也能使银行家收回本金。
限制条件:
1、每个客户再见款前都必须提前说明所需资金总额;
2、每次借钱都是以一个单位(一万元)进行;
3、客户在拿到一个单位的借款前可能需要等待;
4、银行保证客户的等待时间是有限的(借或不借)。
算法策略:将资金优先借予资金需求较少的客户。(可以尽快归还)
应用场景:操作系统内核中的进程管理;数据库内核中的频繁事务管理。
Qt中的算法实现方案:
使用多线程机制模拟客户和银行;银行优先分配资源给最小需求的客户;当客户资源需求无法满足的时候,收回已分配的资源,强制结束线程(资金安全/异常安全)。
编程示例如下:
#include <QCoreApplication>
#include <QThread>
#include <QMutex>
#include <QDebug>
#include <QList>
class Customer : public QThread
{
protected:
int m_need; //资金总额
volatile int m_current; //每次都从内存中去读取变量的值 不需要编译器优化 目前手上拥有的资金数量
QMutex m_mutex;
void run()
{
bool condition = false;
qDebug() << objectName() << "begin to apply money ...";
do
{
m_mutex.lock();
condition = (m_current < m_need);
m_mutex.unlock();
msleep(10);
}
while ( condition );
qDebug() << objectName() << "end (get enough money)";
}
public:
Customer(int current, int need)
{
m_current = current;
m_need = need;
}
int backMoney()
{
int ret = 0;
m_mutex.lock();
ret = m_current;
m_current = 0;
m_mutex.unlock();
return ret;
}
void addMoney(int m)
{
m_mutex.lock();
m_current += m;
m_mutex.unlock();
}
int current()
{
int ret = 0;
m_mutex.lock();
ret = m_current;
m_mutex.unlock();
return ret;
}
int need()
{
return m_need;
}
};
class Bank : public QThread
{
protected:
QList<Customer*> m_list;
int m_total;
void run()
{
int index = -1;
qDebug() << objectName() << "begin : " << m_total;
do
{
index = -1;
for(int i=0; i<m_list.count(); i++)
{
if( m_list[i]->current() == m_list[i]->need() ) //满足借款后收回
{
qDebug() << objectName() << "take back money from : " << m_list[i]->objectName() << " " << m_list[i]->need();
m_total += m_list[i]->backMoney();
}
}
qDebug() << objectName() << "current: " << m_total; //当前金额
int toGet = 0x00FFFFFF;
for(int i=0; i<m_list.count(); i++) //得到需求最小的m_list[i]
{
if( m_list[i]->isRunning() )
{
int tmp = m_list[i]->need() - m_list[i]->current();
if( toGet > tmp )
{
index = i;
toGet = tmp;
}
}
}
if( index >= 0)
{
if( toGet <= m_total )
{
qDebug() << objectName() << "give money to : " << m_list[index]->objectName();
m_total--;
m_list[index]->addMoney(1);
}
else
{
qDebug() << objectName() << "terminate: " << m_list[index]->objectName();
m_total += m_list[index]->backMoney();
m_list[index]->terminate();
}
}
sleep(1);
}while( index >= 0);
qDebug() << objectName() << " end: " << m_total;
}
public:
Bank(int total)
{
m_total = total;
}
void addCustomer(Customer* customer)
{
m_list.append(customer);
}
};
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
Customer p(4, 8);
Customer q(2, 3);
Customer r(2, 9);
Bank bank(2);
p.setObjectName("P");
q.setObjectName("Q");
r.setObjectName("R");
bank.setObjectName("Bank");
bank.addCustomer(&p);
bank.addCustomer(&q);
bank.addCustomer(&r);
p.start();
q.start();
r.start();
bank.start();
return a.exec();
}
银行家算法常用于资源分配的场合,通过对需求总量、需求量进行比较,安排合适的资源分配顺序,并且通过及时收回,保证资源的安全,策略就是优先选择需求量较少的“客户”,然后及时对“借出去”的资源进行收回,再分配给次一级需求量的“客户”,最大程度保证每个“客户”都能申请到需求的资源、
六、多线程中的信号与槽
线程对象是否可以发射信号(signal)?是否可以定义槽函数(slot)?
QThread类拥有发射信号和定义槽函数的能力。关键信号如下:
void started(): 线程开始运行时发射该信号
void finished():线程完成运行时发射该信号
void terminated():线程被异常终止时发生该信号
如果程序中有多个线程,槽函数是在哪个线程中被执行的?
进程中存在栈空间的概念,与栈数据结构不同,栈空间专门同于函数调用(保存函数参数、局部变量等),每个线程都拥有独立的栈空间(可调用其他函数)。
只要函数体中没有访问临界资源的代码,同一个函数可以被多个线程同时调用,且不会产生任何副作用。
操作系统通过整型标识管理进程和线程:
进程拥有全局唯一的ID值——PID;
线程拥有进程内唯一的ID值——TID;
通过QThread中的关键静态成员函数,可以得到上述两个ID值;
QThread *currentThread();
Qt::HANDLE currentThreadId();
通过实验来实现多线程的信号与槽。
定义一个线程类,其中包含一个信号和一个槽函数:
signals:
void testSignal();
protected slots:
void testSlot();
TestThread::TestThread(QObject *parent) : QThread(parent)
{
connect(this, SIGNAL(testSignal()), this, SLOT(testSlot()));
}
void TestThread::run()
{
qDebug() << "void TestThread::run() -- begin tid" << QThread::currentThreadId();
for(int i=0; i<5; i++)
{
qDebug() << "void TestThread::run" << i << QThread::currentThreadId();
sleep(1);
}
emit testSignal();
//exec();
qDebug() << "void TestThread::run() -- end tid" << QThread::currentThreadId();
}
void TestThread::testSlot()
{
qDebug() << "void TestThread::testSlot tid" << QThread::currentThreadId();
}
定义一个Object类,实现三个槽函数:
void MyObject::getStarted()
{
qDebug() << "void MyObject::getStarted() tid" << QThread::currentThreadId();
}
void MyObject::getFinished()
{
qDebug() << "void MyObject::getFinished() tid" << QThread::currentThreadId();
}
void MyObject::getTerminate()
{
qDebug() << "void MyObject::getTerminate() tid" << QThread::currentThreadId();
}
在主函数中,定义一个线程对象,连接线程信号与槽函数:
QCoreApplication a(argc, argv);
qDebug() << "main() tid = " << QThread::currentThreadId();
TestThread tt;
MyObject m;
//下面连接中的槽函数都在主线程中被调用执行
QObject::connect(&tt, SIGNAL(started()), &m, SLOT(getStarted()));
QObject::connect(&tt, SIGNAL(finished()), &m, SLOT(getFinished()));
QObject::connect(&tt, SIGNAL(terminated()), &m, SLOT(getTerminate()));
tt.start();
运行结果如下:
可以看出,main()函数运行后,线程对象开始调用run()函数,在run()函数中打印5个数,然后发送一个testSignal信号,通过构造函数中的信号与槽,调用testslot(),打印输出语句。run()结束后,finished()信号触发getfinished()函数,打印输出语句。
打印的同时,对各个线程的PID也进行的打印。可以发现,在main()线程的PID和在线程类中实现的线程的PID是不一样,也就是说这是两个不同的线程。
槽函数是线程类中的成员时,为什么依然不再本线程内被调用执行?
对象依附于哪一个线程?对象的依附性与槽函数执行的关系?对象的依附性是否可以改变?
默认情况下,对象依附于自身被创建的线程,如上例中的tt、m,都是在main()函数中创建的,所以依附于主线程。
默认的情况下,槽函数在其所依附的线程中被调用执行。
对象的依附性是可以改变的。Qt中通过QObject::moveToThread来改变对象的线程依附性,是的对象的槽函数在依附的线程中被调用执行。
将上面实例中main()函数改为:
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
qDebug() << "main() tid = " << QThread::currentThreadId();
TestThread t;
MyObject m;
QObject::connect(&t, SIGNAL(started()), &m, SLOT(getStarted()));
QObject::connect(&t, SIGNAL(testSignal()), &m, SLOT(testSlot()));
m.moveToThread(&t);
t.moveToThread(&t);
t.start();
return a.exec();
}
即将对象m、t的线程依附性,使其都依附于对象t运行的线程。执行结果如下:
如果不将对象t的线程依附性也依附于t自己呢?
t.start()执行后,会在run()函数中发射信号,构造函数收到这个信号后,调用testSlot()打印语句,这个是一个新的线程,所以可以看到,如果不改变依附性,这个子线程的PID与main()的PID是一样的。
需要注意的版本问题:
在TestThread::run()函数中,有一条被注释的语句:
//exec();
在Qt4.7中,如果不加这条语句,m对象的槽函数将无法执行。
在Qt5.10.1中则是可以的。
线程中存在事件循环,信号与槽的机制需要事件循环的支持,QThread类中提供的exec()函数用于开启线程的事件循环,只有事件循环开启后,槽函数才能在信号发送后被调用。
研究槽函数的具体执行线程有什么意义?
当信号的发送与对应槽函数的执行在不同线程中,可能产生临界资源的竞争问题。
将exec()的注释去掉,执行后结果如下:
可以发现,run()函数体中的
qDebug() << "void TestThread::run() -- end tid" << QThread::currentThreadId();
并没有被执行。
如果线程体函数中开启了事件循环,线程应该如何结束?
quit()和exit()函数用于结束事件循环,quit() <--> exit(0), exec()的返回值有exit()参数决定。
注意:无论事件循环是否开启,信号发送后会直接进入对象所依附的事件队列,然而,只有开启了事件循环,对应的槽函数才会在线程中被调用。
t.start();
t.wait(8 * 1000);
t.quit(); //exit(0);
在主函数中添加上述代码,在等待一定时间(8*1000ms)后,执行quit(),结束事件循环,打印end。
设计相关问题:
什么时候需要在线程中开始事件循环?
事务性操作(间断性IO操作等)可以开启线程的事件循环,每次操作通过发送信号的方式使得槽函数在子线程中执行。
概念——文件缓冲区
默认情况下,文件操作时会开辟一段内存作为缓冲区,向文件中写入的数据回先进入缓冲区,只有当缓冲区满或者遇见换行符的时候,才会将数据写入磁盘。
意义在于,减少磁盘的低级IO操作,提高文件的读写效率。
示例如下:
class FileWriter : public QObject
{
Q_OBJECT
class Worker : public QThread
{
protected:
void run();
};
QFile m_file;
Worker m_worker;
public:
explicit FileWriter(QString file, QObject *parent = 0);
bool open();
void write(QString text);
void close();
~FileWriter();
signals:
void doWrite(QString text);
void doClose();
protected slots:
void writeSlot(QString text);
void closeSlot();
};
#include "FileWriter.h"
#include <QDebug>
void FileWriter::Worker::run()
{
qDebug() << "void FileWriter::Worker::run() - begin: tid = " << currentThreadId();
exec();
qDebug() << "void FileWriter::Worker::run() - end";
}
FileWriter::FileWriter(QString file, QObject *parent) :
QObject(parent), m_file(file)
{
connect(this, SIGNAL(doWrite(QString)), this, SLOT(writeSlot(QString)));
connect(this, SIGNAL(doClose()), this, SLOT(closeSlot()));
moveToThread(&m_worker);
m_worker.start();
}
bool FileWriter::open()
{
return m_file.open(QIODevice::WriteOnly | QIODevice::Text);
}
void FileWriter::write(QString text)
{
qDebug() << "void FileWriter::write(QString text) tid = " << QThread::currentThreadId(); //主函数发送信号三次 主线程
emit doWrite(text);
}
void FileWriter::close()
{
qDebug() << "void FileWriter::close() tid = " << QThread::currentThreadId();
emit doClose();
}
void FileWriter::writeSlot(QString text)
{
qDebug() << "void FileWriter::writeSlot(QString text) tid = " << QThread::currentThreadId(); //子线程中调用
m_file.write(text.toAscii());
m_file.flush(); //写入磁盘
}
void FileWriter::closeSlot()
{
qDebug() << "void FileWriter::closeSlot() tid = " << QThread::currentThreadId();
m_file.close();
}
FileWriter::~FileWriter()
{
m_worker.quit();
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
qDebug() << "main() tid = " << QThread::currentThreadId();
FileWriter writer("L:/Test/test322.txt");
if( writer.open() )
{
writer.write("willwilling\r\n");
writer.write("1111\r\n");
writer.write("\n");
writer.close();
}
return a.exec();
}