多线程总结

进程与线程的区别(总结)

  1. 进程是资源分配的基本单位,线程是cpu调度的基本单位。
  2. 在同一个进程中可以创建多个线程,这些线程共享整个进程的资源(寄存器、堆栈、上下文),一个进程至少有一个线程。
  3. 进程的创建调用fork,系统需要为这个进程分配资源,线程的创建调用pthread_create,系统只需要创建该线程的PCB,这个PCB将使用这个进程资源,不用系统分配。
  4. 正常退出:进程结束后它拥有的所有线程都将销毁,而线程的结束不会影响同个进程中的其他线程的结束
  5. 异常退出:子进程不会影响父进程的运行,而在多线程中,只要有一个线程出错,在操作系统看来,是这个进程出现错误,就会向这个进程发送特定的信号,是整个进程终止,并且进程资源被回收,其他线程自然不复存在。
  6. 线程是轻量级的进程,创建和销毁所需要的代价比进程小得多,所有操作系统中的执行功能都是创建线程去完成的。
  7. 为了保护进程的资源安全,所以进程与进程之间有着明显“界限”,它们之间的独立性使得进程间通信代价很大,线程的资源都是共享的,执行时一般都要进行同步和互斥。
  8. 线程有自己的私有TCB,线程id,寄存器、硬件上下文,而进程也有自己的私有属性进程控制块PCB,这些私有属性是不被共享的,用来标示一个进程或一个线程的标志

线程属性

找到两篇关于线程属性的文章和大家分享

https://blog.csdn.net/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);

等待条件满足

pthread_cond_wait(&cond, &mutex);
注意: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.csdn.net/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)

执行下一行语句,如果有函数调用则进入到函数中


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值