线程(进程)的同步与互斥实例

2023-11-14

1.有一个队列,线程1负责从网络接收分组,并将收到的分组放到队列尾,然后再次从网络中接收下一个到达的分组,并进行同样的队列操作。线程2从此队列头中取出一个分组进行处理,处理完毕后,再次从队列中取出一个分组进行处理,处理完毕后再次从队列头取出下一个分组进行处理,两个线程都以无限循环的方式工作,因此该队列为临时资源,若队列不为空,线程2才应该开始循环处理,否则需要等待,显然如果线程2不停的检测队列是否为空,虽然能够正常工作,但是很消耗cpu资源,cpu效率低,当队列为空时,线程2进入休眠状态,当队列不为空时,线程2自动被唤醒,可以利用条件变量解决这个问题。

 

函数说明:

 

#include <pthread.h>
int  pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);

返回值:0代表成功,错误号代表失败

 

参数mutex对条件句进行保护,线程2自动把加锁的互斥量传给此函数,函数把线程2放到等待条件的线程列表上,然后对互斥量进行自动解锁,这两步操作属于原子操作,这样就不会造成条件检查和线程进入休眠状态等待之间有其它线程进入的问题,当线程2因条件不满足而进入休眠后,因为互斥量已经被解锁,于是线程1就可以加锁互斥量并将收到的分组放到队列尾,然后主动解锁互斥量,接着线程1再调用下面的pthread_cond_signal函数通知线程2条件已经发生变化。

 

#include <pthread.h>
int  pthread_cond_signal(pthread_cond_t *cond);


返回值:0代表成功,错误号代表失败。

 

由于此条件发生了改变,则pthread_cond_wait函数返回,保护条件的互斥量将再次被自动加锁(注意这里是自动加锁,不需要线程2主动加锁),线程2被唤醒,于是就可以对临界资源进行操作,即线程2从队列中取出下一个分组并主动对互斥量进行解锁,之后再对该分组进行后续的处理。
 

include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <sys/types.h>

#include <unistd.h>

#include <pthread.h>

#include <queue>

#include <time.h>

std::queue<int>q;

//静态初始化互斥量

pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;

//静态初始化条件变量

pthread_cond_t condition=PTHREAD_COND_INITIALIZER;

void print(int val)

{

    printf("获取一个新数据:%d\n",val);

}

void *thread1(void *arg)

{

    srand(time(NULL));

    while (1)

    {

        pthread_mutex_lock(&mutex);//加锁互斥量

        q.push(rand()%100+1);      //向队列中插入数据

        printf("插入一个新数据\n");

        pthread_mutex_unlock(&mutex);//解锁互斥量

       //通知线程2条件发生改变

        pthread_cond_signal(&condition);

        sleep(1);

    }

}

void *thread2(void *arg)

{

    srand(time(NULL));

    while (1)

    {

        pthread_mutex_lock(&mutex);//加锁互斥量

       //若队列为空,则休眠等待条件改变

        while (q.empty())

            pthread_cond_wait(&condition, &mutex);

        

       //若队列不为空,则从队列中取出一个数据

        int val=q.front();

        q.pop();

        pthread_mutex_unlock(&mutex);//解锁互斥量

        //处理数据

        print(val);

        sleep(1);

        

    }

}

int main()

{

    pthread_t pit1;  //线程1标志符

    pthread_t pit2;  //线程2标识符

    int err=pthread_create(&pit1,NULL,thread1,NULL);//创建线程1

    if(err!=0)

    {

        perror("creat thread1 error\n");

        exit(1);

    }

    err=pthread_create(&pit2,  NULL, thread2,  NULL);  //创建线程2

    if(err!=0)

    {

        perror("creat thread2 error\n");

        exit(1);

    }

    while (1)

    {}

    return 0;

}

2.用两个线程实现如下功能:主线程负责读取数据,当遇到特殊字符"end"后退出程序,另一个线程负责处理这些输入的数据(输出其字符串长度)。

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <sys/types.h>

#include <unistd.h>

#include <pthread.h>

#include <time.h>

#include <semaphore.h>

#define WORK_SIZE 1024  //缓冲区大小

pthread_mutex_t work_mutex;  //互斥量

int time_to_exit=0;   //退出标记变量

