多线程编程实例:不带缓冲的多线程文件复制(使用队列,互斥,条件变量)

测试环境:linux12.03

说说这个版本的设计思路:


1.引入对文件分块的思想,例如一个文件大小为10000 bytes,我可能会将其分成100块,每块数据大小为
100 bytes,然后用一个结构把块在文件的起始位置以及块大小记录下来,这些结构用一个队列(ds)管理.

2.主要用到两个线程启动函数,fetch 和 transport
fetch线程主要是根据分块的结果到指定的文件中读取对应的文件块,并存储到一个新的结构中
这个结构把块在文件的起始位置,块大小以及块的内容记录下来,这些结构会用一个队列(db)管理.

3.程序流程:
    1).分析出要读取的文件的大小,将文件分块,存储到一个队列中.
    2).创建线程启动函数为fetch的线程(1到多个),开始到文件中进行取出对应数据块的任务,每取出一个数据块就把
       内容存储到一个结构中,然后把这个结构添加到db队列中.
    3).创建线程启动函数为fetch的线程(1到多个),开始从db队列中取出数据块并且写入到目标新文件的任务,每取出一个数据快就
       立刻进行写入.

4.要解决的线程同步问题:
    1).多个fetch线程之间对ds队列的互斥操作.
    2).多个transport线程之间对db队列的互斥操作.
    3).当transport取出数据太快的话,会导致在fetch线程完成对文件的完全读取之前db队列大小变为0,
        因此要用条件变量去令transport在遇到db队列大小为0的时候首先进行睡眠,等到fetch线程取出新的数据之后
        再去通知transport线程继续取出数据写入到目标文件.


5.与之前写的版本的区别:
    1).fetch线程共享同一个读文件描述符,而不是每个线程都开辟一个
    2).transport线程共享同一个写文件描述符,而不是之前的每个线程都开辟一个.

    3).将工作分成了读取数据和写入数据,而不是同一个线程既负责读取数据,又负责写入数据. 这样更加代码清晰.

#include "unp.h"
#include <pthread.h>
#include <string.h>
// #include <sys/wait.h>
#include <queue>
using namespace std;

pthread_mutex_t fMutex = PTHREAD_MUTEX_INITIALIZER;
// pthread_mutex_t fMutex;
pthread_mutex_t tMutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

pthread_mutex_t exitFlag = PTHREAD_MUTEX_INITIALIZER;

const int BLOCK = 5000;

/*传输数据线程数*/
const int tNumber = 2;

/*帮助transport线程退出标志*/
bool finished = false;

/*从文件中读取数据线程数*/
const int fNumber = 4;

// 数据块结构 包含成员
// start 要传输的数据块在文件中的开始位置
// size  要传输的数据块的大小
// buff  要传输的数据块的内容
typedef struct dataBlock
{
    unsigned long start;
    unsigned long size;
    char buff[BLOCK];

    dataBlock(): start(0), size(0) {}
} dataBlock;


/* 数据段结构 包含成员
   start 要传输的数据块在文件中的开始位置
   size  要传输的数据块的大小*/
typedef struct dataSegment
{
    unsigned long start;
    unsigned long size;
    dataSegment(): start(0), size(0) {}
} dataSegment;

queue<dataBlock> db;
queue<dataSegment> ds;

//到指定文件中根据 数据段队列(ds)的内容 读取指定长度的数据到数据块队列(db)
void *fetchData(void *ptr)
{
    int rno = *(int *)ptr;
    while (true)
    {
        pthread_mutex_lock(&fMutex);
        if (ds.empty())
        {
            pthread_mutex_lock(&exitFlag);
            finished = true;
            pthread_mutex_unlock(&exitFlag);
            pthread_mutex_unlock(&fMutex);
            break;
        }

        /*必须先获得互斥锁 否则会出现段错误
         原因在于系统可能在执行
         db.back().size = ds.front().size;
         这句代码之后发生线程切换
         切换到了transport线程
         此时transport获取tMutex锁,检查发现db的大小不为0
         所以就直接取出db.fonrt的内容,把buff数组的内容写入文件
         但此时由于buff数组内容为空,所以就出现内存访问越界.
         之前我的代码是直接调用db.push(b) 此处b已经读取了内容,
         发现如果这个操作不先获取tMutex的话会发生错误,
         原因在于,db.push(b)的时候,db里面会构建一个新对象,调用复制构造函数,
         把b对象复制过来,而这个复制过程并非原子操作,可能发生线程切换,因此可能会发生上述错误*/

        pthread_mutex_lock(&tMutex);
        db.push(dataBlock());
        db.back().start = ds.front().start;
        db.back().size = ds.front().size;

        lseek(rno, db.back().start, SEEK_SET);
        read(rno, db.back().buff, db.back().size);
        //db队列非空,唤醒transport线程

        pthread_mutex_unlock(&tMutex);
        pthread_cond_signal(&cond);

        ds.pop();
        pthread_mutex_unlock(&fMutex);
    }
}

