一步一步写线程之九线程池任务的窃取

一、介绍

在实际的工作安排中,如果有一个比较大的工作,又可以细分的,诸如有一天一万个萝卜要洗这样的工作。假如做为一个工作的分配者,怎么处理这种需求?可能每个人都会想,先看看一个人一天洗多少萝卜,然后一除大概就可以算出来一天完成这个工作量需要多少人。
可以把一个人当成一个线程,假设十个人一天可以完成这项工作,那么就可以使用十个线程来完成类似的工作。但事情总有特殊情况,比如有的人天生麻利,可能半天就完成了。那么此时怎么安排这个人来工作呢?对工作分配者来说,很简单,余下的人哪个剩余工作量最多,就让完成的这个人去帮他做。依次类推,滚雪球一样就把工作完成了。当然,这里不考虑人的情绪问题,只是单纯的从工作完成角度来考虑。
其实把洗萝卜改成多种,即面对的清洗工作内容有洗萝卜、洗白菜、洗土豆等等,出现有人先完成的可能性会急剧增加。
而计算机的世界里,其实和上面的说明类似。只不过,计算机世界里可能更平等,不用考虑诸如为啥我干完了本职工作还要帮别人干这种感情的问题。

二、并行环境

早期的PC是单核的,也就是说,其本质是不提供并发和并行操作的。所谓的上层并发,就是一种虚拟的现象。可随着芯片技术的发展,CPU从单核到多核,从一个到多个,甚至从另外一层角度看,分布式也算是一种并行技术。
那任务的分配就必然会产生冷暖不均。怎么解决这类问题呢?在现实世界中,一般这种可并行量化的情况,都是按件计量,也就是说干得越多收入越多。但计算机世界没有感情,就得需要设计者从数学的角度来设计相关的算法来实现。并行的算法有很多种,有兴趣可以查找相关资料。此处仍然针对不同任务分配的情况,最基础的方式就是使用任务再分配的方式,也就是任务偷窃。

三、任务偷窃

习惯上,人们是愿意通过一个管理者去重新分配剩余的工作量。但在计算机世界中,如果使用这种方式,在大的环境中(分布式)也是常见的,但在一般的多线程开发中,无形中会增加工作量和引入不必要的代码。其实可以逆向思维,如果一个线程把自己的任务都执行完成后,是不是可以去看看别的线程是不是还有任务,如果其它线程剩余的任务数量超过一个阈值,就去它的任务队列拿一个过来执行,这个过程就是任务的偷窃。
一般来说,如果任务量不大,做任务偷窃几乎没有意义,反而增加复杂度。但在一些反复执行任务的线程池中,特别是会动态增加或随机插入任务的的场景下,任务的窃取就非常重要了。

四、例程

下面看一个例子:

//ThreadWorker.cpp
#include "ThreadWorker.h"
#include "ThreadPool.h"
#include <iostream>

thread_local int ThreadWorker::testValue = 10;
ThreadWorker::ThreadWorker() {}

void ThreadWorker::InitThread(bool initStatus, CallBackMsg cb) {
  this->cbm_ = cb;
  this->pWorkerThread_ = std::make_shared<std::thread>(&ThreadWorker::Run, this);
  if (nullptr != this->pWorkerThread_) {
    this->curThreadId_ = this->pWorkerThread_->get_id();
  }

  this->pThreadCon_ = std::make_shared<ThreadCondition>();
}
void ThreadWorker::Start() {}
void ThreadWorker::SetSignal() { this->pThreadCon_->Signal(); }
void ThreadWorker::Run() {
  int data[10] = {0};
  auto tPool = ThreadPool::Get();

  while (!status_) {
    this->runStatus_ = false;
    this->pThreadCon_->Wait(); // Handling false wake-up
    bool bRet = true;
    while (bRet) {
      std::cerr << "cur thread id is:" << this->curThreadId_ << std::endl;
      // ThreadPool::Get()->Wait();
      this->runStatus_ = true;
      std::cerr << "cur run thread id is:" << this->curThreadId_ << std::endl;
      Task task;
      bool ret = tPool->GetPrivateTask(this->curThreadId_, task);
      if (!ret) {
        ret = tPool->GetPubTask(task);
        std::cerr << "GetPubTask:" << this->curThreadId_ << std::endl;
        if (!ret) {
          auto [r, t] = tPool->GetStealTask();
          std::cerr << "GetStealTask:" << this->curThreadId_ << std::endl;
          if (r) {
            std::cerr << "Run--------------GetStealTask:" << this->curThreadId_ << std::endl;
            cbm_(data, 3);
          } else {
            bRet = false;
            continue;
          }
        }
      }

      if (ret) {
        task(++testValue);
        data[0] = testValue++;
        data[1] = testValue++;
        data[2] = testValue++;
        cbm_(data, 3);
      } else {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        if (this->cbm_ != nullptr) {

          cbm_(data, 0);
        }
      }
    }

    std::this_thread::yield();
  }
}