char work_area[WORK_SIZE]; //缓冲区

void *thread_function(void *arg)//线程功能函数

{

    sleep(1);

    pthread_mutex_lock(&work_mutex);//加锁,保证每次输入后都能进入该线程处理

    while (strncmp("end",work_area,3)!=0)//判断是否结束

    {

        printf("You input %d characters\n",strlen(work_area)-1);//输出字符串长度

        work_area[0]='\0';//相当于清空缓冲区

        pthread_mutex_unlock(&work_mutex);//解锁互斥量

        sleep(1);

        

       //判断是否在主线程执行前又再次执行该线程

        pthread_mutex_lock(&work_mutex);

        while (work_area[0]=='\0')

        {

            pthread_mutex_unlock(&work_mutex);

            sleep(1);

            pthread_mutex_lock(&work_mutex);

        }

    }

    time_to_exit=1;//更新退出标记

    work_area[0]='\0';

    pthread_mutex_unlock(&work_mutex);

    pthread_exit(0);//线程退出

}

int main()

{

   

    int res;

    pthread_t a_thread;    //线程标志符

    void *thread_result;

    res=pthread_mutex_init(&work_mutex,NULL);//初始化互斥量

    if(res!=0)

    {

        perror("Mutex initialization failed");

        exit(EXIT_FAILURE);

    }

    res=pthread_create(&a_thread,  NULL,thread_function,  NULL);

    if (res!=0)

    {

        perror("Thread creation failed");

        exit(EXIT_FAILURE);

    }

    

    pthread_mutex_lock(&work_mutex);//加锁保证输出和输入的原子性

    printf("Input some text,Enter 'end'to finish\n");

    while (!time_to_exit)

    {

        fgets(work_area,WORK_SIZE,stdin);

       //输入成功后即可解锁

        pthread_mutex_unlock(&work_mutex);

        

       //避免在功能线程未执行前再次调用主线程,这里需要进行处理

        while (1)

        {

            pthread_mutex_lock(&work_mutex);

            if(work_area[0]!='\0')//功能线程函数还没有处理完毕

            {

                pthread_mutex_unlock(&work_mutex);

                sleep(1);

            }

            else

                break;   //处理完毕退出

        }

    }

    pthread_mutex_unlock(&work_mutex);

    

    printf("Waiting for thread to finish...\n");

    res=pthread_join(a_thread, &thread_result);//等待功能线程退出

    if(res!=0)

    {

        printf("Thread join failed\n");

        exit(EXIT_FAILURE);

    }

    printf("Thread joined\n");

    pthread_mutex_destroy(&work_mutex);//销毁互斥量

    return 0;

}

 

3.一个子父进程的简单对话,利用消息队列传递信息,用信号量实现实现交替发送消息,首先子进程发送消息,父进程接收到消息,然后回消息,然后子进程再回消息,交替进行,输入“exit”退出对话。

这个是自己瞎折腾想的,写的并不怎么好,子父进程对话似乎没有什么问题,但是在退出的时候就会有问题,如果子进程发送“exit”,会出现p操作失败,提示信号量标识被删除的提示(Identifier removed),父进程如果先发送"exit"会出现Interrupted system call,在终端还有可能无法退出来,后来查资料也没有得出个所以然,后来我在后面添加了wait,等待子程序退出后,程序一切都变得正常了,难道是父进程先退出,然后子进程成孤儿了,然后就在后台默默的执行,但是我明明让它退出来的呀,还得多磨练磨练。。。

 

信号量的控制:

两个信号量  sem1=0,sem2=1;

子进程:

p(sem2)

读取消息,显示,发送消息

v(sem1)

 

父进程:

p(sem1)

读取消息,显示,发送消息

v(sem2)

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <sys/types.h>

#include <unistd.h>

#include <pthread.h>

#include <time.h>

#include <sys/ipc.h>

#include <sys/sem.h>

#include <semaphore.h>

#include <sys/msg.h>

#include <errno.h>

#define BUF_SIZE 1024

//定义消息队列结构

struct msgbuf

{

    long mtype;          //消息类型

    char mtext[BUF_SIZE]; //消息缓冲区

    

};

/*

union semun

{

    int val;

    struct semid_ds *buf;

    unsigned short *array;

};

*/

