当前位置:C++技术网 > 精选软件 > 线程池学习总结:3 模拟实现线程池分析-任务机制的建立

线程池学习总结:3 模拟实现线程池分析-任务机制的建立

更新时间:2016-06-16 23:52:14浏览次数:1+次

    在《线程池学习总结2:模拟实现线程池分析-所有线程共同完成任务》中,我们很仔细的描述了线程池中所有线程共同完成任务需要注意的地方,说明了执行相同任务的必要性。执行相同任务也就是给所有线程指定同一个线程函数。而这个线程函数的编写,就是一个关键点了。
    线程池的核心就是执行任务,所有的执行过程都在线程函数里。我们在《线程池学习总结1:模拟实现线程池分析-创建线程池》中说了,要保持线程“永生”,给一个死循环就行了。然而,我们要所有线程都要执行有用的任务才行,否则简单的死循环没有任何意义。我们在线程函数里写的所有代码就是完成任务的,要考虑的各种问题。所以,实际的程序性能的好坏,线程函数就是关键了。如果写的不好,甚至会引起死锁哦。
    我们暂时一下子不知道线程们要执行什么任务,我们现在只知道,至少要让线程死循环着,先不要让线程死掉,然后慢慢想办法找任务。所以线程函数开始写成这样就是了:
DWORD WINAPI ThreadPoolProc(LPVOID lpParam)
{
    while(1)
    {
        Sleep(1000);//睡眠一秒钟
    }
}
    这是最简单的线程函数了。不过就是不停的醒来-睡觉-醒来-睡觉。你可以看到,咱们这是连续的思维的发展,到了这一步,都是很自然而然的。我想你到这里,你也会和我一样继续往下想,该怎么给线程执行任务呢?这个任务以什么形式给线程呢?
    如果让线程啥也不干,一点意思都没有,还浪费CPU。但是我们又不能让线程做一个固定的事情,比如下面的代码:
#include <Windows.h>
#include <iostream>
using namespace std;
DWORD WINAPI ThreadPoolProc(LPVOID lpParam)
{
    while(1)
    {
        cout<<"哈哈哈,我是线程,我笑了。";
        int i=0;
        int j=0;
        int k=i+j;
        Sleep(1000);//睡眠一秒钟
    }
}
    现在线程虽然在做事了,但是并不是我们想要的。这里给的只是一个示例。你可以在这里写任何代码。但是你可以看到,不管你写成什么样,代码总是固定的,而且把剩饭炒来炒去的,也不能发挥实际的意义。这就好像跟工人说,你把这个砖从东边搬到西边,然后再从西边搬到东边,一直这样反反复复的做。这样工人确实在干活,但是对老板有什么意义呢?
    我们要的是,我指定一个实际的任务,工人做了,给我带来效益的。比如你给一个清单,清单中指定了地点,然后工人把砖搬到指定的地点,这样产生了效益。这样,工人要做有用事情,就要去看看任务清单里指定了哪些事情,而不是搬砖玩。
    也就是说,我们要给线程提供一个任务列表,然后线程按照列表中的任务来做,就能够产生效益。而这个任务的描述,自然每一个具体的任务是不同的,比如第一个任务可能是要将砖搬到北京,第二个任务可能是搬到月亮上。所以,不同的任务没法在线程函数中写死,写死了就把砖搬着玩一样的。
    那么这个就是我们说的任务机制。我们提供一个任务列表,客户提出任务要求,老板将这个任务加入到任务列表里,然后线程不停的查看列表,如果有任务,就去干活,没有任务就自己玩。所以我们就加一个列表来。我们现在是分析理解线程池的整体的机制和原理,所以任务的具体结构描述就不管了。我们只提供一个列表,一个元素代表一个任务。你可以用数组,也可以用容器。如果用数组的话,你得写一个函数来判断数组中是否有任务,如果有容器的话,容器有成员函数判断容器中是否有元素存在。用数组处理起来很麻烦,基本思想是,用0表示任务是空的,1表示任务是有效的。因为数组要实现创建好,而且是固定的,所以就只能用数值来表示任务是否存在。如果你用链表实现的话,就看节点数。容器不用我们管内部的实现。我们用数组这样写:
