C++ 线程 任务队列

任务队列有点像windows的消息循环,有任务被压进队列就执行任务,否者就一直等待任务的到来。这样做的好处是外部程序不需要关心队列线程中的同步,只需要将任务压进队列即可,非常的方便。比如现在有一个下载任务要做,没有线程队列的情况下要自己创建线程,编写线程回调,在线程中下载并写文件落盘,必要的时候还要通知界面下载进度,下载完成后还要将线程销毁,有一系列的细节要处理。有了任务队列后只需要编写下载任务,然后将其压到队列中去等待执行即可,这样就屏蔽了线程相关的细节,让重点落在任务的本身。当然也会有缺点,比如任务队列繁忙的时候就要等待其他任务执行完成;比如想要多个任务并行就要创建多个任务队列等。

1、任务

任务是任务队列的执行单位(这不是废话么->->),一个任务应该有任务的开始入口点,这些入口点应该都一致,否者对于不同的任务任务队列就不好统一处理;任务还应该有一个停止入口,用来防止想要停止正在执行的任务。那么对于这个任务的简单抽象可以是这样的:

class ITask
{
public:
    virtual ~ITask()
    {

    }

    /** 执行任务
    */
    virtual void DoTask() = 0;

    /** 停止任务
    */
    virtual void Stop() = 0;
};

使用者只要继承该接口,实现他的DoTask() 和Stop() 即可,如果需要参数的可以放到成员变量中去。

有的时候我们希望在任务执行的过程中可以给外部一些反馈,比如下载任务通知外部下载的进度等,我们可以这样来定义任务:


class DownTask :
    public ITask
{
public:
    DownTask(const std::function& callback) :
        m_callback(callback)
    {

    }

    virtual ~DownTask()
    {

    }

    /** 执行任务
    */
    virtual void DoTask()
    {
        // 下载的代码,可以调用CallBack来通知外界进度
    }

    /** 停止任务
    */
    virtual void Stop() 
    {
        // 如果需要的可以实现这个接口,一般是定义一个标记量flag,DoTask每次循环判断
        // 标记,如果为假就停止
    }


    void CallBack(int all, int cur)
    {
        if (m_callback != NULL)
        {
            m_callback(all, cur);
        }
    }

protected:
    /** 回调
    */
    std::function m_callback;
};

需要注意的是,回调函数是在线程中执行的,访问全局资源是有同步问题的,所以一般来说最好不要在回调中访问需要同步的全局资源,或者直接在回调中封装一个任务压到另外一个队列中去,比如下载进程中可以在回调中发消息到主界面(主界面的消息循环实际上也相当与一个任务队列),对于下载进程来说可以将窗口的句柄直接给任务对象,就不需要callback了,需要说明的是这样做并不是很好,因为我们并不知道这个句柄是否还有效,它有可能已经失效了变成一个野指针(给一个无效但是不为空的句柄发消息是一个未定义行为,可以使用week_ptr来确保指针的有效,但是本文并不讨论)。

2、线程队列

创建一个类,里面维护一个线程和一个队列,在线程的回调函数中不停的循环检测队列中是否有有效的任务,如果有就执行,没有就等待。比如:


class SimpleTaskQueue :
    public NonCopyable
{
public:
    /** 构造函数
    */
    SimpleTaskQueue();

    /** 析构函数
    */
    ~SimpleTaskQueue();

public:
    /** 开始循环
    @return 是否成功
    */
    bool Start()
    {
        m_pThread = new (std::nothrow) std::thread(&SimpleTaskQueue::ThreadCallBack, this);
        return (m_pThread != NULL);
    }


public:
    /** 线程回调
    */
    virtual void ThreadCallBack()
    {
        while (1)
        {
            if (m_taskQue.size() > 0)
            {
                auto task = m_taskQue.front();
                m_taskQue.pop();
                task->DoTask();
            }
        }
    }

private:
    /** 线程
    */
    std::thread* m_pThread;

    /** 任务队列
    */
    std::queue > m_taskQue;
};

