C++多核高级编程 - 06 并发任务的通信和同步(3) 线程的策略方法及工作的分解和封装

一 , 线程的策略方法

线程的策略决定了在将应用程序线程化时可能使用的方法。方法决定了被线程化的应用程序如何将它的工作委派给任务及通信如何进行。

常见的模型:

  • 委托(boss-worker)
  • 对等(peer-to-peer)
  • 流水线(pipeline)
  • 生产者-消费者(producer-consumer)

在对应用程序建模时对于不同的阶段要采取不同的模型,一个模型可以嵌入到另一个模型中。

例如:流水线模型可用于整个进程的控制,但在流水线的一个节点中可以使用委托模型来处理局部数据。


1,委托模型(boss-worker)

在委托模型中,一个线程(boss)创建其他线程(worker)并给每个线程指定任务。在能继续运行之前,boss可能会等待所有的worker完成任务。boss线程执行一个事件循环。当事件发生时boss创建worker并指派任务,但为每个到达任务创建新线程的方式可能导致进程超出他的资源的限制。另一种方式是在boss中维护一个线程池,在boss初始期间创建所以的worker并使他们挂起,当有任务时从线程池中指定一个worker来完成任务。


boss的主要用途:

  • 创建所有的线程
  • 将工作放置到队列中
  • 当有工作到来时,唤醒worker.

worker的主要用途:

  • 检查队列中的请求
  • 执行被指派的任务
  • 如果没有可执行的任务,则将自己挂起

示例伪代码:

pthread_t  Thread[N];


//boss thread
{
    pthread_create(&(Thread[0]...TaskX...));
    pthread_create(&(Thread[1]...TaskY...));
    pthread_create(&(Thread[2]...TaskZ...));    
}

while(Req Queue is not empty)
{
    getRequest()
    switch(req_type)
    {
    case X:
        enqueue request to XQueue;
        broadcast to thread XQueue request available;
        break;
    case Y:
        enqueue request to YQueue;
        broadcast to thread YQueue request available;
        break;
    case Z:
        enqueue request to ZQueue;
        broadcast to thread ZQueue request available;
        break;
    }
}

// worker man  X Y Z
void *TaskX(void* p)
{
    while(1)
    {
        wait for signal XQueue is available;
        when signed
        while(XQueue is not empty)
        {
            lock mutex;
            dequeue request
            unlock mutex;
            process request;
            lock mutexResult;
            enqueue resutlQueue;
            unlock mutexResult; 
        }
    }
}

void *TaskY(void* p)
{
    while(1)
    {
        wait for signal YQueue is available;
        when signed
        while(YQueue is not empty)
        {
            lock mutex;
            dequeue request
            unlock mutex;
            process request;
            lock mutexResult;
            enqueue resutlQueue;
            unlock mutexResult; 
        }
    }
}
void *TaskZ(void* p)
{
    while(1)
    {
        wait for signal XQueue is available;
        when signed
        while(ZQueue is not empty)
        {
            lock mutex;
            dequeue request
            unlock mutex;
            process request;
            lock mutexResult;
            enqueue resutlQueue;
            unlock mutexResult; 
        }
    }
}


2, 对等模型(peer-to-peer)

在对等模型中,所有的线程有着平等的工作状态。有一个线程会最初创建执行所有任务的全部线程,但该线程仍然是一个worker线程,它不会委派工作。worker线程有更为局部的职责。对等线程可以处理被所有线程共享的一个输入流中的请求。或者每个线程维护自己的输入流。对等线程间可能有通信的请求。

示例伪代码:

pthread_t  Thread[N];

//create thread
{
    pthread_create(&(Thread[0]...TaskX...));
    pthread_create(&(Thread[1]...TaskX...));
    pthread_create(&(Thread[2]...TaskX...));    
}


// worker man  X
void *TaskX(void* p)
{
    while(1)
    {
        lock mutex;
        dequeue request
        unlock mutex;
        process request;
        lock mutexResult;
        enqueue resutlQueue;
        unlock mutexResult; 
    }
}


3,生产者-消费者模型(produce-consumer)

生成者-消费者模型中,生成者负责产生数据,这些数据将会被消费者消费掉。数据被保存在共享内存中。生成者和消费者间需要读写锁来保持同步。生成者和消费者模型在大规模应用时也被称为客户端-服务器模型


示例伪代码:

pthread_t  Thread[2];

//initial thread
{
    pthread_create(&(Thread[0]...producer...));
    pthread_create(&(Thread[1]...consumer...));  
}

// producer thread
void *producer(void* p)
{
    while(1)
    {
        perform work
        lock mutex;
        enqueue request
        unlock mutex;
        set signal consumer
    }
}


