【Linux】对线程的认识

一. 什么是线程

  1. 在一个程序里的一个执行路线叫做线程,线程是一个进程内部的控制序列。一个进程可以拥有多个线程,但是至少都有一个执行线程(单线程进程),线程的执行粒度比进程更细致,线程资源共享。
  2. 在Linux中并不存在真正的线程,Linux的线程是使用进程模拟的。我们在Linux系统中,线程的创建是在内核外进行的,有POSIX提供的线程库实现。因此链接这些线程函数库时要使用编译器命令的”-lpthread”选项。Linux下的线程也叫做轻量级进程(LWP)。
  3. 进程与线程的关系:进程是资源竞争的基本单位,线程是程序执行的最小单位,是承担调度的基本单位。
  4. 线程共享进程数据,但是也拥有自己独立的部分:线程ID,独立的上下文数据,栈,errno,信号屏蔽字,调度优先级
  5. 一个进程的多个线程之间可以共享
    (1)同一地址空间,因此Text Segment,Data Segment都是共享的,如果定义一个函数或者全局变量,那么在各个线程中都可以访问到
    (2)还共享文件描述符,每种信号的处理方式,当前工作目录,用户ID和组ID这样的进程资源和环境
  6. 线程的优点
    (1)创建进程消耗的资源更少
    (2)线程切换的开销更少
    (3)充分利用多处理器的可并行数量
    (4)线程占用的资源比进程少
  7. 线程的缺点
    (1)如果计算密集型线程比可用处理器多,有可能增加了额外的同步和调度开销,而可用的资源不变。
    (2)健壮性降低:编写多线程程序需要考虑的更加全面,有可能共享了不该共享的变量,造成不良影响
    (3)缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些系统函数对整个进程造成影响
    (4)编写程序难度较高

二. 线程控制

1. 创建线程

#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传给执行的函数的参数

返回值:成功返回0,失败返回错误码。

2. 进程ID和线程ID
  在Linux中,目前的线程是靠POSIX线程库和进程实现的,在这种实现下,线程又被称为轻量级进程,每一个用户态的线程,在内核中都有一个对应的调度实体,也有自己的task_struct结构体。
  在没有线程之前,一个进程对应内核里的一个task_struct,对应一个进程ID;但是在引入线程之后,一个进程下有n个用户态线程,每个线程作为独立的调度实体有自己的进程描述符,这样,进程和内核中的进程描述符变成了1:n关系,但是POSIX标准要求所有的线程调用getpid时返回相同的进程ID。此时,就有了线程组的概念。

struct task_struct {
    ...
    pid_t pid;
    pid_t tgid;
    ...
    struct task_struct *group_leader;
    ...
    struct list_head thread_group;
    ...
};

  多线程的进程,又被称为线程组,线程组内的每一个线程在内核中存在一个进程描述符与之对应。其实在进程描述符中pid,描述的线程ID,其中的tgid对应的才是用户层的进程ID

用户态系统调用内核进程描述符中对应的结构
进程IDpid_t getpid(void)pid_t tgid(Thread Group ID)
线程IDpid_t gettid(void)pid_t pid

到此,我们知道线程ID跟进程ID一样,是pid_t类型的变量,那么如何查呢?
这里写图片描述
LWP:线程ID,即gettid()系统调用的返回值
NLWP:线程组内线程的个数
在Linux提供了gettid系统调用返回其线程ID,但是glibc并没有将该系统调用封装起来,并开放接口供我们使用;因此如果要获得线程ID,可以采用如下办法:

#include<sys/syscall.h>
pid_t tid;
tid = syscall(SYS_gettid);

3. 线程ID及进程地址空间布局
  在使用pthread_create函数创建线程的时候,也会返回一个线程ID,这个线程ID和我们上面说的并不是一个东西。上面我们讲的是进程调度的范畴,pthread_create函数产生的线程是线程库的范围,后续的相关操作,根据的是这个线程ID。可以通过调用pthread_self()获得线程ID。

pthread_t  pthread_self();
//pthread_t类型的线程ID,本质上是一个进程地址空间上的一个地址。

这里写图片描述

4. 进程终止
有三个方法可以终止线程:
(1)从线程函数return,但是对于主线程,从main函数return相当于调用exit
(2)线程可以通过pthread_exit终止自己

void pthread_exit(void *value_ptr);

(3)一个线程可以通过pthread_cancel终止同一进程中的另一个线程