int tasklist[100]={0};//全局变量
//判断任务列表是否为空
bool isempty()
{
    for(int i=0;i<100;i++)
    {
        if(tasklist[i]!=0)return false;
    }
    return true;
}
//判断任务列表是否满了
bool isfull()
{
    for(int i=0;i<100;i++)
    {
        if(tasklist[i]==0)return false;
    }
    return true;
}
//取出一个任务
bool gettask(int& task)
{
    for(int i=0;i<100;i++)
    {
        if(tasklist[i]!=0)
        {
            task = tasklist[i];
            tasklist[i]=0;
            return true;
        }
    }
    return false;
}
//添加一个任务
bool addtask(int task)
{
    for(int i=0;i<100;i++)
    {
        if(tasklist[i]==0)
        {
            tasklist[i] = task;
            return true;
        }
    }
    return false;
}
    我们用数组元素的值来表示是否有任务,如果元素值为0,表示这个元素不是有效任务,如果所有的元素值都为0,表示任务列表为空,如果都不为0,表示任务列表满了。添加和取出任务也都是对一个元素进行值的操作,获取任务后,将对应的元素设置为0,表示这个任务取出执行了,如果返回值为false,表示没有取到任务。添加任务时,就是将值为0的设置为非零,设置为传入的参数task,如果返回为false,表示任务列表已满。
    而用容器的话,就方便的多了。我们可以使用STL的vector容器,用vector的push_back()添加一个任务,用pop_back()取出一个任务,用size()获取任务的个数,通过个数我们可以判断是否为空。因为vector是动态增长的,所以没有满的说法。除非内存不够了。
    那么我们的线程函数就可以获取任务了。我用vector容器来实现:
#include <Windows.h>
#include <iostream>
#include <vector>
using namespace std;
vector<int> g_tasklist;//全局任务列表
void TaskProc(int i)//执行任务的函数
{
    cout<<"线程"<<i<<"正在";
    cout<<"假装处理任务,并且很快处理完了一个,剩余任务数:"<<g_tasklist.size()<<endl;
}
DWORD WINAPI ThreadPoolProc(LPVOID lpParam)
{
    while(1)
    {
        if (g_tasklist.size()<=0)//判断任务是否为空
        {
            Sleep(5000);//任务为空,则多睡会
        }
        else
        {
            g_tasklist.pop_back();//有任务,则取出一个任务
            //执行任务
            TaskProc(*(int*)lpParam);//将创建线程时传入的序号提取出来
            Sleep(1000);//执行完后休息一会
        }
        
    }
}
    我们就知道了如何取得一个任务,也知道如何执行一个任务了。那么任务从哪来呢?任务列表的任务在哪里添加呢?
    线程函数中是处理任务的,所以不能自己添加任务。因为任务列表是线程函数和添加任务的线程共享的,所以任务列表要是全局的,否则不好沟通。我们很自然也就想到了,在主线程里添加任务。我们这个是模拟实现,就在主线程实现好了,不增加复杂度。而实际开发中会是一个单独的线程来管理任务的添加的。
    因为线程池线程是主线程创建的,所有线程都是一个进程内的。主线程的死活决定了进程的死活,如果进程死了,那么进程内的所有线程就都要跟着死去,Windows会强制杀死这个进程的所有线程。所有,主线程决定了大局。既然要线程池永生,那么主线程也必须要永生。所以,我们在主线程(执行main函数的线程)设置一个死循环。当然,我们也不是让主线程什么也不干,我让主线程不停的添加任务,来模拟任务的创建的过程。我们看到的main函数就是这样的:
void main()
{
    HANDLE hThread[5]={0};
    DWORD dwThreadID[5]={0};
    static int id[5];
    for (int i=0;i<5;i++)
    {
        id[i]=i;
        hThread[i]=CreateThread(NULL,0,ThreadPoolProc,&id[i],0,&dwThreadID[i]);
    }
    while(1)
    {
        g_tasklist.push_back(0);
        cout<<"新增1个任务.\n";
        Sleep(2000);
    }
}
    主线程每隔2秒添加1个任务,这样任务列表不断的被取出,也不断的被添加,这样就形成了完整的任务机制了。我们可以看到,从创建线程池到任务机制建立,其实并没有太多的问题,当你看完这几篇文章后,就会感觉,其实并没有现象中的那么神秘和高深莫测。
    做到这一步,确实值得高兴,好好玩一把吧。不过,也别高兴的太早。我们还需要进一步完善这个基本的任务机制。我们可以判断,所有线程都执行了同一个线程函数,而且也没有状态标志的干扰,不会有可重入的问题。但是却有公共资源访问的问题。因为多线程切换不确定性,假如线程池线程判断有一个任务,当准备执行任务的时候,这个线程被暂停执行,换到另一个线程执行,人家也判断有一个任务,然后执行完了,等原来那个线程回去执行,结果程序崩溃了。因为会操作一个不存在的任务而导致严重的问题。C++中通常是内存访问错误,因为任务已经被处理,对应的内存被释放了,你去读取一个被释放的内存,就会出现内存读错误。我们会在下一篇文章详细介绍。