//初始化信号量

int init_sem(int sem_id,int id,int val)

{

    union semun sem_union;

    sem_union.val=val;      //信号量的值

    if(semctl(sem_id, id, SETVAL,sem_union)==-1)

    {

        perror("init fail...");

        return -1;

    }

    return 0;

}

// p操作函数

int sem_p(int sem_id,int id)

{

    struct sembuf sem_b;

    sem_b.sem_num=id;   //信号量编号

    sem_b.sem_op=-1;  //信号量操作,取值为-1表示p操作

    sem_b.sem_flg=SEM_UNDO; //在进程没有释放信号量而退出时,系统自动释放进程中未释放的信号量。

    if(semop(sem_id, &sem_b, 1)==-1)  //进行p操作

    {

    

        perror("p fail...");

        return -1;

    }

    return 0;

}

// v操作函数

int sem_v(int sem_id,int id)

{

    struct sembuf sem_b;

    sem_b.sem_num=id;   //信号量编号

    sem_b.sem_op=1;  //信号量操作,取值为+1表示v操作

    sem_b.sem_flg=SEM_UNDO; //在进程没有释放信号量而退出时,系统自动释放进程中未释放的信号量。

    if(semop(sem_id, &sem_b, 1)==-1)  //进行p操作

    {

    

        perror("p fail...");

        return -1;

    }

    return 0;

}

//删除信号量

int del_sem(int sem_id,int id)

{

    if(semctl(sem_id, id, IPC_RMID,0)==-1)

    {

        perror("delete fail...");

        return -1;

    }

    return 0;

}

int main()

{

    pid_t pid;

    int qid;    //消息队列标识

    int sem_id;//信号量标识

    key_t key;  //键值

    struct msgbuf buf; //消息缓冲区

    

    key=ftok("/home", 'y');    //生成信号量的键值

    sem_id=semget(key,  2, IPC_CREAT|0666);  //创建一个信号量集

    if (sem_id<0)

    {

        perror("semget error");

        exit(1);

    }

    key=ftok("/home", 'm');    //生成消息队列的键值

    qid=msgget(key,  IPC_CREAT|0666);//创建一个消息队列

    if(qid<0)

    {

        perror("msgget error");

        exit(1);

    }

    init_sem(sem_id,  0,0);  //初始化信号量0

    init_sem(sem_id,  1,1);  //初始化信号量1

    

    pid=fork();

    if(pid<0)

    {

        printf("fail...\n");

        exit(EXIT_FAILURE);

    }

    else if(pid==0) //子进程

    {

        

        while (1)

        {

            sem_p(sem_id,1);   //p操作

            buf.mtext[0]='\0';

            msgrcv(qid, &buf,BUF_SIZE, 0, IPC_NOWAIT); //读取消息队列中的一个的消息

            if (strncmp(buf.mtext, "exit",4)==0)  //如果键盘输入exit,退出循环

            {

                strcpy(buf.mtext, "exit");

                buf.mtype=getpid();

                msgsnd(qid, &buf,BUF_SIZE , IPC_NOWAIT);  //向消息队列中发送一个消息

                sem_v(sem_id,0);  //v操作

                break;

            }

            if(buf.mtext[0]!='\0')

            {

                printf("父进程:%s\n",buf.mtext);

            }

            printf("子进程说:");

            fgets(buf.mtext,BUF_SIZE,stdin);  //从键盘输入消息的内容

            buf.mtext[strlen(buf.mtext)-1]='\0';

            buf.mtype=getpid();

            msgsnd(qid, &buf,BUF_SIZE , IPC_NOWAIT);  //向消息队列中发送一个消息

            sem_v(sem_id,0);  //v操作

        }

        exit(EXIT_SUCCESS);

    }

    else if(pid>0)  //父进程

    {

        while (1)

        {

            sem_p(sem_id,0);

            buf.mtext[0]='\0';

            msgrcv(qid, &buf,BUF_SIZE, 0, IPC_NOWAIT); //读取消息队列中的一个的消息

            if (strncmp(buf.mtext, "exit",4)==0)  //如果键盘输入exit,退出循环

            {

                strcpy(buf.mtext, "exit");

                buf.mtype=getpid();

                msgsnd(qid, &buf,BUF_SIZE , IPC_NOWAIT);  //向消息队列中发送一个消息

                sem_v(sem_id,1);

                break;

            }

            if(buf.mtext[0]!='\0')

            {

                printf("子进程:%s\n",buf.mtext);

            }

            printf("父进程说:");

            fgets(buf.mtext,BUF_SIZE,stdin);  //从键盘输入消息的内容

            buf.mtext[strlen(buf.mtext)-1]='\0';

            buf.mtype=getpid();

            msgsnd(qid, &buf,BUF_SIZE , IPC_NOWAIT);  //向消息队列中发送一个消息

            sem_v(sem_id,1);

        }

        wait(NULL);

        del_sem(sem_id,1); //删除信号量

        msgctl(qid,  IPC_RMID, NULL);  //删除消息队列

    }

    

    return 0;

}

 

 

 

 

