Linux 多线程编程入门:线程基础、创建、生产者消费者与线程安全
基本知识
线程:进程内部的一条执行路径,主线程结束则进程结束,其他线程没运行完也不能运行了。两个线程如果实现功能相同,则可以用同一个线程函数
线程并发运行:多个线程同时执行;作用:一个进程可能既需要接收键盘的数据,又需要接收网络传递的数据
对于多线程程序,folk操作之后 ,父进程会执行两条执行路径,fork函数在哪个线程中,子进程就会执行哪条线程,即子进程只有一条执行路径
线程分类
操作系统角度分析,线程实现分三种:
用户级线程(并发,同时发生,一个处理器,用户空间有多个线程,在内核中只有一条处理路径);
内核级线程(并行,同时执行,内核中有多条处理路径,多个处理器);
混合模型(用户级线程+内核级线程)
并发与并行区别:
并发:多个线程在一个处理器上交替执行,多个任务交替执行,看起来同时跑,实际是轮流跑。
并行:多个处理器同时执行,多个任务真正同时执行,必须多核 CPU。
普通创建线程
编译需要链接线程库:
gcc -o test test.c -pthreadtest.c文件
void *fun(void *arg)//线程函数 { for (int i = 0; i < 7; i++) { printf("fun run\n"); sleep(1); } pthread_exit("fun exit");//退出线程 } int main() { pthread_t id; //定义线程id pthread_create(&id, NULL, fun, NULL);//创建线程 //第一个参数传id的地址,创建成功后给id赋值;最后一个参数是交给fun函数的参数 for (int i = 0; i < 2; i++) { printf("main run\n"); sleep(1); } char* s = NULL; pthread_join(id, (void**)&s);//等待线程结束 //最后一个参数传二级指针,指向线程函数想返回给主函数的信息(&s:让s指向"fun exit") printf("s=%s\n",s); }代码执行:主线程先循环2次(2s),子线程一共循环7次(7s),主线程循环结束后阻塞在 pthread_join ,等子线程全部跑完才继续执行printf。
生产者消费者问题
生产者消费者问题,也称为有限缓冲区问题
图解
多个生产者消费者可以并发就绪/运行,但同一瞬间,只有一个生产者在写缓冲区,不会并发写,因为互斥锁控制访问缓冲区的权限,写要申请锁,别人不能再申请
两个生产者可以都先p,但只有一个先设锁,另一个生产者先等着,同时3个消费者也等着。设锁的生产者操作完之后arr位置以已经往后挪了一位
先p再加锁:先加锁再p操作若为0则会阻塞,导致死锁
代码
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <pthread.h> #include <semaphore.h> #define BUFF_MAX 30 #define SC_NUM 2 #define XF_NUM 3 int buff[BUFF_MAX] = {0};//缓冲区 int in = 0;//产生数据的位置 int out = 0;//消费数据的位置 sem_t sem_empty;//代表空格子的数目,生产者参考 sem_t sem_full;//代表有数据的格子数目,消费者参考 pthread_mutex_t mutex;//控制对缓冲区的互斥性访问,互斥锁 void* sc_fun(void* arg) { for(int i = 0; i < 30; i++) { sem_wait(&sem_empty);//p(s1) pthread_mutex_lock(&mutex);//加锁 buff[in] = rand() % 100;//产生数据 printf("生产者在%d,产生数据:%d\n",in,buff[in]); in = (in + 1) % BUFF_MAX; pthread_mutex_unlock(&mutex);//解锁 sem_post(&sem_full);//v(s2) sleep(1); } } void* xf_fun(void* arg) { for(int i = 0; i < 20; i++) { sem_wait(&sem_full);//p(s2) pthread_mutex_lock(&mutex);//加锁 printf("-----消费者在%d,位置打印 %d\n",out,buff[out]); out = (out + 1) % BUFF_MAX; pthread_mutex_unlock(&mutex);//解锁 sem_post(&sem_empty);//v(s1) sleep(1); } } int main() { pthread_mutex_init(&mutex,NULL); sem_init(&sem_empty,0,BUFF_MAX);//s1 sem_init(&sem_full,0,0);//s2 pthread_t sc_id[SC_NUM];//生产者线程组 pthread_t xf_id[XF_NUM];//消费者线程组 for(int i = 0; i < SC_NUM; i++) { pthread_create(&sc_id[i],NULL,sc_fun,NULL);//创建两个生产者 } for(int i = 0; i < XF_NUM; i++) { pthread_create(&xf_id[i],NULL,xf_fun,NULL);//创建三个消费者 } //等待线程结束 for(int i = 0; i < SC_NUM;i++) { pthread_join(sc_id[i],NULL); } for(int i = 0; i < XF_NUM;i++) { pthread_join(xf_id[i],NULL); } sem_destroy(&sem_empty); sem_destroy(&sem_full); pthread_mutex_destroy(&mutex); exit(0); }线程安全
线程安全,即程序运行正确性,指对于多线程程序,无论调度顺序如何,都能得到正确的结果
保证线程安全
- 同步:信号量,互斥锁,读写锁,条件变量
- 使用线程安全的函数(也称为可重入函数)
举例:通过条件变量实现线程安全
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> pthread_mutex_t mutex;//互斥锁 pthread_cond_t cond;//条件变量 char buff[128] = {0};//存放键盘输入的数据 void *funa(void* arg)//线程函数a { while(1) { pthread_mutex_lock(&mutex);//加锁 pthread_cond_wait(&cond, &mutex); //wait:放在等待队列中,先进行解锁操作,解锁前阻塞,唤醒返回时进行加锁操作 pthread_mutex_unlock(&mutex);//解锁 if( strncmp(buff, "end", 3) == 0 ) { break; } printf("funa buff=%s\n",buff); } } void *funb(void* arg)//线程函数b { while(1) { pthread_mutex_lock(&mutex); pthread_cond_wait(&cond, &mutex); pthread_mutex_unlock(&mutex); if( strncmp(buff, "end", 3) == 0 ) { break; } printf("funb buff=%s\n",buff); } } int main() { pthread_mutex_init(&mutex, NULL); pthread_cond_init(&cond, NULL); pthread_t ida, idb; pthread_create(&ida, NULL, funa, NULL); pthread_create(&idb, NULL, funb, NULL); while(1) { fgets(buff, 128, stdin); if( strncmp(buff, "end", 3) == 0 ) { pthread_mutex_lock(&mutex); pthread_cond_broadcast(&cond);//唤醒所有线程函数 pthread_mutex_unlock(&mutex); break; } else { pthread_mutex_lock(&mutex); pthread_cond_signal(&cond);//仅唤醒一个线程函数 pthread_mutex_unlock(&mutex); } } pthread_join(ida, NULL); pthread_join(idb, NULL); pthread_mutex_destroy(&mutex);//销毁锁 pthread_cond_destroy(&cond);//销毁条件变量 exit(0); }wait、signal和broadcast唤醒前都要先加锁。原因:保护wait操作的原子性;不允许多个线程同时对等待队列进行操作,保证同步性
进程和线程的区别
本质定义
进程:资源分配的独立单位,是程序运行的完整独立容器(有独立内存、CPU时间、文件句柄等)。
线程:进程内的执行单位,共享所属进程的全部资源,只独有栈、寄存器。
关键核心差异
- 内存隔离
进程:完全独立,互不干扰,一个崩不影响其他。
线程:共享进程堆/全局内存,一个线程出错,整个进程易崩溃。
- 开销成本
进程:创建、销毁、切换开销极大(要复刻内存资源)。
线程:轻量,创建切换开销极小(只改执行上下文)。
- 通信方式
进程:复杂(管道、socket、共享内存,需系统调用)。
线程:简单,直接读写共享全局变量/堆内存即可。
- 资源归属
进程:独占内存、端口、文件描述符。
线程:共用进程资源,仅私有栈空间。
- 调度层级
进程:操作系统内核级调度。
线程:内核/用户态均可调度,切换更快。
一句话总结
进程=独立工厂,有自己的厂房物料;线程=工厂里的工人,共用厂房物料,分工干活。
有栈空间。
- 调度层级
进程:操作系统内核级调度。
线程:内核/用户态均可调度,切换更快。
一句话总结
进程=独立工厂,有自己的厂房物料;线程=工厂里的工人,共用厂房物料,分工干活。