void ThreadWorker::Join() {
  if (this->pWorkerThread_ != nullptr && this->pWorkerThread_->joinable()) {
    this->pWorkerThread_->join();
  }
}

void ThreadWorker::Quit() {
  this->status_ = true;
  // trigger conditional
  this->pThreadCon_->Signal();
}
bool ThreadWorker::getCurRunStatus() { return this->runStatus_; }

//ThreadPool.cpp
#include "ThreadPool.h"
#include "ThreadWorker.h"
#include <iostream>

ThreadPool::ThreadPool() {}
ThreadPool::~ThreadPool() { this->Destory(); }
void ThreadPool::InitThreadPool(int threadCount, bool initThreadStatus, CallBackMsg cb) {
  this->funcCallBack_ = cb;

  if (threadCount > this->maxThreadCount_) {
    threadCount = this->maxThreadCount_;
  }

  this->threadCount_ = threadCount;
  this->initThreadStatus_ = initThreadStatus;

  for (int num = 0; num < threadCount; num++) {
    auto workerThread = std::make_shared<ThreadWorker>();
    workerThread->InitThread(initThreadStatus, cb);
    this->pVecThreadWorker_.emplace_back(workerThread);

    // create task deque
    this->taskDeques_.emplace(workerThread->GetCurThreadID().value(), std::make_unique<TaskDeque<Task>>());
  }

  this->pThreadCon_ = std::make_shared<ThreadCondition>();

  this->GetThreadID();
}

void ThreadPool::AddTask(Task t) {
  this->taskQueue_.Push(t);
  this->SetSignal();
  std::cerr << "add task and signal" << std::endl;
}
void ThreadPool::Destory() {
  for (auto &au : this->pVecThreadWorker_) {
    au->Join();
  }
}
std::tuple<bool, Task> ThreadPool::GetTask() { return this->taskQueue_.PopFront(); }
std::shared_ptr<ThreadPool> ThreadPool::Get() {
  static auto threadPool = std::make_shared<ThreadPool>();
  return threadPool;
}

void ThreadPool::Wait() { this->pThreadCon_->Wait(); }
void ThreadPool::SetSignal() { this->pThreadCon_->Signal(); }
// Wake up specified thread
void ThreadPool::SetSignal(std::thread::id threadid) {
  for (auto &pWorker : this->pVecThreadWorker_) {
    if (pWorker->GetCurThreadID() == threadid) {
      pWorker->Start();
    }
  }
}
void ThreadPool::SetMaxThreadCount(int maxCount) { this->maxThreadCount_ = maxCount; }
bool ThreadPool::getThreadRunStatus(std::thread::id id) {
  for (auto &worker : this->pVecThreadWorker_) {
    if (id == worker->GetCurThreadID()) {
      return worker->getCurRunStatus();
    }
  }

  return false;
}
std::vector<std::thread::id> ThreadPool::GetThreadID() {
  for (auto &worker : this->pVecThreadWorker_) {
    this->idVec_.emplace_back(worker->GetCurThreadID().value());
  }

  return this->idVec_;
}

// deque
void ThreadPool::AddTaskDeque(Task t) {
  static int count = 0;
  std::cerr << "AddTaskDeque,thread count:" << this->idVec_.size() << std::endl;
  if (this->idVec_.size() < 1) {
    return;
  }

  std::cerr << "AddTaskDeque,push task!count is:" << count << std::endl;
  if (count < 37) {
    this->taskDeques_[this->idVec_[this->curThreadIndex++]]->pushFront(t);
    this->curThreadIndex = this->curThreadIndex % this->idVec_.size();

    count++;

  } else {
    this->taskQueue_.Push(t);
    count++;
  }
  // this->SetSignal();

  for (auto &th : this->pVecThreadWorker_) {
    th->SetSignal();
  }
  std::cerr << "set signal" << std::endl;
}
bool ThreadPool::GetPrivateTask(std::thread::id tid, Task &t) {
  if (this->taskDeques_.count(tid) > 0) {
    return this->taskDeques_[tid]->popFront(t);
  }
  return false;
}