待续。。。

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

线程(进程)的同步与互斥实例 的相关文章

  • 线程的基本概念,线程的同步互斥机制

    一 线程的概念 1 1 什么是线程 线程 线程是进程的一个实体 是被系统独立调度和分派的基本单位 是一个进程并发执行多个任务的机制 并发 单核CPU多任务同时运行 CPU以ms级进行进程调度 1 2 为什么引入线程 进程间的切换表现为上下文
  • 调度器简介,以及Linux的调度策略

    进程是操作系统虚拟出来的概念 用来组织计算机中的任务 但随着进程被赋予越来越多的任务 进程好像有了真实的生命 它从诞生就随着CPU时间执行 直到最终消失 不过 进程的生命都得到了操作系统内核的关照 就好像疲于照顾几个孩子的母亲内核必须做出决
  • 多线程、反射

    目录 线程 实现线程 死锁 反射 注解 多线程 在Java思想中 将并发完成的每一件事情称为线程 java语言提供了并发机制 程序猿可以在程序中执行多个线程 每一个线程完成一个功能 并与其他线程并发执行 这种机制称为多线程 一个简单的线程代
  • 进程调度的控制—文件锁

    前言 在进程之间 我们不能决定应该先去调度哪一个进程 这时候我们就需要对一个文件加锁 来要求那个先来执行 整体文件锁 int flock int fd int operation fd 文件描述符 operation 定义的宏 也就是选项
  • QT多线程(QThread)小结

    QThread只有run函数是在新线程里的 其他所有函数都在QThread生成的线程里 如果QThread是在ui所在的线程里生成 那么QThread的其他非run函数都是和ui线程一样的 所以 QThread的继承类的其他函数尽量别要有太
  • 一个Chen系统的激活控制同步——MATLAB实现

    对于 C h e n Chen Chen系统 驱动系统定义为
  • Java中的wait()与notify()/notifyAll()

    1 wait 与sleep yield 的不同 调用sleep 和yield 时 线程掌握的对象上的锁不会被释放 而调用wait 时 线程掌握的对象上的锁会被释放掉 这一点是需要注意的 也是有意义的 因为调用wait 会释放锁 所以在一个s
  • c#中代码中多线程动态创建progressbar的实例,概念很重要可扩展很多类似概念

    以下是代码中创建progressbar的实例 int count 0 private void button4 Click object sender EventArgs e Thread th new Thread gt Form for
  • 【Linux】进程地址空间

    需要云服务器等云产品来学习Linux的同学可以移步 gt 腾讯云 lt gt 阿里云 lt gt 华为云 lt 官网 轻量型云服务器低至112元 年 新用户首次下单享超低折扣 目录 一 虚拟地址 二 对进程地址空间的理解 三 32位下的进程
  • 如何自动更新-CMD

    不打算讲得很细 我本人能看懂就行 现在客户端自动更新 一般要用到更新程序 这样就是两个项目 两个exe了 所以对我这来说比较反感 所以就选择了另外一种方法 cmd 由于是自动更新客户端 且是内置的 所以要关闭自身 但是cmd在process
  • Java基础总结之各个模块重要知识点

    一 对象模块 一 初始化 1 对this super 构造函数 构造代码块 静态代码块总结 this 代表当前对象 也就是所在函数所属对象的引用 this对象后面加 调用的是对象的成员变量和方法 this say this对象后面加 调用的
  • 网络同步与异步概念整理

    在网络同步中 有两种同步方式 分别为同步与异步 同步的操作指的是 当所有的操作请求都做完 才将结果返回给用户 用户才能进行下一个操作 这样就会让用户有一种卡顿的感觉 因为需要等待上一步操作的执行结果 异步操作指的是 用户的操作之间不需要进行
  • ES recovery、主副分片复制会有一段时间block写入?

    先说结论 1 ES在主副本分片复制时候不会block写入 version gt 2 x 2 ES在recovery主分片时候会有一段时间block写入 全文 ES recovery 主副分片复制会有一段时间block写入 阿里云开发者社区E
  • JDK1.8 AbstractQueuedSynchronizer的实现分析(上)

    深度解析Java 8 JDK1 8 AbstractQueuedSynchronizer的实现分析 上 作者 刘锟洋 发布于 2014年7月31日 http www infoq com cn articles jdk1 8 abstract
  • MFC之AfxbeginThread 线程 创建、挂起、释放、结束、退出

    MFC之AfxbeginThread 线程 创建 挂起 释放 结束 退出 本文简单阐述了如何使用一个afxbeginthread创建一个线程 两种方法 使用默认设置 使用自主设置参数 以及一些如同 挂起 释放 边界锁等操作 h文件添加声明
  • 线程连接池

    第一种 Executors newCacheThreadPool 可缓存线程池 先查看池中有没有以前建立的线程 如果有 就直接使用 如果没有 就建一个新的线程加入池中 缓存型池子通常用于执行一些生存期很短的异步型任务 package tes
  • fork之后子进程到底复制了父进程什么

    fork之后子进程到底复制了父进程什么 发表于2015 4 3 9 54 08 2161人阅读 分类 操作系统 include
  • 条件变量(condition variable)详解

    原理 假设我们需要解决这样一个问题 一个列表记录需要处理的任务 一个线程往此列表添加任务 一个线程processTask处理此列表中的任务 这个问题的一个关键点在于processTask怎么判断任务列表不为空 一般有两种方法 一 proce
  • Java线程的6种状态及切换(透彻讲解)

    Java中线程的状态分为6种 1 初始 NEW 新创建了一个线程对象 但还没有调用start 方法 2 运行 RUNNABLE Java线程中将就绪 ready 和运行中 running 两种状态笼统的称为 运行 线程对象创建后 其他线程
  • 什么是SSL协议?

    转自 微点阅读 https www weidianyuedu com 什么是SSL协议 SSL协议是一种安全传输协议 SSL是SecureSocketLayer的缩写 即安全套接层协议 该协议最初由Netscape企业发展而来 目前已经成为

