Linux----线程(同步与互斥)

46 篇文章 2 订阅
19 篇文章 0 订阅

在之前接触线程的时候,对它还不算特别了解,所以写出的总结就比较浅浅谈Linux线程
今天在之前博客的基础上在重新总结一下。
就不去解释线程的基本概念了,可以戳上面的链接


线程相关函数

线程创建
#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);

//thread:线程ID
//attr:设置线程的属性,通常设置为NULL表示默认属性
//start_routine:是一个函数指针,指向创建出来的线程要执行的函数
//arg:传给start_routine函数的参数
//返回值:成功返回0,失败返回错误码

直接上代码来测试:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

void* thread_run(void* arg)
{
    while(1)
    {   
        printf("thread is run...\n");
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    int ret = pthread_create(&tid,NULL,thread_run,NULL);

    if(ret!=0)
    {
        perror("pthread_create");
        return -1;
    }
    while(1)
    {
        printf("I am running\n");
        sleep(1);
    }
    return 0;
}

来看看结果:
这里写图片描述

这里需要注意的一点是:与线程有关的函数绝大多数名字都是以“pthread_”打头的 要使用这些函数,除了要引入相应的头文件,链接这些线程函数库时要使用编译器命令的-lpthread选项

如何获得线程ID

没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID。但是引入线程概念之后,情况发生了变化,一个用户进程下管辖N个用户态线程,每个线程作为一个独立的调度实体在内核态都有自己的进程描述符,进程和内核的描述符一下子就变成了一对多的关系,POSIX标准又要求进程内的所有线程调用getpid函数时返回相同的进程ID,所以Linux内核就引入了线程组的概念
这里写图片描述
上图是进程描述符task_struct结构体中的一些成员,进程描述符结构体中的pid,表面上看对应的是进程ID,其实不然,它对应的是线程ID;进程描述符中的tgid,含义是Thread Group ID,该值对应的是用户层面的进程ID。

那如何获得线程ID呢?Linux系统提供了gettid系统调用来返回其线程ID,可是该系统调用并没有封装起来,不过可以采用如下方法:

#include <sys/syscall>

pid_t tid = syscall(SYS_gettid);

在来学习一条命令查看系统中的线程

ps -eLf |head -1 && ps -eLf |grep a.out |grep -v grep 

把刚刚的代码改造一下,在线程运行的函数中,加入获取线程ID的代码,来看看结果
这里写图片描述

我们可以看到,运行结果显示一个线程ID是5671,而我们利用命令显示出的LWP也有一个5671

除了上面的方法之外,我们还有一个函数,可以获取自身的线程ID

#include <pthread.h>

pthread_t pthread_self(void);

我们来试试这种方式:
这里写图片描述
这里写图片描述
我们会发现,这次获得的线程ID是一个非常大的数字,来解释一下。这个线程ID和我们刚刚讲的线程ID LWP 是不一样的。
我们说的创建线程的函数pthread_ create会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
可以注意到,pthread_self()函数返回值得类型是pthread_t,对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。

线程终止

我们一共有三种方式来终止线程:

  • 从线程函数中return进行终止,但是对于主线程而且,return相当于调用exit,不适用
  • 线程可以调用pthread_exit来终止自己
  • 一个进程还可以调用pthread_cancel来终止自己或别的线程

先来看看pthread_exit函数

#include <pthread.h>

void pthread_exit(void *retval);
//retval:和线程函数返回值的用法一样
//其它线程可以调用pthread_join获得这个指针。但是它不能指向局部变量
//返回值:无返回值,线程结束的时候无法返回到它的调用者

再来看看pthread_cancel函数

//取消一个执行中的进程
#include <pthread.h>

int pthread_cancel(pthread_t thread);
//thread:要取消线程的ID
//返回值:成功返回0,失败返回错误码

因为线程终止比较简单,在这里不单独演示,在后面函数的学习中可以测试一下

线程等待

由于已经退出的线程,它的空间没有被释放,仍然在进程的地址空间内。所以我们需要回收资源,这就是线程等待的作用

#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);
//thread:要等待线程ID
//retval:指向一个指针,这个指针存储了退出线程的退出状态

线程以不同的方式退出pthread_join得到的退出信息是不同的

  • 如果线程通过return返回,retval所指向的单元里存放的是线程函数的返回值
  • 如果线程被别的线程调用pthread_cancel异常终止,retval所指向的单元里存放的是常数PTHREAD_CANCELED
  • 如果线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给 pthread_exit的参数
  • 如果对线程的终止状态不感兴趣,可以直接传NULL

来测试一下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

void* run1(void* arg)
{
    printf("thread1 running...\n");
    int *p=(int *)malloc(sizeof(int));
    *p=1;
    return (void*)p;
}

void* run2(void* arg)
{
    printf("thread2 running...\n");
    int *p=(int *)malloc(sizeof(int));
    *p=2;
    pthread_exit(p);
}
void* run3(void* arg)
{
    while(1)
    {
        printf("thread3 running...\n");
        sleep(1);
    }
    return NULL;
}

int main()
{
    pthread_t t1,t2,t3;
    void *ret; 
    //创建线程1,利用return终止
    pthread_create(&t1,NULL,run1,NULL);
    pthread_join(t1,&ret);
    printf("thread1 return,id:%d,return code:%d\n",t1,*(int*)ret);
    free(ret);

    //创建线程2,利用pthread_exit终止
    pthread_create(&t2,NULL,run2,NULL);
    pthread_join(t2,&ret);
    printf("thread2 return,id:%d,return code:%d\n",t2,*(int*)ret);
    free(ret);

    //创建线程3,利用pthread_cancel终止
    pthread_create(&t3,NULL,run3,NULL);
    sleep(3);
    pthread_cancel(t3);
    pthread_join(t3,&ret);
    if(ret== PTHREAD_CANCELED )
        printf("thread3 return,id:%d,return code: PTHREAD_CANCELED\n",t3);
    return 0;

}

测试结果:
这里写图片描述

线程分离

默认情况下,线程的属性是joinable可结合的,一般当线程终止后,其终止状态就会一直保留,直到其它线程调用pthread_join获取它的状态为止。 但是线程也可以被置为detach分离状态,这样的线程一旦终止就立刻回收它占用的所有资源, 而不保留终止状态。

#include <pthread.h>

int pthread_detach(pthread_t thread);
//thread:要分离的线程
//返回值:成功返回0,失败返回错误码

在这里需要注意的是:一个线程如果调用了pthread_detach就不能再调用pthread_join了,一个已经分离的线程是不能被其他线程回收或杀死的,它的资源在它终止时由系统释放

来测试一下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

void* thread_run(void* arg)
{
    pthread_detach(pthread_self());
    printf("thread running...\n");
    sleep(1);
    return NULL;
}

int main()
{
    pthread_t t1;
    pthread_create(&t1,NULL,thread_run,NULL);
    sleep(1);

    int ret = pthread_join(t1,NULL);
    if(ret!=0)
    {
        printf("wait failed\n");
    }
    else
    {
        printf("wait sucess\n");
    }
    return 0;
}

这里写图片描述


线程的同步与互斥

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

互斥

之前我们在学习进程间通信的时候已经谈论过这个概念了,所以在这里对于概念就不再解释,来看一个例子就会明白。

当我们去电影院买票的时候,一般情况下不会发生两个人买到了同一张票,或者电影院已经显示没票了却还有人可以买到票。那么我们来模拟实现一些售票系统

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

int ticket=500;//总票数
void* sell_tic(void *arg)
{
    while(1)
    {   
        if(ticket>0)
        {
            usleep(1000);
            printf("%s get ticket%d\n",(char*)arg,ticket);
            ticket--;
        }
        else
           break;  
    }
}

int main()
{
    //创建四个线程进行买票操作
    pthread_t tid1,tid2,tid3,tid4;
    pthread_create(&tid1,NULL,sell_tic,"thread 1");  
    pthread_create(&tid2,NULL,sell_tic,"thread 2");  
    pthread_create(&tid3,NULL,sell_tic,"thread 3");  
    pthread_create(&tid4,NULL,sell_tic,"thread 4");  

    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
    pthread_join(tid3,NULL);
    pthread_join(tid4,NULL);

    return 0;
}

这里写图片描述

根据结果很容易发现问题,票务系统多卖出了三张票。按理来说这种情况是不能发生的,我们来找一下可能会出现问题的地方。
来看看ticket–这句代码,这条语句的作用是把一个整数减一。分析一下这个操作,这个整数变量最开始在内存中,我们要执行这个操作就需要先将数据从内存拿到CPU中,然后在CPU中对整数减一,最后再把数据写回内存。可以发现的是,我们这一句代码如果换成汇编的话就是三条了。
那么假想,线程A先执行代码,一直到到票数减一的操作,当它在CPU中将票数减一之后,准备将数据写回内存的时候它被切走了,这个时候线程B来了,线程B买完了所有的票,这是票数变成了0,然后线程B走了。线程A回来继续执行自己刚刚的操作,将数据写回内存。可想而知,真实情况下已经没有票了,可是线程A却不知道,这就发生了数据不一致问题。
根本原因用互斥的概念就很容易解释清楚:我们在对临界资源(即票数)进行操作的时候,该操作不是一个原子操作

所以我们必须保证代码必须要有互斥行为

  • 当代码进入临界区执行时,不允许其他线程进入该临界区
  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区

就相当于给临界区加锁实现互斥访问,就引入了互斥量

互斥量相关函数
初始化互斥量

初始化互斥量有两种方式:

  • 动态分配,利用函数pthread_mutex_init
#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *restrict mutex,
              const pthread_mutexattr_t *restrict attr);