有了循环后还需要将任务压进队列里的接口,在定义接口之前还要考虑一个问题,线程中一直不停的取队列,另外一个线程中向队列里压任务,那么就需要同步,将循环改成:

    /** 线程回调
    */
    virtual void ThreadCallBack()
    {
        while (1)
        {
            std::shared_ptr spTask = NULL;
            // 取出队首
            {
                std::lock_guard lock(m_mutex);

                if (m_taskQue.empty())
                {
                    continue;
                }

                spTask = m_taskQue.front();
                m_taskQue.pop();

                if (spTask == nullptr)
                {
                    continue;
                }
            }
            
            spTask->DoTask();
            
        }
    }

需要注意的是DoTask一定不能放到花括号内被mutex保护。压任务的代码:

void PushTask(const std::shared_ptr& task)
{
    std::lock_guard lock(m_mutex);
    m_taskQue.push(task);
}

好了简单任务队列就完成了。

3、任务队列的润色

上述代码其实并没有什么用,有两个问题:

1、没有停止循环的接口,也没有停止当前任务的接口;这个问题很好解决,想要停止循环只要加上一个flag即可,想要停止当前任务只要将当前执行的任务记录下来,需要停止的时候调用任务的stop即可。

2、没有任务的时候循环一直空转,占用了大量的cup。这个问题有两个解决方案:
      a:循环时没有任务时候加上Sleep(0)释放cpu的使用权。(这个方法简单却不是很好,但是可以衍生出另外一种方法,会在后续文章中给出,本文不讨论)

      b:使用事件event,没有任务的时候等待任务,有任务的时候执行,很像生产者消费者模型,只不过只有一个消费者而已。

对于第一个问题,修改代码如下:

class SimpleTaskQueue :
    public NonCopyable
{
public:
    /** 构造函数
    */
    SimpleTaskQueue():m_stop(false),m_pThread(nullptr){};

    /** 析构函数
    */
    ~SimpleTaskQueue()
	{
        if (m_pThread != nullptr)
        {
            StopAll();
            m_pThread->join();
            delete m_pThread;
            m_pThread = nullptr;
        }
    };

public:
    /** 开始循环
    @return 是否成功
    */
    bool Start()
    {
        m_pThread = new (std::nothrow) std::thread(&SimpleTaskQueue::ThreadCallBack, this);
        return (m_pThread != NULL);
    }

    void StopAll()
    {
        std::lock_guard lock(m_mutex);
        ClearAll();
        m_stop = true;
    }

    void StopCurrent()
    {
        std::lock_guard lock(m_mutex);
        
        if (m_currentTask != nullptr)
        {
            m_currentTask->Stop();
        }
    }

    void ClearAll()
    {
        std::lock_guard lock(m_mutex);
        StopCurrent();
        ClearQueue();
    }

    void ClearQueue()
    {
        std::lock_guard lock(m_mutex);
        std::queue > tmp = std::queue >();
        m_taskQue.swap(tmp);
    }

    void PushTask(const std::shared_ptr& task)
    {
        std::lock_guard lock(m_mutex);
        m_taskQue.push(task);
    }
	
public:
    /** 线程回调
    */
    virtual void ThreadCallBack()
    {
        while (1)
        {

            {
                std::lock_guard lock(m_mutex);

                if (m_stop)
                {
                    break;
                }
            }
		
            // 取出队首
            {
                std::lock_guard lock(m_mutex);

                if (m_taskQue.empty())
                {
                    ::Sleep(0);
                    continue;
                }

                m_currentTask = m_taskQue.front();
                m_taskQue.pop();

                if (m_currentTask == nullptr)
                {
                    ::Sleep(0);
                    continue;
                }
            }
            
            m_currentTask->DoTask();
			

            // 准备销毁
            std::shared_ptr tmp = m_currentTask;

            {
                std::lock_guard lock(m_mutex);
                m_currentTask = nullptr;
            }
            
        }
    }

private:

    /** 互斥量
    */
    std::recursive_mutex m_mutex;
	
    /** 线程
    */
    std::thread* m_pThread;

    /** 任务队列
    */
    std::queue > m_taskQue;
	
	/** 是否停止了
    */
    bool m_stop;

    /** 当前正在执行的
    */
    std::shared_ptr m_currentTask;
};