bool ThreadPool::GetPubTask(Task &t) {
  auto [ret, d] = this->taskQueue_.PopFront();
  if (ret) {
    t = d;
  }

  return ret;
}
std::tuple<bool, Task> ThreadPool::GetStealTask(/*std::thread::id tid*/) {
  for (std::thread::id &id : this->idVec_) {
    std::cerr << "--------------------other thread task count:" << this->taskDeques_[id]->size() << std::endl;
    if (this->taskDeques_[id]->size() > 0) {
      return this->taskDeques_[id]->popBack();
    }
  }

  return {false, Task{}};
}


//main.cpp
#include <iostream>

#include "TaskBucket.h"
#include "ThreadPool.h"
#include "common.h"
#include <chrono>
#include <iostream>
#include <list>
#include <random>

unsigned long getRand() {
  static std::default_random_engine dre(std::chrono::system_clock::now().time_since_epoch().count() /
                                        std::chrono::system_clock::period::den);

  return dre();
}
int getRangeRand() {
  static std::default_random_engine d;
  static std::uniform_int_distribution<unsigned> u(1, 9);
  return u(d);
}
void DisplayResult(int *buf, int size) {
  if (size < 1) {
    std::cerr << "not task run!" << std::endl;
    return;
  }

  std::cout << "run result:" << std::endl;
  for (int num = 0; num < size; num++) {
    std::cout << "first data:" << buf[num] << std::endl;
  }
}

void AppTaskFast(int d) {
  std::cout << "enter thread function AppTaskFast,d is:" << d << std::endl;
  std::this_thread::sleep_for(std::chrono::milliseconds(1000));
  if (d > 0) {
    std::cout << "cur value is:" << d << std::endl;
    return;
  }

  std::cout << "err:nothing to do!" << std::endl;
}
void AppTask(int d) {
  std::cout << "enter thread function AppTask,d is:" << d << std::endl;
  std::cout << "random value:" << getRangeRand() << std::endl;
  std::this_thread::sleep_for(std::chrono::milliseconds(getRangeRand() * 1000));
  if (d > 0) {
    std::cout << "cur value is:" << d << std::endl;
    return;
  }

  std::cout << "err:nothing to do!" << std::endl;
}

void AppTaskGlobal(int dd) {
  std::cout << "enter thread function AppTaskGlobal,d is:" << dd << std::endl;
  std::this_thread::sleep_for(std::chrono::milliseconds(1000));
  if (dd > 0) {
    std::cout << "AppTaskGlobal:cur value is:" << dd << std::endl;
    return;
  }

  std::cout << "AppTaskGlobal:err:nothing to do!" << std::endl;
}

int main() {

  auto pTp = ThreadPool::Get();
  pTp->InitThreadPool(6, false, DisplayResult);
  std::this_thread::sleep_for(std::chrono::milliseconds(1000));

  std::cerr << "start assign task!" << std::endl;
  for (int num = 0; num < 48; num++) {
    if (num < 43) {
      pTp->AddTaskDeque(AppTask);
    } else {
      pTp->AddTaskDeque(AppTaskGlobal);
    }
  }

  char a;
  std::cin >> a;
}

执行后可能的结果:

在这里插入图片描述

需要说明的是,不同的环境执行的结果可能是不同的,在测试中发现,即使是同样的机器,反复执行的结果也不尽相同。大家可以适当的修改一下各个任务的延时情况,来模拟执行任务的长短,可能会有更好的体会。
程序主要是设计了三类任务(普通任务、快速任务和全局任务)和两种任务队列(线程私有任务队列和公共任务队列),在线程执行完成自己的任务后去公共队列取任务,如果公共队列没有了则去偷窃别的线程的任务。三类任务通过设置不同的延时来模拟实际的执行任务的情况。实际只选取了两个,在AddTaskDeque中,动态的修改全局和私有队列的数量(可以根据自己的机器来动态修改那些写死的数字),从而达到模拟任务完成参差不齐的情况来实现任务的偷窃。
程序中还有很多需要优化的地方,比如可以监听一个信号,然后使用全部触发模拟会更简单。此处只是为了使用原来的代码,所以就使用了循环唤醒。任务队列写成三层判断也是为了让大家明白任务执行的流程,其实实际开发时可以优化成一个语句,重点是明白如何进行线程窃取即可。

五、总结

对绝大多数软件人员而言,其实很多技术拼到最后不是拼设计者的聪明才智,拼的是经验。特别是对于工程类的开发更是如此。而软件开发领域,基本以现有的成熟的技术为主,这点就更突出。随着软件的规模越大,经验的要求越高。很多开发者可能终其一生都遇不到所谓的千万并发,那希望他设计一个支持千万并发的服务,就是一个不可能的任务,其它情况亦是如此。
所以开发者们勿需气馁,把基础打好,认真学习,找机会就上。不断总结经验得失,技术就会越来越磨炼得精粹。

  • 26
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值