//mutex:要初始化的互斥量
//attr:互斥量的属性,设置为NULL表示缺省属性
//返回值:成功返回0,失败返回错误码
  • 静态分配,利用宏直接初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
销毁互斥量
#include <pthread.h>

int pthread_mutex_destroy(pthread_mutex_t *mutex);
//mutex:要销毁的互斥量
//返回值:成功返回0,失败返回错误码

使用pthread_mutex_destroy函数需要注意几点:
1、用静态分配方法初始化的互斥量不用进行销毁
2、不要销毁一个已经加锁的互斥量
3、一个已经销毁的互斥量要保证后面不会在有别的线程来尝试加锁

互斥量的加锁和解锁
#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//mutex:需要加锁和解锁的互斥量
//返回值:成功返回0,失败返回错误码

来解释一下这三个函数调用之后的情况:
pthread_mutex_lock获得了互斥量之后,则当前线程需要挂起等待,直到另一个线程调用
pthread_mutex_unlock释放互斥量,当前线程被唤醒,才能获得该互斥量并继续执⾏行。
如果一个线程既想获得锁,又不想挂起等待,可以调用pthread_mutex_trylock,如果互斥量已经被另一个线程获得,这个函数会失败返回EBUSY,而不会使线程挂起等待。

好了,接下来我们来改进一下刚刚的票务系统:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