需要注意的是:

// 准备销毁
std::shared_ptr tmp = m_currentTask;

{
    std::lock_guard lock(m_mutex);
    m_currentTask = nullptr;
}

智能指针的置空也需要加锁,如果没有std::shared_ptr tmp = m_currentTask;会导致内存泄露。

4、事件event

windows中有现成的事件,但是C11没有,上文有说过event有点像生产者消费者模型,C11中的条件变量也很想生产者消费者,所以可以自己实现一个简单事件:

/** 简单事件
@note 不提供析构函数,如果当该类析构的时候,但是还处于Wait() 状态,很不幸的告诉你发生未定义行为
*/
class SimpleEvent :
    public NonCopyable
{
public:
    /** 等待一个信号
    */
    void Wait()
    {
        std::unique_lock lck(m_mutex);

        if (!m_hasSignal)
        {
            // 不会死锁,考虑:
            // 1、m_mutex.unlock();
            // 2、wait()
            // 3、m_mutex.lock();
            // wait(lck) 相当于将1、2两步放到一个cpu周期内
            m_con.wait(lck);
        }

        m_hasSignal = false;
    }

    /** 设置一个信号
    */
    void SetEvent()
    {
        std::unique_lock lck(m_mutex);
        m_hasSignal = true;
        m_con.notify_all();
    }

private:
    /** 互斥量
    */
    std::mutex m_mutex;

    /** condition_variable
    */
    std::condition_variable m_con;

    /** 是否有信号,只是记录是否有信号,但是不记录信号次数
    @note 主要是为了先调用SetEvent(),然后调用wait的情况。考虑在Wait()前加上::Sleep(1000);
    */
    bool m_hasSignal = false;
};

将m_hasSignal 改为int计数将会是一个好用的事件,但是对于任务队列来说,会让队列空转,所以这里使用bool类型标记一下即可。加入event后的代码:

/** 前置声明,简单事件,只SimpleTaskQueue用
*/
class SimpleEvent;

/** 简单线程任务模型
@note 有点像生产者消费者模型,只是消费者只有一个
*/
class SimpleTaskQueue :
    public NonCopyable
{
public:
    /** 构造函数
    */
    SimpleTaskQueue();

    /** 析构函数
    */
    ~SimpleTaskQueue();

public:
    /** 开始循环
    @return 是否成功
    */
    bool Start();

    /** 停止所有任务
    */
    void StopAll();

    /** 停止当前的任务
    */
    void StopCurrent();

    /** 清空所有任务包括当前的,但不停止循环
    */
    void ClearAll();

    /** 清空所有任务不包括当前的,但不停止循环
    */
    void ClearQueue();

    /** 添加一个任务
    @param [in] task 任务
    */
    void PushTask(const std::shared_ptr& task);

    /** 添加一个函数任务
    @param [in] task 任务
    */
    void PushTask(const std::function& task);

public:
    /** 线程回调
    */
    virtual void ThreadCallBack();

private:
    /** 线程
    */
    std::thread* m_pThread;

    /** 互斥量
    */
    std::recursive_mutex m_mutex;

    /** 线程事件
    */
    std::shared_ptr m_spEvent;

    /** 任务队列
    */
    std::queue > m_taskQue;

    /** 是否停止了
    */
    bool m_stop;

    /** 当前正在执行的
    */
    std::shared_ptr m_currentTask;
};
#include "SimpleTaskQueue.h"

