进程与线程的区别(总结)
- 进程是资源分配的基本单位,线程是cpu调度的基本单位。
- 在同一个进程中可以创建多个线程,这些线程共享整个进程的资源(寄存器、堆栈、上下文),一个进程至少有一个线程。
- 进程的创建调用fork,系统需要为这个进程分配资源,线程的创建调用pthread_create,系统只需要创建该线程的PCB,这个PCB将使用这个进程资源,不用系统分配。
- 正常退出:进程结束后它拥有的所有线程都将销毁,而线程的结束不会影响同个进程中的其他线程的结束
- 异常退出:子进程不会影响父进程的运行,而在多线程中,只要有一个线程出错,在操作系统看来,是这个进程出现错误,就会向这个进程发送特定的信号,是整个进程终止,并且进程资源被回收,其他线程自然不复存在。
- 线程是轻量级的进程,创建和销毁所需要的代价比进程小得多,所有操作系统中的执行功能都是创建线程去完成的。
- 为了保护进程的资源安全,所以进程与进程之间有着明显“界限”,它们之间的独立性使得进程间通信代价很大,线程的资源都是共享的,执行时一般都要进行同步和互斥。
- 线程有自己的私有TCB,线程id,寄存器、硬件上下文,而进程也有自己的私有属性进程控制块PCB,这些私有属性是不被共享的,用来标示一个进程或一个线程的标志
线程属性
找到两篇关于线程属性的文章和大家分享
https://blog.youkuaiyun.com/lizhun19900119/article/details/12748991
https://www.cnblogs.com/meihao1203/p/8531962.html
线程的同步与互斥
大部分情况下,线程使用的都是局部变量,变量的地址空间在线程的栈空间内,这样一来变量就属于单个线程,其他线程无法获得这个变量
一个全局变量(全局变量)被多个线程共享,这样的变量就称为共享变量,可以通过数据的共享完成线程间的交互
但是对个线程并发就会出现问题,先看一个有问题的售票系统,有一百张票,由四个线程去抢票,便面上并没有什么问题,代码如下:
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
int ticket = 100;
void *route(void* arg){
char* tid = (char*)arg;
while(1){
if(ticket > 0) {
usleep(1000);
ticket--;
printf("%s正在卖票,还剩%d张票\n", tid, ticket);
} else {
break;
}
}
}
int main()
{
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, route, "一号售票员");
pthread_create(&t2, NULL, route, "二号售票员");
pthread_create(&t3, NULL, route, "三号售票员");
pthread_create(&t4, NULL, route, "四号售票员");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
return 0;
}
编译命令:
gcc ***.c -lpthread // ***代表文件名
实验结果:
图中出现负数的原因其实很容易解释,当一个线程调用usleep时,这个正在被执行的线程就会被切出去进入睡眠状态,当最后剩一张票时,进去的四个线程都会进入睡眠状态,睡眠时间结束之后,最先醒的线程取到了最后一张票,之后如果这个苏醒的线程试图进入到if语句,发现不成立,break退出循环,随即线程退出,之后三个线程会逐个苏醒,虽然票数已经为0了,但是这三个线程已经在if语句内部,同样会对票数减一,所以就会出现-1,-2,-3.
要解决上面问题要做到以下三点:
- 代码必须要有互斥⾏为:当代码进⼊临界区执⾏时,不允许其他线程进⼊该临界区。
- 如果多个线程同时要求执⾏临界区的代码,并且临界区没有线程在执⾏,那么只能允许⼀个线程进⼊该临界区。
- 如果线程不在临界区中执⾏,那么该线程不能阻⽌其他线程进⼊临界区。
要做到这三点,本质上就是需要⼀把锁。Linux上提供的这把锁叫互斥量。
初始化互斥量
静态初始化:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITALIZER;
动态初始化:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
参数:
mutex:要初始化的互斥量
attr:NULL
销毁互斥量
注意:
- 使用PTHREAD_MUTEX_INITALIZER初始化的互斥量不需要销毁
- 不要销毁一个已经加锁了的互斥量
- 已经销毁的互斥量,要确保后面不会有线程再尝试枷锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
调⽤pthread_ mutex_lock 时,可能会遇到以下情况:
- 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
- 发起函数调⽤时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调⽤会陷⼊阻塞,等待互斥量解锁。
改进后的售票系统如下:
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
int ticket = 100;
pthread_mutex_t mutex;
void *route(void* arg){
char* tid = (char*)arg;
while(1){
pthread_mutex_lock(&mutex);
if(ticket > 0) {
usleep(10000);
ticket--;
printf("%s正在卖票,还剩%d张票\n", tid, ticket);
pthread_mutex_unlock(&mutex);
} else {
pthread_mutex_unlock(&mutex);
break;
}
}
}
int main()
{
pthread_t t1, t2, t3, t4;
pthread_mutex_init(&mutex,NULL);
pthread_create(&t1, NULL, route, "一号售票员");
pthread_create(&t2, NULL, route, "二号售票员");
pthread_create(&t3, NULL, route, "三号售票员");
pthread_create(&t4, NULL, route, "四号售票员");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
实验结果:
生产者消费者模型及应用场景
在进入主题之前先来说一下条件变量
- 当⼀个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
- 例如⼀个线程访问队列时,发现队列为空,它只能等待,只到其它线程将⼀个节点添加到队列中。
这种情况就需要⽤到条件变量。
条件变量函数:
和互斥量相似,在使用条件变量前必须先申请一个条件变量,要申请为全局变量,方法如下:
pthread_cond_t cond;
初始化
pthread_cond_init(&cond, NULL);
函数原型:
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
销毁
pthread_cond_destroy(&cond);
函数原型:
int pthread_cond_destroy(pthread_cond_t *cond);
等待条件满足
注意:mutex为互斥量,需要申请pthread_cond_wait(&cond, &mutex);
函数原型:
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex); 参数: //cond:要在这个条件变量上等待 //mutex:互斥量
唤醒
pthread_cond_signal(&cond);
函数原型:
int pthread_cond_signal(pthread_cond_t* cond)
还有另外一种唤醒的方法:
int pthread_cond_broadcast(pthread_cond_t* cond)
生产者消费者模型实际上就是多个线程的同步问题
多个生产者中一次只能有一个向缓冲区中生产数据,多个消费者中一次只能一个消费者从缓冲区中读取数据,任何时候,只有一个生产者或者消费者可以访问缓冲区,生产之后,缓冲区会多一个元素,消费之后,缓冲区会少一个元素,如果这个缓冲区是一个环形队列,那么生产者对应的就是入队列,消费者对应的就时出队列,如果缓冲区是一个栈,那么生产者对应的就是入栈,消费者对应的就时出栈。
代码如下(缓冲区是一个栈):
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#define CONSUMERS_COUNT 2
#define PRODUCERS_COUNT 2
struct msg{
struct msg* next;
int num;
};
struct msg* head = NULL;
pthread_cond_t cond;
pthread_mutex_t mutex;
pthread_t pthreads[CONSUMERS_COUNT+PRODUCERS_COUNT];
void* consumer(void* arg) {
int num = *(int*)arg;
free(arg);
struct msg *mp;
for(;;) {
pthread_mutex_lock(&mutex);
while(head == NULL) {
printf("consumer%d wait a producer ... \n");
pthread_cond_wait(&cond, &mutex);
}
printf("consumer%d end wait ...\n",num);
printf("consumer%d begin consume ...\n",num);
mp = head;
head = mp->next;
printf("consumer%d Consume %d\n", num, mp->num);
free(mp);
pthread_mutex_unlock(&mutex);
printf("consumer%d end ...\n",num);
sleep(rand()%5);
}
}
void* producer(void* arg) {
int num = *(int*)arg;
free(arg);
struct msg* mp;
for(;;){
printf("producer%d begin produce ...\n", num);
mp = (struct msg*)malloc(sizeof(struct msg));
mp->num = rand()%1000 + 1;
printf("producer%d produce %d\n", num, mp->num);
pthread_mutex_lock(&mutex);
mp->next = head;
head = mp;
printf("producer%d end produce ...\n");
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
sleep(rand()%5);
}
}
int main()
{
srand(time(NULL));
pthread_cond_init(&cond, NULL);
pthread_mutex_init(&mutex, NULL);
int i = 0;
for(; i<CONSUMERS_COUNT; i++) {
int* p = (int*)malloc(sizeof(int));
*p = i;
pthread_create(&pthreads[i], NULL, consumer, (void*)p);
}
for(i=0; i<PRODUCERS_COUNT; i++) {
int* p = (int*)malloc(sizeof(int));
*p = i;
pthread_create(&pthreads[CONSUMERS_COUNT+i], NULL, producer, (void*)p);
}
for(i=0; i<CONSUMERS_COUNT+PRODUCERS_COUNT; i++) {
pthread_join(pthreads[i], NULL);
}
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);
return 0;
}
实验结果:
gdb调试多线程
建议使用cgdb,拥有gdb功能的同时,有可自动显示代码,为gdb穿上一件外衣,效果如下:
安装教程:
https://blog.youkuaiyun.com/luhaowei0066/article/details/79718130
gdb在调试多线程程序时,由于有多个线程,在调试一个线程时,其他线程也在运行,所以调试多线程程序首先需要在每个线程执行的函数中(最好是函数开始位置)打断点,使得这个线程在这个断点初停下来,这样在调试一个线程时,其他的线程都停在了断点初处等待调试。
#include<stdio.h>
#include<pthread.h>
void* t1_fun(void* arg) {
char* str = (char*)arg;
printf("this is %s\n", str);
int a = 1;
int b = 1;
int c;
c = a + b;
printf("a + b = %d\n", c);
}
void* t2_fun(void* arg) {
char* str = (char*)arg;
printf("this is %s\n", str);
int x = 2;
int y = 2;
int z;
z = x + y;
printf("x + y = %d\n", z);
}
int main()
{
pthread_t t1, t2;
pthread_create(&t1, NULL, t1_fun, "pthread1");
pthread_create(&t2, NULL, t2_fun, "pthread2");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
return 0;
}
在gcc 编译时 要加上-g选项,保存文字信息
1、先使用gdb进行调试
2、打断点并查看断点信息
3、运行
4、继续向下运行,创建了两个新线程之后查看新线程信息,id表示线程号,切换线程时,可直接使用这个id号,例如:使用命令:thread 3就可以切换到线程3,id前面的星号(*)表示当前线程。
切换线程后需要输入c(continue),这个切换到的线程才会运行
总结:
- gdb调试多线程常用命令:
- 查看当前存在的线程的信息:i thread
- 切换线程:thread ID
- 一般在线程能都打了断点,切换后使线程继续运行的命令:c(continue)
其他操作与调试单线程的命令及操作保持一致:
命令 | 描述 |
backtrace(或bt) | 查看各级函数调用及参数 |
finish | 连续运行到当前函数返回为止,然后停下来等待命令 |
frame(或f) | 帧编号 选择栈帧 |
info(或i) | locals 查看当前栈帧局部变量的值 |
list(或l) | 列出源代码,接着上次的位置往下列,每次列10行 |
list 行号 | 列出从第几行开始的源代码 |
list 函数名 | 列出某个函数的源代码 |
next(或n) | 执行下一行语句 |
print(或p) | 打印表达式的值,通过表达式可以修改变量的值或者调用函数 |
quit(或q) | 退出gdb调试环境 |
start | 开始执行程序,停在main函数第一行语句前面等待命令 |
step(或s) | 执行下一行语句,如果有函数调用则进入到函数中 |