C语言|C语言 之 多线程编程
一、基础知识
- 计算机的核心是CPU,承担了所有的计算任务。
- 操作系统是计算机的管理者,负责任务的调度、资源的分配和管理,统领整个计算机硬件。
- 应用程序则是具有某种功能的程序,程序是运行于操作系统之上的。
进程由程序、数据集合和进程控制块三部分组成:
- 程序:描述进程要完成的功能,是控制进程执行的指令集;
- 数据集合:程序在执行时所需要的数据和工作区;
- 程序控制块:(Program Control Block,简称PCB),包含进程的描述信息和控制信息,是进程存在的唯一标志。
线程: 线程的一些好处:(个人理解,保留质疑!)
在进程为任务调度的最小单位时,但进程遇到堵塞时,操作系统会切换其它的进程进行处理。但由于进程不仅是调度的基本单位,同时还是资源分配的独立单位,所以对进程进行切换时,开销会比较大。为了减小切换时的开销,将任务调度的最小单位这个责任交给了线程,进程依然是资源分配的单位。
线程的基本理解:
- 是程序执行中一个单一的顺序控制流程
- 是程序执行流的最小单元
- 是处理器调度和分派的基本单位
二、线程的创建 在C语言中,使用pthread_create函数创建一个线程。该函数定义在头文件pthread.h中,函数原型为:
int pthread_create(pthread_t *restrict tidp,const pthread_attr_t *restrict attr,void *(*start_rtn)(void *),void *restrict arg);
介绍:
- 参数1:存储线程ID,线程的句柄,可通过该变量操纵指向的线程;
- 参数2:线程的属性,默认且一般是NULL;
- 参数3:一个函数用于给新创建的线程去执行;
- 参数4:参数3中的函数的传入参数。不需要则为NULL;
- 返回值:成功返回0,失败则返回错误编号;
另一个比较重要的函数:pthread_join()
- 函数原型:int pthread_join(pthread_t thread,void**retval);
- 功能:等待第一个参数的线程执行完成后,去执行retval指向的函数(起到线程同步的作用)
先开始我们C语言多线程编程的第一个小程序吧!
文章图片
文章图片
演示代码:#include#include#include void* Print(char* str){printf("%s ",str); } int main(){pthread_t thread1,thread2; pthread_create(&thread1,NULL,(void*)&Print,"Hello"); pthread_create(&thread2,NULL,(void*)&Print,"World"); return 0; }
View Code
!在编译时,pthread_create函数会报未定义引用的错误:
文章图片
在解决报错后,得到了可执行文件。但在运行时,却看不到任何输入。Why?这里涉及到条件竞争的概念了,使用pthread_create函数创建了两个线程,两个线程创建后,并不影响主线程的执行,所以这里就存在了三个线程的竞争关系了。可见,似乎主线程执行return 0; 先于另外两个线程的打印函数。主线程的退出会导致创建的线程退出,所以我们看不见它们的输出。
文章图片
那么,为了使return 0语句慢点执行,可以采用sleep()函数进行延迟。
文章图片
可以看到有打印了输出,但有World Hello和Hello World两种情况,也是因为竞争的原因。
三、线程同步与互斥锁机制 在遇到条件竞争的问题中,上面采用sleep()函数进行延迟似乎也能解决问题。但实则不然,采用sleep()的弊端很是明显:
- 不能判断延迟的时间长度,加上每次执行都会有所改变,更加不可控。
- 会使程序执行卡顿,缺乏紧凑。
互斥锁机制: 通过访问时对共享资源加锁的方法,防止多个线程同时访问共享资源。锁有两种状态:未上锁和已上锁。在访问共享资源时,进行上锁,在访问结束后,进行解锁。若在访问时,共享资源已被其它线程锁住了,则进入堵塞状态等待该线程释放锁再继续下一步的执行。这种锁我们称为互斥锁。
通过锁机制,前面的代码不难进行改变,这里将不进行描述。下面将介绍一下生产者消费者模型,为了进一步演示锁机制。
互斥锁相关函数介绍:1、pthread_mutex_init :初始化一个互斥锁。函数原型:int pthread_mutex_init(pthread_mutex_t*mutex,constpthread_mutexattr_t*attr); 2、pthread_mutex_lock:若所访问的资源未上锁,则进行lock,否则进入堵塞状态。函数原型:intpthread_mutex_lock(pthread_mutex_t*mutex); 3、pthread_mutex_unlock:对互斥锁进行解锁。函数原型:intpthread_mutex_unlock(pthread_mutex_t*mutex); 4、pthread_mutex_destroy:销毁一个互斥锁。函数原型:intpthread_mutex_destroy(pthread_mutex_t*mutex);
生产者消费者模型: 生产者和消费者在同一时间段内共用同一个存储空间,生产者往存储空间中生成产品,消费者从存储空间中取走产品。当存储空间为空时,消费者阻塞;当存储空间满时,生产者阻塞。(下面代码中存储空间为1)
文章图片
文章图片
演示代码test02.c:#include#include#include int buf = 0; pthread_mutex_t mut; void producer(){while(1){pthread_mutex_lock(&mut); if(buf == 0){buf = 1; printf("produced an item.\n"); sleep(1); }pthread_mutex_unlock(&mut); }} void consumer(){while(1){pthread_mutex_lock(&mut); if(buf == 1){buf = 0; printf("consumed an item.\n"); sleep(1); }pthread_mutex_unlock(&mut); }} int main(void){pthread_t thread1,thread2; pthread_mutex_init(&mut,NULL); pthread_create(&thread1,NULL,&producer,NULL); consumer(&buf); pthread_mutex_destroy(&mut); return 0; }
生产者消费者模型演示代码
执行结果:
文章图片
从执行结果可以看出,运行顺序井然有序。生产后必是消费,消费完后必是生产。由于互斥锁机制的存在,生产者和消费者不会同时对共享资源进行访问。
四、信号量机制 上面了解到的互斥锁有两种状态:资源为0和1的状态。当我们所拥有的资源大于1时,可以采用信号量机制。在信号量机制中,我们有n个资源(n>0)。在访问资源时,若n>=1,则可以访问,同时信号量-1,否则堵塞等待直到n>=1。其实互斥锁可以看出信号量的一种特殊情况(n=1)。
信号量相关函数的介绍:
头文件:semaphore.h
1、sem_init函数:初始化一个信号量。
函数原型:int sem_init(sem_t* sem, int pshared, unsigned int value);
参数:
- sem:指定了要初始化的信号量的地址;
- pshared:如果其值为0,就表示信号量是当前进程的局部信号量,否则信号量就可以在多个进程间共享;
- value:指定了信号量的初始值;
2、 sem_post函数:信号量的值加1,如果加1后值大于0:等待信号量的值变为大于0的进程或线程被唤醒。
函数原型:int sem_post(sem_t* sem);
返回值:成功=>0 , 失败=> -1;
3、sem_wait函数:信号量的减1操作。如果当前信号量的值大于0,则可继续执行。如果当前信号量的值等于0,则会堵塞,直到信号量的值大于0.
函数原型:int sem_wait(sem_t* sem);
返回值:成功=>0 , 失败=> -1;
4、sem_destroy函数:销毁一个信号量。
函数原型:int sem_destroy(sem_t* sem);
返回值:成功=>0 , 失败=> -1;
5、sem_getvalue函数:获取信号量中的值。
函数原型:int sem_getvalue(sem_t* sem, int* sval);
获取信号量的值,并放在&sval上。
文章图片
文章图片
#include#include#include#include#includesem_t npro; //还可以生产多少sem_t ncon; //还可以消费多少 //producer functionvoid* producer(void* arg){while(1){int num; sem_wait(&npro); //先判断是否可以生产sem_post(&ncon); //生产一个,可消费数+1sem_getvalue(&ncon,&num); printf("produce one,now have %d items.\n",num); sleep(0.7); } } //consumer functionvoid consumer(void* arg){while(1){int num; sem_wait(&ncon); //判断是否可以消费sem_post(&npro); //消费一个,可生产数+1sem_getvalue(&ncon,&num); printf("consume one,now have %d items.\n",num); sleep(1); } } int main(void){pthread_t thread1,thread2; //init semaphoresem_init(&npro,0,5); //设最大容量为5sem_init(&ncon,0,0); pthread_create(&thread1,NULL,&producer,NULL); consumer(NULL); return 0; }
生产者消费者模型(信号量机制)
运行结果:
文章图片
同样也可以解决条件竞争问题,而且使用范围更广了。
五、小结
- 进程和线程的基础知识
- 线程的创建以及线程存在的条件竞争问题
- pthread_join()函数
- 互斥锁机制
- 信号量机制
参考文章:
http://www.lix.polytechnique.fr/~liberti/public/computing/parallel/threads/threads-tutorial/tutorial.html
C语言多线程编程(一) -知乎(zhihu.com)
报错解决:https://blog.csdn.net/weixin_43876206/article/details/101158947
【C语言|C语言 之 多线程编程】
tolele 2022-04-02
推荐阅读
- 聊聊mysql的多列组合查询
- 新闻速递 I MobTech首席数据官杨冠军受CSDN之邀,探索企业数字化转型最佳路径
- 面试周连续剧之首战大捷
- C语言与C++编程|C++ 并发编程(C++11 到 C++17 )
- TDengine 荣获 CSDN IT 技术影响力之星 “年度开源项目” 、 “年度IT领军人物”奖项
- 做一个分销小程序要多少钱
- C|【C语言|RUNOOB教程】100道经典例题详解(1~5题)
- C语言进阶|C语言进阶(自定义类型)
- Python实现合成多张图片到PDF格式
- java垃圾回收之实现串行GC算法