// 每个线程从数据块队列(db)中拿出数据并且写入到指定的文件的位置
void *transport(void *ptr)
{
    int wno = *(int *)ptr;

    //定时器设置为3秒
    struct timeval tv;
    struct timespec ts;
    gettimeofday(&tv, NULL);
    ts.tv_sec = tv.tv_sec + 3;
    ts.tv_nsec = tv.tv_usec * 1000;

    while (true)
    {
        pthread_mutex_lock(&tMutex);
        /* 考虑在数据块队列为空的情况下
        1.所有数据已经传输完毕
        2.传输线程(transport)的传输数据速度比取数据线程(fetch)的速度要快,这时候当db队列的大小变为0
        的时候,fetch线程仍未把所有的数据取出.
        这时候就要先用条件变量让这个线程先睡眠,直到fetch线程取出了新数据,然后唤醒transport线程继续工作
        之所以要设置超时,是因为如果数据真的是已经传输完毕,那么fetch线程将会结束,不会有再有线程唤醒仍在睡眠中的
        transport线程,这样transport线程就会永远睡眠
        这里设定假如等待3秒后仍未有通知就退出等待 */
        while (db.empty())
        {
            pthread_cond_timedwait(&cond, &tMutex, &ts);
            gettimeofday(&tv, NULL);
            ts.tv_sec = tv.tv_sec + 3;
            ts.tv_nsec = tv.tv_usec * 1000;

            pthread_mutex_lock(&exitFlag);
            if (finished)
            {
                pthread_mutex_unlock(&exitFlag);
                break;
            }
            pthread_mutex_unlock(&exitFlag);
        }

        /* 
        如果此时db仍然为空,数据已经传输完毕,线程结束任务. */
        if (db.empty())
        {
            pthread_mutex_unlock(&tMutex);
            break;
        }
        /*设置偏移量并且写入数据 ps:若fetch线程中 db.push(b);这段代码不添加互斥的话这里会出现段错误 */
        lseek(wno, db.front().start, SEEK_SET);
        printf("%ld\n", db.front().start);
        write(wno, db.front().buff, db.front().size);

        db.pop();
        pthread_mutex_unlock(&tMutex);
    }
}


int main(int argc, char **argv)
{
    /*自行更改文件名称*/
    int rno = open("ss.rar", O_RDONLY);
    struct stat s;
    fstat(rno, &s);

    unsigned long length = s.st_size;
    printf("the length of the file is: %ld bytes\n", length);

    dataSegment g;
    long start = 0;

    //把要读取的文件分段,把分段结果写入到数据块队列(ds)中
    while (1)
    {
        // 最后一块大小少于block 要特别处理
        if (length < BLOCK)
        {
            g.start = start;
            g.size = length;
            ds.push(g);
            break;
        }
        // 块大小等于 block
        g.start = start;
        g.size = BLOCK;
        ds.push(g);
        start += BLOCK;
        length -= BLOCK;
    }

    int wno = open("target.rar", O_WRONLY | O_CREAT | O_TRUNC, S_IWUSR | S_IRUSR);

    /*首先创建取数据线程*/
    pthread_t pFetch[fNumber];
    for (int i = 0; i < fNumber; ++i)
    {
        pthread_create(&pFetch[i], NULL, fetchData, (void *)&rno);
    }

    /*再创建写入数据线程*/
    pthread_t pTran[tNumber];
    for (int i = 0; i < tNumber; ++i)
    {
        pthread_create(&pTran[i], NULL, transport, (void *)&wno);
    }


    /* pthread_join 只能写成这个形式 不能用循环形式*/
    pthread_join(pFetch[0], NULL);
    pthread_join(pFetch[1], NULL);
    pthread_join(pFetch[2], NULL);
    pthread_join(pFetch[3], NULL);
    pthread_join(pTran[0], NULL);
    pthread_join(pTran[1], NULL);


    // for (int i = 0; i < fNumber; ++i)
    // {
    //     pthread_join(pFetch[fNumber], NULL);
    // }

    // for (int i = 0; i < tNumber; ++i)
    // {
    //     pthread_join(pTran[tNumber], NULL);
    // }

    close(rno);
    close(wno);
}




猜你喜欢

转载自blog.csdn.net/lewiskyo/article/details/24923863