☘写在前面☘
学习一个语言最好的方法是做一个小项目,这个项目不需要多么复杂,但是一定能激发你的学习兴趣。让我们话不多说,开始吧
本文将带你手撸一个多线程词频统计工具,你将学到
- 📝 如何创建多线程
- 📝 互斥信号量的使用方式
- 📝 单词的统计方法
全文大约阅读时间: 25min
🧑🏻作者简介:一个从工业设计改行学嵌入式的年轻人
🔒资源下载:gitee仓库
✨联系方式:2201891280(QQ)
📔全文目录📔
一、基本要求介绍
🧭目标
完成 多线程协同的词频统计 程序。
🥙形式要求
基于C/C++,完成上述完整功能
👜实现形式
word_count ./prog ,通过遍历线程(2个以上)对输入目录prog中的文件进行递归并行遍历,统计各个文本文件中的各个单词出现的数量,由汇总现场收集各个遍历进程的统计信息进行汇总,并将汇总词频统计信息打印在屏幕上。(注:英文文本文件可以网络上下载英文小说或拷贝英文网页内容生成)
注:此为楼主的一次linux作业,鉴于还没到提交作业的截至日期,所以如果你 借鉴 此篇文章,请修改一定量以防大家都没分数0.0
二、解决方法思路
拿到一道要求时,最重要的是学会找资源,找到一些思路,然后自己去做实现。
所以我平时很喜欢问思路的同学,但是知道思路还不知道代码怎么写呢,我就觉得这是态度问题了,希望大家还是只借鉴思路,千万别直接复制粘贴,这对自己一点好处都没有。
✍参考文章
-
【Linux】【C/C++】多进程协同词频统计
这个是一个多进程的实现方式,其中值得借鉴的是文件的遍历和保存路径的方式。所以主程序的思路可以从这里来。 -
多线程编程—单词统计和热词统计
这篇文章是利用多线程统计热词,然后文件的命名是直接用的1-10这种方式,无法自定义,其实就是一个线程统计一个。所以这里我们可以用这里的子函数统计的方法。
🧇主体实现思路
主线程解析路径并将路径放入内存中,子线程负责取出内存中的路径并完成遍历所有的单词并进行统计。利用vector来保存路径,利用一个map来作为结果的保存。
主线程与子线程的同步
由于是多线程,通信方式选择的是堆区的内存。在主线程执行的时候将所有文件的绝对路径写入vector,用于子函数的遍历。
子线程与子线程的互斥
有两个需要互斥的地方。第一个是从路径列表里面取出一个路径用于遍历。然后是遍历结束后需要返回结果的时候,由于是堆区内存,也要保证互斥的访问。
三、一些必备知识
🧿信号量
完成进程间或者是线程间的互斥或者同步关系的方式主要就是利用信号量。其原理就是利用原子操作进行申请锁。这个锁其实很类似于我们平常的生活。
假如你在使用试衣间,为了防止此时别人闯入,你肯定会做的一件事就是给试衣间加锁。用完了出来了就会打开锁供别人使用。
所谓的原子操作就是不可分割,保证了申请锁过程中的不可打断,从而保证了程序的正确性。
同时,如果信号量申请锁失败,程序会进入休眠状态,释放cpu,这保证了资源的使用效率。
linux中的信号量使用
#include <pthread.h>
pthread_mutex_t mute_txt; //创建信号量
pthread_mutex_init(&mute_txt,NULL);//初始化
pthread_mutex_lock(&mute_txt);//申请锁
pthread_mutex_unlock(&mute_txt);//释放锁
上面就表达了linux下线程的一个互斥锁的使用。因为是所有线程共享的互斥信号量,所以一开始的创建信号量需要是全局变量。然后初始化需要在程序一开始进行,创建好了之后锁默认的就是释放状态。申请锁和释放锁的话需要遵循用时申请,使用完即释放,
就像你在占用资源,你用完还不是赶紧把锁打开让别人用,防止资源的浪费嘛。
linux下线程的创建
#include <pthread.h>
pthread_t *tids = (pthread_t *) malloc( sizeof(pthread_t) * (p_number + 1)); //创建子进程的指针
int* id=(int *)malloc(sizeof(int)*(p_number + 1)); //子进程id
pthread_create(&tids[i], NULL,find, &id[i]);
pthread_join(tids[i],NULL);
其中pthread_t 是子程序运行时的指针变量。我创建的多线程,所以需要创建一个数组。
- 创建线程的第一个参数是线程指针,用于返回线程指针。
- 创建的第二个参数是设置参数,我们不需要就直接NULL。
- 创建的第三个参数是线程函数。其中这个函数需要时void *
- 创建的四个参数是find函数的传参,我有些需要所以传了一个整数。
最后一行是等待线程正常结束返回NULL。
四、主线程的实现
主线程的主要流程图如下,主要功能就是完成初始化以及将所有路径写入到vector中供子线程进行使用。
然后在完成所有的操作后负责调用输出函数来进行结果的输出。
int main(int argc , char* argv[]){
while(pthread_mutex_init(&mute_txt,NULL)!=0); //等待信号量x初始化
while(pthread_mutex_init(&mute_ans,NULL)!=0); //等待信号量y初始化
time_t start,end; //统计运行时间
start = time(NULL); //记录开始时间
char dir[FILE_PATH_LEN];
dir[0] = 0;// 插入结束符号
strcpy(dir,".");//默认路径
if(argc ==2) strcpy(dir,argv[1]); //读入扫描的路径
else{
notice(); //输入不合法 打印输出信息
return 0;
}
if(scandir(dir) == -1) //查看目录下所有的文件
return 0;
pthread_t *tids = (pthread_t *) malloc( sizeof(pthread_t) * (p_number + 1)); //创建子进程的指针
int* id=(int *)malloc(sizeof(int)*(p_number + 1)); //子进程id
if(txtfiles.size() == 0){ //无txt文件
printf("no txt file\n");
return 0;
}
while(txtfiles.size()){
for(int i = 1;i <= p_number;i++) //创建进程
{
id[i] = i; //记录进程id
if(pthread_create(&tids[i], NULL,find, &id[i]) == -1)
printf("create thread failed\n");
}
for(int i=1;i <= p_number;i++) //等待进程结束
{
pthread_join(tids[i],NULL);
//printf("%d exit\n",i); //调试使用
}
}
pthread_mutex_destroy(&mute_txt);
pthread_mutex_destroy(&mute_ans);
free(tids); //释放内存
free(id); //释放内存
print_ans();
end = time(NULL); //记录结束时间
printf("run time : %lf secodes\n", difftime(end, start));//输出运行时间
return 0;
}
五、子线程的实现
其实子线程需要做的工作就很简单了,就是拿到路径,遍历,然后将结果写入到结果map里。这里根据读到的是否是字母来判断,所以只有连续的字母才被判定为单词。
void *find(void *id){
if(!txtfiles.size()) return NULL; //没有待遍历路径
int num = *(int *)id;
printf("hello from : %d\n",num);
while(pthread_mutex_lock(&mute_txt)!=0); //锁定txt路径
string s = txtfiles[txtfiles.size() - 1];
txtfiles.pop_back(); //拿到路径
pthread_mutex_unlock(&mute_txt);
char path[FILE_PATH_LEN];
strcpy(path, s.c_str()); //转为字符串
//printf("%s",path); //调试用
FILE* p = fopen(path,"r"); //打开文件
string word="";
char c;
c=fgetc(p); //读入字符
while(c!=EOF)
{
if((c >= 'a' && c <= 'z')||(c >= 'A' && c <= 'Z')||( c >= '0' && c <= '9')) //字母数字组合都认为是单词
{
word += c;
}
else
{
if(word == "")
{
c=fgetc(p);
continue;
}
while(pthread_mutex_lock(&mute_txt) != 0); //插入字母
mp[word] ++;
sum_num ++;
word = "";
pthread_mutex_unlock(&mute_txt);
}
c=fgetc(p);
}
fclose(p);
return NULL;
}
六、其它补充
这里还有两个小零件没有进行介绍。先来一个简单的
输出结果
其实这个非常简单,就是把map里所有的元素进行输出就好了。但是为了美观。我将结果变成了4位来对其,然后先显示数字再显示单词,然后一行显示print_num 个单词,这个可以手动改来适应屏幕。
void print_ans(){
printf("count : %d\n",sum_num);
map<string,int>::iterator it;
int i = 0;
for(it=mp.begin();it!=mp.end();it++) //输出
{ i++;
printf("%04d : %s ", it->second, (it->first).c_str());
for(int j = 20 - (it->first).size();j > 0;j--) putchar(' ');
if(i % print_num == 0) puts("");
}
if(i % print_num != 0) puts("");
}
然后有一个稍微难亿点点的。遍历路径保存到vector
scandir扫描目录所有文件
其实看图还是很号理解的把?是吧是吧,对比代码看一下把。
int scandir(char *dir){
DIR *dp;
struct dirent *entry; //暂存目录项
struct stat statbuf; //暂存文件信息
if((dp = opendir(dir)) == NULL){ //不能打开相应的文件夹
printf("cant't open dir : %s\n",dir);
return -1;
}
int ignore_a = chdir(dir); //改变当前目录到目的目录
while((entry = readdir(dp)) != NULL){
lstat(entry->d_name,&statbuf);//获取文件信息
if(S_ISDIR(statbuf.st_mode)){ //如果是目录
if((!strcmp(".", entry->d_name) || (!strcmp("..",entry->d_name))))
continue; //如果是. ..就跳过
else
scandir(entry->d_name); //递归扫描
}
else{ //非目录
if(txt(entry->d_name)){//判断是否是txt文件
char path[FILE_PATH_LEN];
char *ignore_c = getcwd(path,sizeof(path)); //写入当前目录
strcat(path,"/");
strcat(path,entry->d_name);//插入当前扫描的txt完整路径
//printf("%s\n",path);
string s(path); //将path转换为字符串
txtfiles.push_back(s);
}
}
}
int ignore_b = chdir(".."); //返回上级
closedir(dp); //关闭文件夹
return 0;
}
七、最终效果
对已知文件的统计结果输出。
对网站随机下载的伊索寓言进行统计结果的输出。
写在最后
这个作业还是有点用的,可以对某个文件目录所有英文单词的出现频率进行统计打印,做完这所有的工作后,回头看看对同步互斥和多线程的编程都有一定的提升。还是有很大的成就感的。希望大家也可以尝试一下。
如果喜欢还希望大家可以点赞收藏啥的。作者会有更大的动力去分享
这个合集应该还会有几部分,对应我的下一个分布式的作业,暂时的计划是做一个bt下载器,只做完了bt解析,我边做边整理边发的话应该会更有助于大家把。
咱们不见不散!