/** 简单事件
@note 不提供析构函数,如果当该类析构的时候,但是还处于Wait() 状态,很不幸的告诉你发生未定义行为
*/
class SimpleEvent :
    public NonCopyable
{
public:
    /** 等待一个信号
    */
    void Wait()
    {
        std::unique_lock lck(m_mutex);

        if (!m_hasSignal)
        {
            // 不会死锁,考虑:
            // 1、m_mutex.unlock();
            // 2、wait()
            // 3、m_mutex.lock();
            // wait(lck) 相当于将1、2两步放到一个cpu周期内
            m_con.wait(lck);
        }

        m_hasSignal = false;
    }

    /** 设置一个信号
    */
    void SetEvent()
    {
        std::unique_lock lck(m_mutex);
        m_hasSignal = true;
        m_con.notify_all();
    }

private:
    /** 互斥量
    */
    std::mutex m_mutex;

    /** condition_variable
    */
    std::condition_variable m_con;

    /** 是否有信号,只是记录是否有信号,但是不记录信号次数
    @note 主要是为了先调用SetEvent(),然后调用wait的情况。考虑在Wait()前加上::Sleep(1000);
    */
    bool m_hasSignal = false;
};

SimpleTaskQueue::SimpleTaskQueue() :
    m_stop(false),
    m_pThread(nullptr)
{
}

SimpleTaskQueue::~SimpleTaskQueue()
{
    if (m_pThread != nullptr)
    {
        StopAll();
        m_pThread->join();
        delete m_pThread;
        m_pThread = nullptr;
    }
}

bool SimpleTaskQueue::Start()
{
    m_spEvent.reset(new(std::nothrow) SimpleEvent());
    m_pThread = new (std::nothrow) std::thread(&SimpleTaskQueue::ThreadCallBack, this);
    return (m_pThread != NULL) & (m_spEvent != NULL);
}

void SimpleTaskQueue::StopAll()
{
    std::lock_guard lock(m_mutex);
    ClearAll();
    m_stop = true;
    m_spEvent->SetEvent();
}

void SimpleTaskQueue::StopCurrent()
{
    std::lock_guard lock(m_mutex);

    if (m_currentTask != nullptr)
    {
        m_currentTask->Stop();
    }
}

void SimpleTaskQueue::ClearAll()
{
    std::lock_guard lock(m_mutex);
    StopCurrent();
    ClearQueue();
}

void SimpleTaskQueue::ClearQueue()
{
    std::lock_guard lock(m_mutex);
    std::queue > tmp = std::queue >();
    m_taskQue.swap(tmp);
}

void SimpleTaskQueue::PushTask(const std::shared_ptr& task)
{
    std::lock_guard lock(m_mutex);
    m_taskQue.push(task);
    m_spEvent->SetEvent();
}

void SimpleTaskQueue::PushTask(const std::function& task)
{
    PushTask(std::shared_ptr(new SimpleFunTask(task)));
}

void SimpleTaskQueue::ThreadCallBack()
{
    while (true)
    {
        {
            std::lock_guard lock(m_mutex);

            if (m_stop)
            {
                break;
            }
        }

        m_spEvent->Wait();

        // 将队列里的全部执行掉
        while (true)
        {
            // 取出队首
            {
                std::lock_guard lock(m_mutex);

                if (m_taskQue.empty())
                {
                    break;
                }

                m_currentTask = m_taskQue.front();
                m_taskQue.pop();

                if (m_currentTask == nullptr)
                {
                    continue;
                }
            }

            // 执行任务
            m_currentTask->DoTask();

            // 准备销毁
            std::shared_ptr tmp = m_currentTask;

            {
                std::lock_guard lock(m_mutex);
                m_currentTask = nullptr;
            }
        }
    }
}

使用示例代码:

    void Test_taskQue1()
    {
        SimpleTaskQueue simpleTaskQue;
        simpleTaskQue.Start();

        bool ret = false;
        int t1 = 0;
        int t2 = 0;

        simpleTaskQue.PushTask([&]()
        {
            t1 = 1;
            t2 = 2;
            ret = true;
        });

        while (true)
        {
            ::Sleep(1);
        }
    }

有意思的是

 simpleTaskQue.PushTask([&]()
 {
      t1 = 1;
      t2 = 2;
      ret = true;
 });

可以直接的将lambda中的代码放到线程中执行,非常方便。

代码链接:ddm/ddm/src/thread/task at main · yunate/ddm (github.com)

5、总结

本文介绍了一个简单任务队列,下文会介绍基于任务队列的一个应用:简单日志

你可能感兴趣的:(线程,C++,线程,任务队列,1024程序员节)