// consumer thread
void *TaskX(void* p)
{
    while(1)
    {
        wait for signaled
        when signed
        while(Data Queue is not empty)
        {
            lock mutex;
            dequeue request
            unlock mutex;
            process request;
            lock mutexResult;
            enqueue resutlQueue;
            unlock mutexResult; 
        }
    }
}


4,流水线模型(pipeline)

流水线模型可以通过装配线的方法来刻画,其中产品流被分为多个阶段进行处理。在每个阶段,由一个线程在输入单元上进行工作。当单元经过流水线的所有阶段后,则对输入的处理已经完成。这种模型在每个处理阶段的线程上可能需要有必要的缓冲输入单元。流水线中应对负载加以均衡。防止某一阶段有积压的现象。


示例伪代码:

pthread_t  Thread[N];
Queues[N]

//initial thread
{
    pthread_create(&(Thread[0]...stage1...));
    pthread_create(&(Thread[1]...stage2...));
    pthread_create(&(Thread[0]...stage3...));      
}

// stage X
void *stageX(void* p)
{
    while(1)
    {
        wait for signaled
        when signed
        while(Data Queue is not empty)
        {
            lock mutex;
            dequeue unit data
            unlock mutex;
            
            process unit data;
            
            lock mutexNextStageQueue;
            enqueue data unit result into next stage queue;
            unlock mutexNextStageQueue; 
        }
    }
}


二 , 工作的分解和封装

这里将用解决一个实际问题的例子来综合的说明多线程及相关知识的使用方式。其中最值得学习和借鉴的是在遇到问题时如何逐步的澄清问题,考虑解决方法并使用并行的思考方式来处理问题。


一 ,问题陈述

我们有大量的文本文件需要过滤,希望从多个文本中删除指定的Token或指定的字符,例如 ",", "!", "&"等等。而且希望系统能实时的完成。


可以立即确定的对象:

  • 文本文件
  • 待删除的字符
  • 过滤后的结果文件

二,策略

假设我们有超过70个文件需要处理。并且每个文件包含成百上千行的文本。程序过滤指定的字符集并创建新的目标文件。

简化目标:

  • 删除所有需要过滤的字符
  • 实时完成工作
  • 保持文件内容的完整性

方法1:

在文件中查找一个字符,找到后删除。然后查找这个字符的下一次出现的位置。当这些字符都被删除之后,再次针对下一个多余的字符对文件进行遍历。为每个文件重复这一过程。


方法2:

从每个文件中删除某个多余字符的位置,然后为每个多余字符重复这个过程。


方法3:

读进一行文本,删除一个多余字符。检查该行文本并删除下一个多余字符,以此类推。当从该行文本中删除所有的字符后,将过滤的文本写入新的文件中。对每个文件进行这样的操作。


方法4:

基本与方法3相同,但我们从一行文本中删除一个多余字符之后 (可能出现多次) 就将它写入文件或容器中。一旦整个文件处理完毕,则为下一个字符再次进行处理。当最后一个字符被删除之后,文件被过滤完毕。如果文本位于容器中,则将其写入文件。为每个文件重复该过程。容器在重新构建文件时变得非常重要。


三,观察

在考虑每种方法时,我们看到得是对文件或对一行文本的多次遍历。对此必须加以考虑,看看它对性能的影响。对于方法1和2,对于每个字符都要重入文件。其产生的中间结果是删除一个字符时的整个文件。方法3和4,过滤的是一行文本,中间的结果也是一行文本。与文件相比文本的处理速度要快很多。


四,问题和解决方案

我们将使用方法3。基于观察我们发现这种方法是能最快给出结果的,即使是大的数据集也是如此。现在可以考虑并发模型了。并发模型可以帮助我们决定使用哪种通信和何种类型的协作来解决问题。一行文本应该在处理另一行文本开始处理前被解决,这与流水线模型十分类似。同一行文本,用不同的处理阶段过滤不同的字符。在过滤完成时文本被写入文件。相邻的2个阶段使用一个队列来传递中间结果。

解决方案中的通信和协作需求:

  • 队列要求同步
  • 需要EREW访问策略
  • 主agant组装第一个队列并创建线程
  • 将输入和输出队列及互斥量传递给各个阶段

至此关于问题的分析告一段落,可以进入文档和原型开发阶段来验证我们分析所得的解决方案,以及其性能是否能够满足需求。


本章中所涉及的四种模型的实例代码



你可能感兴趣的:(C++多核高级编程 - 06 并发任务的通信和同步(3) 线程的策略方法及工作的分解和封装)