随机推荐

  • Java--垃圾回收机制

    垃圾回收算法思想 引用计数 标记清除 先标记 然后再整理 会存在效率低下的问题 存在内存碎片 进而提前触发GC 复制拷贝 将内存区域分为两块 一块存储对象 如果对象满了 那么将存活的对象移动到另外的一块区域中 新生代中 空间利用率底 标记整
  • win11系统之安装MySQL8.0版本

    win11系统之安装MySQL8 0版本 一 本次实践介绍 1 1 MySQL简介 1 2MySQL8 0特点 1 2 环境介绍 二 下载MySQL软件包 2 1 MySQL官网 2 2 下载MySQL安装包 三 环境配置工作 3 1 解压
  • en结尾的单词_en后缀形容词——动词

    形容词 动词 名词 直接加en Bright brighten brightness Broad broaden broadness Dark darken darkness Deaf deafen deafness Deep deepen
  • 【正点原子STM32连载】第五十七章 USB读卡器(Slave)实验 摘自【正点原子】MiniPro STM32H750 开发指南_V1.1

    1 实验平台 正点原子MiniPro H750开发板 2 平台购买地址 https detail tmall com item htm id 677017430560 3 全套实验源码 手册 视频下载地址 http www openedv
  • java文档注释

    1 3注释 1 为什么会有注释 如果一个工程的代码量较大 代码过于繁杂 可能会在编写的过程中忘记了某行或者某段的用途 可以通过注释帮助编写者回忆起代码的用处 也可以让后来者理解明白编写者写代码的用途 方法或者其他用途 2 注释的特点 不参与
  • 微服务负载均衡器Ribbon实战

    1 什么是Ribbon 目前主流的负载均衡方式有两种 集中式负载均衡 在客户端和服务端中间建立一个独立的代理来做负载均衡 硬件比如F5 软件比如Nginx 根据客户自己的情况做负载均衡 Ribbon 就属于这一种 Spring Cloud
  • 互联网行业潜规则:宁花11k招新人,不花9k留老人

    最近一位互联网从业者发出这样的感慨 有些互联网公司 宁愿花11k招新人 也不愿意花9k留住老员工 为什么 对此 许多网友给出了答案 有的网友说 因为老员工的能量已经耗尽 再也不能为公司创造更高的价值 有的网友说 因为只要给一个老员工涨工资
  • Vmware Workstation Pro16安装

    Vmware Workstation Pro16安装 一 右击打开安装包 二 下一步 三 勾选 我接收许可协议中的条款 点击 下一步 四 修改安装路径 五 两个复选框都取消勾选 六 下一步 七 安装 八 许可证 九 完成
  • 线性表的查找算法-C语言

    文章目录 一 实验目的 二 实验内容 三 实验工具 四 实验代码 五 实验结果 六 总结与思考 一 实验目的 了解查找的基本概念 理解顺序查找 折半查找和分块查找的思想 掌握折半查找过程的判定树构造方法 实现线性表的查找算法 二 实验内容
  • mybatis中的if-else语句的使用解答

    1 mybatis中if else语句的语法 使用模板样例
  • 什么是沙箱技术?与容器有什么区别

    沙箱和容器的 隔离 机制 首先 什么是沙箱 它本身就是一种线下生活现象的虚拟化 现实世界里 小孩子们在沙地 沙滩上用木板隔离出一个方盒子 在盒子里堆砌 创造各种东西 城堡 房屋 山丘 这就是一个沙箱 它有两个根本特点 一 它有边界 通过木板
  • aix oracle 11 补丁包,oracle 11g for aix6.1安装基本步骤(含升级11.1.0.7)

    oracle 11g for aix6 1安装基本步骤 含升级11 1 0 7 1 检查物理内存 swap空间以及tmp空间 usr sbin lsattr E l sys0 a realmem 检查内存至少1G usr sbin lsps
  • anaconda创建python环境

    1 前提 系统中安装了anaconda沙箱环境 下载地址 anaconda官网 conda V 检验是否安装以及当前conda的版本 2 conda常用的命令 1 conda list 查看安装了哪些包 2 conda env list 或
  • bcd码和十进制码之间的转换

    BCD码转十进制 static u8 BCDToInt u8 value unsigned char temp 0 temp value gt gt 4 10 temp value 0x0F return temp 十进制转BCD码 sta
  • java awt linux_解决在linux下awt调用错误的问题

    在java中使用awt在服务器上处理图片的时候发现有错 第一遍执行 500 Servlet Exception java lang InternalError Can t connect to X11 window server using
  • MyBatis自动生成实体类

    MyBatis MySQL生成实体类 需要的工具jar包 mybatis generator core 1 3 2 jar mysql connector java 5 0 4 jar 第一步 编写一个MybatisGeneratorUti
  • android 之 如何让app没有图标

    我们有时候需要让我们的app没有图标 不要问我没有图标要干啥 我只是说的一个需求 单讲技术不说别的 首先我们要获得 PackageManager 的对象 PackageManager packageManager getPackageMan
  • Caffeine缓存的使用

    1 springboot集成Caffeine
  • KeePass搭建一个私人密码库

    文章作者 GoodBoyboy 文章链接 https blog goodboyboy top posts 2546190081 html 版权声明 本博客所有文章除特别声明外 均采用 CC BY NC SA 4 0 许可协议 转载请注明来自
  • 线程(进程)的同步与互斥实例

    1 有一个队列 线程1负责从网络接收分组 并将收到的分组放到队列尾 然后再次从网络中接收下一个到达的分组 并进行同样的队列操作 线程2从此队列头中取出一个分组进行处理 处理完毕后 再次从队列中取出一个分组进行处理 处理完毕后再次从队列头取出