int pthread_cancel(pthread_t thread);

三. 线程等待与分离

1. 线程等待
  已经退出的线程,其空间没有被释放,仍然在进程的地址空间内,必须有线程等待回收它,否则会造成内存泄漏。创建的新线程不会服用刚才退出线程的地址空间。
函数原型:

int pthread_join(phread_t thread, void **value_prt);

参数: thread线程ID,value_ptr它指向一个指针,后者指向线程的返回值
返回值:成功返回0,失败返回错误码

  1. 如果线程通过return返回,value_ptr所指向的单元里存放thread线程函数的返回值
  2. 如果被别的线程通过pthread_cancel异常终止掉,value_ptr所指向的单元里存放常数PTHREAD_CANAELED
  3. 通过pthread_exit函数终止,value_ptr指向的单元存放的是传给pthread_exit的参数
  4. 不关心线程返回值,可以传NULL

2. 分离线程
  默认的线程是joinable的,我们知道线程退出后,需要对其进行pthread_join操作,但是如果不关心线程的返回值,join就是一种负担,此时,我们可以让系统在线程退出后,自动释放线程资源。

int pthread_detach(pthread_t thread);

可以自己分离,也可以是对其他目标线程分离。需要注意的是,一个线程不能既是分离的又是joinable。


四. 线程同步与互斥

mutex互斥量

  很多变量需要在线程间共享,这样的变量称为共享变量,通过数据的共享,完成线程之间的交互。但是此时会存在问题。
  举一个例子:假如模拟两个线程去买火车票,可以发现剩下的票出现了负数的结果。

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

int ticket = 100;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void* route(void* arg)
{
    char* id = (char*)arg;
    while(1)
    {
        //pthread_mutex_lock(&lock);
        if(ticket>0)
        {
            usleep(1000);
            printf("%s sells ticket: %d\n", arg, ticket);
            ticket--;
            //pthread_mutex_unlock(&lock);
        }
        else
        {
            //pthread_mutex_unlock(&lock);
            break;
        }
    }
}

int main()
{
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, NULL, route, "thread1");
    pthread_create(&t2, NULL, route, "thread2");
    pthread_create(&t3, NULL, route, "thread3");
    pthread_create(&t4, NULL, route, "thread4");

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
    return 0;
}

运行结果:
  这里写图片描述
  为什么会这样?

  1. if判断后,代码可能并发的切换到其他线程
  2. 如果在买了票之后,票的总数减少一之前,可能会有其他进程进入该代码段
  3. 票数减少这个操作本身并不是一个原子操作。

因此,我们需要做到:

  1. 代码要有互斥行为,代码进入临界区时,其他线程不能进入该段代码
  2. 多个线程同时要求执行临界区的代码,并且临界区没有线程执行,只允许一个线程进入该临界区
  3. 如果线程不在临界区,不能阻止其他线程进入临界区

要达到以上要求,需要一把锁。下面介绍一下互斥量的使用

1. 初始化
(1)静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
(2)动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr); //mutex要初始化的互斥量,attr:NULL
2. 销毁互斥量

int pthread_mutex_destroy(pthread_mutex_t *mutex);

需要注意:

  • 不要销毁一个已经加锁的互斥量;
  • 静态初始化的互斥量不需要销毁;
  • 已经销毁的互斥量,确保不会再有线程加锁

3. 加锁解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

条件变量
条件变量是实现线程同步的
1. 初始化

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *rest attr);
cond:要初始化的条件变量
attr:NULL

2. 销毁

int pthread_cond_destroy(pthread_cond_t *cond);

3. 等待条件满足

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
cond:要在这个条件变量上等待
mutex:互斥量

4. 唤醒等待

int pthread_cond_signal(pthread_cond_t *cond);//唤醒一个
int pthread_cond_broadcast(pthread_cond_t *cond);//全部唤醒

通常的情况是这样的:先上锁,发现条件不满足,解锁,然后等待条件变量满足,再加锁,完成临界区的操作后,最后在解锁。我们的pthread_cond_wait函数中,解锁和等待是一个原子操作。

条件变量使用规范

1. 等待条件

pthread_mutex_lock(&mutex);
while (条件为假)
    pthread_cond_wait(cond, mutex); 
    //修改条件为真
pthread_mutex_unlock(&mutex);

2. 给条件发送信号

pthread_mutex_lock(&mutex); 
//设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);
在实际执行过程中,往往是执行快的等待执行较慢的。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值