【C语言有什么用?②】制作一个多线程词频统计工具

简介: 【C语言有什么用?②】制作一个多线程词频统计工具

☘写在前面☘

学习一个语言最好的方法是做一个小项目,这个项目不需要多么复杂,但是一定能激发你的学习兴趣。让我们话不多说,开始吧


本文将带你手撸一个多线程词频统计工具,你将学到


📝 如何创建多线程

📝 互斥信号量的使用方式

📝 单词的统计方法

全文大约阅读时间: 25min


🧑🏻作者简介:一个从工业设计改行学嵌入式的年轻人

🔒资源下载:gitee仓库

✨联系方式:2201891280(QQ)


📔全文目录📔

一、基本要求介绍

🧭目标

🥙形式要求

👜实现形式

二、解决方法思路

✍参考文章

🧇主体实现思路

三、一些必备知识

🧿信号量

linux中的信号量使用

linux下线程的创建

四、主线程的实现

五、子线程的实现

六、其它补充

输出结果

scandir扫描目录所有文件

七、最终效果

写在最后

一、基本要求介绍

🧭目标

完成 多线程协同的词频统计 程序。


🥙形式要求

基于C/C++,完成上述完整功能


👜实现形式

word_count ./prog ,通过遍历线程(2个以上)对输入目录prog中的文件进行递归并行遍历,统计各个文本文件中的各个单词出现的数量,由汇总现场收集各个遍历进程的统计信息进行汇总,并将汇总词频统计信息打印在屏幕上。(注:英文文本文件可以网络上下载英文小说或拷贝英文网页内容生成)


注:此为楼主的一次linux作业,鉴于还没到提交作业的截至日期,所以如果你 借鉴 此篇文章,请修改一定量以防大家都没分数0.0


二、解决方法思路

拿到一道要求时,最重要的是学会找资源,找到一些思路,然后自己去做实现。

所以我平时很喜欢问思路的同学,但是知道思路还不知道代码怎么写呢,我就觉得这是态度问题了,希望大家还是只借鉴思路,千万别直接复制粘贴,这对自己一点好处都没有。


✍参考文章

1.【Linux】【C/C++】多进程协同词频统计

这个是一个多进程的实现方式,其中值得借鉴的是文件的遍历和保存路径的方式。所以主程序的思路可以从这里来。


2.多线程编程—单词统计和热词统计

这篇文章是利用多线程统计热词,然后文件的命名是直接用的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解析,我边做边整理边发的话应该会更有助于大家把。

咱们不见不散!


相关文章
|
3月前
|
算法 NoSQL IDE
C语言性能优化:代码优化技巧与工具。
C语言性能优化:代码优化技巧与工具。
97 0
|
3月前
|
安全 Java C语言
C语言线程解池解读和实现01
C语言线程解池解读和实现01
|
16天前
|
安全 程序员 API
|
20天前
|
存储 安全 UED
多线程在打包工具中的运用
【11月更文挑战第2天】本文介绍了多线程技术在打包工具中的应用,包括提高打包效率、优化用户体验和多线程安全考虑。通过并行处理文件和加速资源收集,多线程可以显著缩短打包时间。在用户体验方面,多线程使界面保持响应,并支持优先级处理。此外,文章还讨论了资源访问冲突和死锁预防的解决方案,确保多线程环境下的稳定性和安全性。
|
2月前
|
网络协议 C语言
C语言 网络编程(十四)并发的TCP服务端-以线程完成功能
这段代码实现了一个基于TCP协议的多线程服务器和客户端程序,服务器端通过为每个客户端创建独立的线程来处理并发请求,解决了粘包问题并支持不定长数据传输。服务器监听在IP地址`172.17.140.183`的`8080`端口上,接收客户端发来的数据,并将接收到的消息添加“-回传”后返回给客户端。客户端则可以循环输入并发送数据,同时接收服务器回传的信息。当输入“exit”时,客户端会结束与服务器的通信并关闭连接。
|
2月前
|
存储 Ubuntu Linux
C语言 多线程编程(1) 初识线程和条件变量
本文档详细介绍了多线程的概念、相关命令及线程的操作方法。首先解释了线程的定义及其与进程的关系,接着对比了线程与进程的区别。随后介绍了如何在 Linux 系统中使用 `pidstat`、`top` 和 `ps` 命令查看线程信息。文档还探讨了多进程和多线程模式各自的优缺点及适用场景,并详细讲解了如何使用 POSIX 线程库创建、退出、等待和取消线程。此外,还介绍了线程分离的概念和方法,并提供了多个示例代码帮助理解。最后,深入探讨了线程间的通讯机制、互斥锁和条件变量的使用,通过具体示例展示了如何实现生产者与消费者的同步模型。
|
2月前
|
C语言
C语言 网络编程(九)并发的UDP服务端 以线程完成功能
这是一个基于UDP协议的客户端和服务端程序,其中服务端采用多线程并发处理客户端请求。客户端通过UDP向服务端发送登录请求,并根据登录结果与服务端的新子线程进行后续交互。服务端在主线程中接收客户端请求并创建新线程处理登录验证及后续通信,子线程创建新的套接字并与客户端进行数据交换。该程序展示了如何利用线程和UDP实现简单的并发服务器架构。
|
3月前
|
C语言
【C语言】线程同步
【C语言】线程同步
39 3
|
3月前
|
C语言
【C语言】多线程服务器
【C语言】多线程服务器
30 0
|
3月前
|
程序员 C语言
【C语言】多线程
【C语言】多线程
27 0