int ticket=500;//总票数
//pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex;//定义互斥量

void* sell_tic(void *arg)
{
    while(1)
    {   
        pthread_mutex_lock(&mutex);//加锁
        if(ticket>0)
        {
            usleep(1000);
            printf("%s get ticket%d\n",(char*)arg,ticket);
            ticket--;
            pthread_mutex_unlock(&mutex);//解锁
        }
        else
        {
            pthread_mutex_unlock(&mutex);//解锁
            break;   
        }
    }
}

int main()
{
    pthread_mutex_init(&mutex,NULL);//初始化互斥量
    pthread_t tid1,tid2,tid3,tid4;
    pthread_create(&tid1,NULL,sell_tic,"thread 1");  
    pthread_create(&tid2,NULL,sell_tic,"thread 2");  
    pthread_create(&tid3,NULL,sell_tic,"thread 3");  
    pthread_create(&tid4,NULL,sell_tic,"thread 4");  

    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
    pthread_join(tid3,NULL);
    pthread_join(tid4,NULL);

    pthread_mutex_destroy(&mutex);//销毁互斥量
    return 0;
}

这里写图片描述

现在我们会发现票务系统已经没有刚刚的问题了

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

同步

再回到买票的例子中,我们已经实现了互斥,但是如果发生下面这种情况呢?
一个人去买票,买了之后立马退掉,退掉之后再立马买回来,就这样一直循环,这样别人想买票也买不到。就相当于一直在申请锁,释放锁,造成了别的线程因为长时间得不到资源而产生饥饿问题。
当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。 例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量

条件变量相关函数
初始化条件变量

和互斥量类似,有两种方法

动态分配,调用pthread_cond_init

#include <pthread.h>

int pthread_cond_init(pthread_cond_t *restrict cond,
              const pthread_condattr_t *restrict attr);
//cond:要初始化的信号量
//arrt:条件变量的属性,默认为NULL
//返回值:成功返回0,失败返回错误码

静态分配,利用宏直接初始化

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
销毁条件变量
#include <pthread.h>

int pthread_cond_destroy(pthread_cond_t *cond);
//cond:要销毁的条件变量
//返回值:成功返回0,失败返回错误码
等待
#include <pthread.h>

int pthread_cond_wait(pthread_cond_t *restrict cond,
              pthread_mutex_t *restrict mutex);
//cond:在这个条件变量上等待
//mutex:等待时要释放的互斥量,也是为了被唤醒是重新申请互斥量做准备
//返回值:成功返回0,失败返回错误码
唤醒
#include <pthread.h>

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
//cond:要唤醒这个条件变量的线程

在这里我们发现有两种方式唤醒

  • 一次唤醒多个线程用pthread_cond_broadcast
  • 一次唤醒一个线程用pthread_cond_signal

我们来演示一下条件变量的例子:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

pthread_cond_t cond;
pthread_mutex_t mutex;

void* run1(void* arg)
{
    while(1)
    {
        pthread_cond_wait(&cond,&mutex);
        printf("active\n");
    }
}
void* run2(void* arg)
{
    while(1)
    {
        pthread_cond_signal(&cond);
        sleep(1);
    }
}

int main()
{
    pthread_t t1,t2;

    pthread_cond_init(&cond,NULL);
    pthread_mutex_init(&mutex,NULL);

    pthread_create(&t1,NULL,run1,NULL);
    pthread_create(&t2,NULL,run2,NULL);

    pthread_join(t1,NULL);
    pthread_join(t2,NULL);

    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);

    return 0;
}

这里写图片描述
读者自己演示的时候就会发现,如果没有条件变量,线程1就会一直不停的打印active,但是加上条件变量之后,线程2每隔一秒唤醒一次线程1,线程1每隔一秒打印一次active。

有关线程的概念就谈到这里,欢迎补充

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值