一、学习内容
-
多线程基础
1> 线程是任务器调度的基本单位,是进程的一个执行单元
2> 一个进程中可以包含多个线程,但是至少要包含一个线程称为主线程
3> 一个进程中的多个线程共享进程的资源,不会为线程再 单独分配内存空间
4> 线程的切换比进程的切换效率会更高,而且所占用内存会更小(8K左右)
5> 对于多线程编程,需要使用第三方库,pthread库,使用相关函数后,编译程序时,需要加上库的链接: -lpthread
6> 多线程也可以实现多任务的并发执行,并且比多进程效率会更高,所占内存更小,所以在实现并发操作中,一般常用多线程实现,而不是多进程
-
多线程的编程
-
pthread_create(创建线程)
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);-
返回值
成功返回0, 失败返回一个错误码(该错误码是pthread库中定义的错误码,而不是内核定义的错误码) -
参数
参数1:创建的线程的线程号,使用地址传递进行返回给主线程
参数2:线程的属性,一般填NULL,表示使用默认的线程属性
参数3:是线程体函数指针,需要传递一个函数名,该函数返回值类型为void* , 参数为 void *
参数4:当前线程向新线程传递的数据,也就是参数3的参数
-
功能
在当前进程中,启动一个线程
-
-
pthread_self(线程号获取)
pthread_t pthread_self(void);-
返回值
当前线程的线程号 -
参数
无 -
功能
获取当前线程的线程号
-
-
pthread_exit(退出线程)
void pthread_exit(void *retval);-
返回值
无 -
参数
线程退出时的状态,一般填NULL -
功能
退出当前线程
-
-
pthread_join(线程资源的回收)
int pthread_join(pthread_t thread, void **retval);-
返回值
成功返回0,失败返回一个错误码 -
参数
参数1:要回收的线程id号
参数2:接受线程退出时的状态,一般为NULL
-
功能
阻塞等待指定线程的退出,并将其资源吸收到自身身上
-
-
pthread_detach(线程资源回收)
int pthread_detach(pthread_t thread);-
返回值
成功返回0,失败返回一个错误码 -
参数
要被分离的线程号 -
功能
非阻塞形式,将线程设置成分离态,被设置成分离态的线程退出后,其资源由系统进行回收
-
-
多线程之间的数据通信
对于同一个进程的多个线程而言,他们是共用一个进程的资源。虽然不能使用局部变量进行相互通信,但是,可以使用全局变量来进行资源的通信-
临界区
每个线程中,访问临界资源的代码段 -
临界资源
多个线程共享全局资源、堆区资源
-
-
-
竞态
当多个线程共同使用同一个临界资源,可能会出现当一个线程正在处理数据时,其时间片可能会结束,另一个线程启动后,可将临界资源进行更改,等到再执行该线程时,数据就发生错误了。这种多个线程抢占同一个进程的临界资源的现象-
解决方式
-
互斥机制
使用互斥锁来完成,互斥锁也是一个特殊的临界资源,当某个线程获得了互斥锁后,其余线程只能等待该线程释放互斥锁后,再进行抢占。同一时刻,只能一个线程拥有互斥锁 -
同步机制
使用同步机制,可以完成多个线程有顺序的访问临界资源,这样也起到保护临界资源的作用
-
-
-
多进程和多线程相关函数的对比
-
线程的同步互斥机制
-
互斥机制
互斥机制使用的是互斥锁,是一种特殊的临界资源:不能同时被两个不同的线程锁定,如果一个线程试图去锁定一个已经被其他线程锁定的互斥锁时,那个线程将被挂起,直到该线程释放了该互斥锁-
销毁锁资源
int pthread_mutex_destroy(pthread_mutex_t *mutex);-
返回值
成功返回0,失败返回错误码 -
参数
互斥锁地址 -
功能
销毁互斥锁
-
-
释放锁资源
int pthread_mutex_unlock(pthread_mutex_t *mutex);-
返回值
成功返回0,失败返回错误码 -
参数
互斥锁地址 -
功能
释放锁资源
-
-
获取锁资源
int pthread_mutex_lock(pthread_mutex_t *mutex);-
返回值
成功返回0,失败返回错误码 -
参数
互斥锁地址 -
功能
获取锁资源,如果该互斥锁已经被锁定,那么当前线程会在该函数处阻塞
-
-
初始化互斥锁
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);-
返回值
总是返回0 -
参数
参数1:互斥锁的地址,用于更改互斥锁内容
参数2:互斥锁属性,一般填NULL,表示使用默认属性
-
功能
初始化一个互斥锁
-
-
创建互斥锁
pthread_mutex_t mutex;
-
-
同步机制之无名信号量
-
概念
无名信号量中,本质上维护了一个value值,任何一个线程都可以申请该值和释放该值。当某个线程申请该值时,该值就会减少,该操作称为 P 操作。当某个线程释放该资源时,该值就会增加,该操作称为 V 操作。当value的值为0时,申请资源的线程会在申请处阻塞,直到某个线程将该资源增加到大于0-
同步机制主要实现的是:生产者消费者模型
-
1同步:表示的是多个进程有先后顺序的执行
-
-
创建一个无名信号量
sem_t sem; -
初始化无名信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);-
返回值
成功返回0,失败返回-1,并置位错误码 -
参数
参数1:要被初始化的无名信号量的地址
参数2:判断是多进程还是多线程的同步
0:表示多线程之间的同步
非0:表示多进程之间的同步(亲缘进程间同步)参数3:value的初始值
-
功能
初始化一个无名信号量
-
-
申请资源P操作
int sem_wait(sem_t *sem);-
返回值
成功返回0,失败返回-1并置位错误码 -
参数
无名信号量的地址 -
功能
申请无名信号量的资源,使得无名信号量维护的value值减1操作,如果该无名信号量中的值为0,则当前线程在该函数处阻塞
-
-
释放资源 V操作
int sem_post(sem_t *sem);-
返回值
成功返回0,失败返回-1并置位错误码 -
参数
无名信号量地址 -
功能
是否无名信号量中的资源,是的无名信号量维护的value值加1操作
-
-
销毁无名信号量
int sem_destroy(sem_t *sem);-
返回值
成功返回0,失败返回-1并置位错误码 -
参数
无名信号量的地址,该无名信号量必须要是使用sem_init初始化的无名信号量 -
功能
销毁一个无名信号量
-
-
-
-
脑图
二、作业
作业1:
使用多线程完成两个文件的拷贝,分支线程1,拷贝前一半,分支线程2拷贝后一半,主线程用于回收分支线程的资源
代码解答:
#include <myhead.h>// 声明全局变量
pthread_mutex_t mutex; // 互斥锁
int source = -1; // 源文件描述符
int dest = -1; // 目标文件描述符
int len = 0; // 源文件总长度
int half_len = 0; // 源文件的一半长度// 线程1:负责拷贝源文件的前半部分
void *task1(void *arg)
{char rbuf[128] = ""; // 用于存储读取的数据的缓冲区lseek(source, 0, SEEK_SET); // 设置源文件指针到文件开头int sum = 0; // 记录读取的字节总数while (sum < half_len) // 循环读取直到读取的字节数达到文件的一半{pthread_mutex_lock(&mutex); // 加锁,防止其他线程干扰文件操作int res = read(source, rbuf, sizeof(rbuf)); // 读取数据到缓冲区if (res == 0) // 如果读取到文件末尾,退出循环{pthread_mutex_unlock(&mutex); // 解锁break;}sum += res; // 更新已读取的字节总数// 如果超过文件的一半,仅写入多余部分,避免越界写入if (sum > half_len){write(dest, rbuf, res - (sum - half_len)); // 写入目标文件的剩余部分}else{write(dest, rbuf, res); // 写入读取的完整数据}pthread_mutex_unlock(&mutex); // 解锁,允许其他线程操作}pthread_exit(NULL); // 线程结束
}// 线程2:负责拷贝源文件的后半部分
void *task2(void *arg)
{char rbuf[128] = ""; // 用于存储读取的数据的缓冲区lseek(source, half_len, SEEK_SET); // 设置源文件指针到文件的中间位置int res;while (1){pthread_mutex_lock(&mutex); // 加锁,防止其他线程干扰文件操作res = read(source, rbuf, sizeof(rbuf)); // 读取数据到缓冲区if (res == 0) // 如果读取到文件末尾,退出循环{pthread_mutex_unlock(&mutex); // 解锁break;}write(dest, rbuf, res); // 将读取的数据写入目标文件pthread_mutex_unlock(&mutex); // 解锁,允许其他线程操作}pthread_exit(NULL); // 线程结束
}int main(int argc, const char *argv[])
{pthread_mutex_init(&mutex, NULL); // 初始化互斥锁// 打开源文件,只读模式if ((source = open("./source.txt", O_RDONLY)) == -1){perror("open source error"); // 打开失败,输出错误信息return -1;}// 打开目标文件,写模式,如果不存在则创建,存在则清空内容if ((dest = open("dest.txt", O_WRONLY | O_CREAT | O_TRUNC, 0664)) == -1){perror("open dest error"); // 打开失败,输出错误信息close(source); // 关闭已打开的源文件return -1;}// 获取源文件的长度len = lseek(source, 0, SEEK_END);if (len == -1){perror("lseek error"); // 获取文件长度失败,输出错误信息close(source); // 关闭源文件close(dest); // 关闭目标文件return -1;}// 计算源文件的一半长度half_len = (len + 1) / 2;// 创建线程1,负责拷贝前半部分pthread_t tid1, tid2;if (pthread_create(&tid1, NULL, task1, NULL) != 0){printf("tid1 create error\n"); // 线程1创建失败return -1;}// 创建线程2,负责拷贝后半部分if (pthread_create(&tid2, NULL, task2, NULL) != 0){printf("tid2 create error"); // 线程2创建失败return -1;}// 打印线程的ID,便于调试printf("tid1=%#lx, tid2=%#lx\n", tid1, tid2);// 等待线程1、2执行结束pthread_join(tid1, NULL);pthread_join(tid2, NULL);pthread_mutex_destroy(&mutex); // 销毁互斥锁close(source); // 关闭源文件close(dest); // 关闭目标文件printf("拷贝成功\n"); // 拷贝成功提示return 0;
}
成果展现:
作业2:
互斥锁基础的使用代码
代码解答:
#include <myhead.h>int money=10000; //全局变量 临界资源//创建一个互斥锁
pthread_mutex_t mutex;//创建取钱任务1
void *task1(void *arg)
{while(1){ sleep(1);//获取锁资源pthread_mutex_lock(&mutex);if(money<500){//释放锁资源pthread_mutex_unlock(&mutex);break;}money-=500;printf("张三消费500元,剩余%d元\n",money);pthread_mutex_unlock(&mutex);}pthread_exit(NULL);
}void *task2(void *arg)
{while(1){sleep(1);//获取锁资源pthread_mutex_lock(&mutex);if(money<200){ pthread_mutex_unlock(&mutex);break;}money-=200;printf("李四消费200元,剩余%d元\n",money);//释放锁资源pthread_mutex_unlock(&mutex);}pthread_exit(NULL);
}/**************主程序****************/
int main(int argc, const char *argv[])
{//初始化互斥锁pthread_mutex_init(&mutex,NULL);//启动多个线程用于消费pthread_t tid1,tid2;if(pthread_create(&tid1,NULL,task1,NULL)!=0){printf("tid1 create error\n");return -1;}if(pthread_create(&tid2,NULL,task2,NULL)!=0){printf("tid2 create error\n");return -1;}printf("tid1=%#lx,tid2=%#lx\n",tid1,tid2);pthread_join(tid1,NULL);pthread_join(tid2,NULL);//销毁锁资源pthread_mutex_destroy(&mutex);return 0;
}
成果展现:
作业3:
无名信号量基础的使用代码
代码解答:
#include <myhead.h>
//创建一个无名信号量
sem_t sem;//定义生产者线程
void *task1(void *arg)
{int num=5;while(num--){sleep(1);printf("%#lx:我生产了一辆小米SU7\n",pthread_self());//释放无名信号量sem_post(&sem);}//退出线程pthread_exit(NULL);
}void *task2(void *arg)
{int num =5;while(num--){//申请资源sem_wait(&sem);printf("%#lx:我消费了一辆小米SU7\n",pthread_self());}pthread_exit(NULL);
}int main(int argc, const char *argv[])
{//初始化无名信号量sem_init(&sem,0,0);//第一个0:表示用于多线程的同步//第二个0;表示初始化value值为0//创建两个任务线程pthread_t tid1,tid2;if(pthread_create(&tid1,NULL,task1,NULL) !=0){printf("tid1 create error\n");return -1;}if(pthread_create(&tid2,NULL,task2,NULL)!=0){printf("tid2 create error\n");return -1;}//主线程序中printf("tid1=%#lx,tid2=%#lx\n",tid1,tid2);//阻塞等分支线程的结束pthread_join(tid1,NULL);pthread_join(tid2,NULL);sem_destroy(&sem);return 0;
}
成果展现:
三、总结
学习内容概述:
1. 线程创建与控制
使用 `pthread_create` 创建线程,`pthread_self` 获取线程号,`pthread_exit` 退出线程,`pthread_join` 回收线程资源,`pthread_detach` 实现非阻塞线程分离。
2. 线程间通信
线程共享全局资源,通过全局变量通信,临界区和临界资源的概念。
3. 互斥机制
通过互斥锁(`pthread_mutex_lock` 和 `pthread_mutex_unlock`)保护临界资源,防止竞态条件的发生。
4. 同步机制
使用无名信号量实现线程之间的同步,确保多个线程有序访问临界资源。
学习难点:
1. 竞态条件和互斥机制
当多个线程同时访问共享资源时,容易引发竞态条件,理解如何通过互斥锁机制保护资源是一大难点。
2. 线程的同步
同步机制的实现(如无名信号量)涉及线程的协调和资源的有序访问,这需要深刻理解信号量的操作模式(P操作和V操作)以及如何避免死锁。
3. 线程回收与分离
线程资源的回收机制涉及到阻塞与非阻塞的区别,如何根据需求选择 `pthread_join` 和 `pthread_detach` 是一个细节上的难点。
主要事项:
1. 互斥锁的使用
在多线程访问临界资源时,务必通过互斥锁保护资源,避免多个线程在同一时间修改共享资源,确保数据的一致性。
2. 信号量同步
合理使用无名信号量来控制线程的同步,确保在某些线程完成资源操作之前,其他线程不会抢先访问资源。
3. 线程的生命周期管理
理解线程的创建、执行、退出和资源回收是多线程编程的重要基础,避免僵尸线程或资源泄漏。
未来学习的重点:
1. 多线程中的高级同步机制
如条件变量和读写锁,这些同步机制在复杂场景中更有效地管理资源访问。
2. 线程池的实现
提升多线程程序的效率和资源管理,减少频繁创建和销毁线程的开销。
3. 多线程的调试技巧
学习如何使用调试工具分析多线程程序中的竞态条件、死锁和线程同步问题。