LINUX【线程概念】

线程概念

线程是进程中的一个执行流

一个进程可以有一到多个执行流

透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流

linux中,进程和线程的数据结构都是pcb,线程是CPU调度的基本单位

进程的中的执行流共享同一个mm_struct页表

image-20230116191926046

虚拟地址通过页表转换到物理地址

首先,物理地址会被分成一个个的page,每个page为4kb,而且页表不只是包含虚拟地址,和物理地址,还包括是否命中,RWX权限,UK权限

是否命中:访问的数据是否在物理内存中

RWX权限:读写权限

U/K权限:当前是用户级页表还是内核级页表

image-20230131164646127

char* msg="hello world";
*msg='a';

上述代码编译没有问题,但是在执行时会报错,就是因为在运行,执行代码时,在页表转换时,是只读权限,而这句代码需要写的权限,所以运行报错

还有,页表并不是我们想象中直接从32/64位虚拟地址映射到32/64位物理地址,而是将虚拟地址分为3部分(10位/10位/12位),使用前10位通过一级页表(页目录)映射到二级页表,使用第二个10位映射到物理内存的某一个page的起始地址,最后12位正好对应每一个page的大小(4kb

image-20230131185852829

线程的优点

  • 创建一个新线程的代价要比创建一个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  • 线程占用的资源要比进程少很多
  • 能充分利用多处理器的可并行数量
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

线程的缺点

  • 性能损失(如果计算密集型的线程数量比可用的处理器多,增加了同步和调度的开销,造成了性能损失)
  • 健壮性降低,线程不具有独立性,线程是不具有保护的
  • 缺乏访问控制
  • 编程难度提高

线程异常

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,进程内的所有线程也就随即退出

线程用途

  • 合理的使用多线程,能提高CPU密集型程序的执行效率
  • 合理的使用多线程,能提高IO密集型程序的用户体验

linux进程和线程

  • 进程是资源分配的基本单位
  • 线程调度的基本单位
  • 线程共享进程数据,但也拥有自己的一部分数据
    • 线程ID(LWP
    • 一组寄存器数据
    • errno
    • 信号屏蔽字
    • 调度优先级

进程的多个线程共享同一地址空间,因此代码段、数据段都是共享的,在一个线程中定义一个全局变量或一个函数,在其他线程也可以访问的,除此之外,各线程还共享以下进程资源和环境:

  • 文件描述符表
  • 每种信号的处理方式
  • 当前工作目录
  • 用户id和组id

线程控制

pthread

pthread库是一个动态库

image-20230203164124314

在内核中并没有线程,是使用LWP(轻量级进程)模拟的,而在用户层面需要创建进程、等待进程……,所以在在用户层和内核之间使用pthread库来实现。比如:用户创建线程,通过线程库,在内核中创建轻量级进程。

而且管理线程(先描述,再组织)是在pthread库中进行的

线程栈:每个线程有自己独立的栈,而主线程则直接使用进程地址空间中的栈区;

线程局部存储:同一个进程的多个线程对于全局变量是共享的,那么如果我们想让每个线程对于这个全局变量是独立的,那么每个线程的独立的全局变量就存储再线程局部存储中。

__thread int global=100;	//在全局变量前加上__thread,线性局部存储

image-20230203163808136

pthread_t类型是线程ID,但是和LWP(线程ID)不同

LWP(线程ID)是属于进程调度的范畴,内核中,线程是用轻量级进程模拟的,是操作系统调度的基本单位,LWP(线程ID)是用来唯一的标识线程的。

pthread_t类型的线程ID,本质上是一个地址,这个地址指向pthread动态库中的线程控制块,

创建线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg)
/创建线程
/thread: 线程ID(但不是LWP)
/attr: 设置线程ID,NULL为线程的默认属性
/start_routine: 该线程执行的函数
/arg: 传给函数的参数
/返回值:成功返回0,失败返回错误码
线程等待
int pthread_join(pthread_t thread, void **value_ptr);//线程退出后,需要进行线程等待,才能释放线程的资源
/thread :线程ID
/value_ptr :一个输出型参数,指向线程的返回码
/返回值:成功返回0,失败返回错误码

获取退出进程的退出码:
void *retval=nullptr;
pthread_join(tid1,&retval);
cout<<*((int*)retval)<<endl;		/退出码

代码

void *func1(void *args)
{
     while (true)
     {
           string name = (char *)args;
           cout << name << "正在运行" << endl;
           sleep(1);
     }
}

void *func2(void *args)
{
     while (true)
     {
           string name = (char *)args;
           cout << name << "正在运行" << endl;
           sleep(1);
     }
}

int main()
{
    pthread_t tid1, tid2;
     / 创建线程
     pthread_create(&tid1, nullptr, func1, (void *)"thread1");
     pthread_create(&tid2, nullptr, func2, (void *)"thread2");
     while (true)
     {
         cout << "主线程正在运行" << endl;
           sleep(1);
       }
     / 释放线程
     pthread_join(tid1, nullptr);
     pthread_join(tid2, nullptr);
 
    return 0;
 }

image-20230131190104760

查看进程

ps axj|head -1 && ps axj|grep mytest

image-20230131190430380

查看线程

ps -aL

image-20230131190330343

LWP:线程ID

查看线程自己的ID

pthread_t pthread_self(void);
void *func1(void *args)
{
    sleep(1);
    pthread_t id = pthread_self();
    cout << "tid1: " << id << endl;
}
void *func2(void *args)
{
    sleep(2);
    pthread_t id = pthread_self();
    cout << "tid2: " << id << endl;
}
int main()
{
    pthread_t tid1, tid2;

    pthread_create(&tid1, nullptr, func1, (void *)"thread1");
    pthread_create(&tid2, nullptr, func2, (void *)"thread2");
    sleep(3);
    pthread_t id = pthread_self();
    cout << "主线程: " << id << endl;
    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);

    return 0;
}

image-20230131205029367

线程退出

**pthread_exit函数 **

void pthread_exit(void *value_ptr);/线程退出
/等同于return (void*)退出码

例子:
    int *p =new int(10);
    pthread_exit((void*)p);

pthread_cancel函数

int pthread_cancel(pthread_t thread);/取消一个执行中的线程
/thread 线程ID

return 退出码

return (void*)退出码;

例子:
    int *p =new int(10);
    return (void*)p;

注意:主线程结束,整个进程直接而结束

线程分离
int pthread_detach(pthread_t thread);/分离线程,不需要回收线程,操作系统直接释放线程
/可以由本线程分离,也可由其他线程分离
/一个线程如果分离了,就不能对该线程进行线程等待    

线程互斥

线程互斥相关概念

临界资源:多线程执行流共享的资源就叫做临界资源

临界区:每个线程内部,访问临界资源的代码,就叫做临界区

互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用

原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

线程互斥

为什么需要线程互斥,看一个例子

一个关于抢票的代码:

int tickets = 10000;

/ 抢票
void *function(void *argc)
{
    while (true)
    {
        if (tickets > 0)
        {
            usleep(3000);
            cout << (char *)argc << " ticket:" << tickets << endl;
            tickets--;
        }
        else
        {
            cout << "failed ticket:" << tickets << endl;
            break;
        }
    }
}

int main()
{
    pthread_t tid1, tid2, tid3, tid4;
    / 创建线程
    pthread_create(&tid1, nullptr, function, (void *)"thread1");
    pthread_create(&tid2, nullptr, function, (void *)"thread2");
    pthread_create(&tid3, nullptr, function, (void *)"thread3");
    pthread_create(&tid4, nullptr, function, (void *)"thread4");
    / 主线程
    cout << tickets << endl;
    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    pthread_join(tid3, nullptr);
    pthread_join(tid4, nullptr);

    return 0;
}

image-20230206211517208

为什么会出现上面这种情况?

因为在判断完票数不为零和tickets--之间,发生了线程切换(因为多个线程并发执行),其他线程票数–了,等线程再切回来,再–,票数就可能变成负数

所以需要满足以下三点:

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

为了解决这个问题,满足这三点,就需要加锁

互斥量

LINUX中把加的这把锁,叫做互斥量

加锁的本质就是将加锁的这段代码由并发执行变为串行执行

一般情况下,加锁的粒度越小越好,只给临界区加锁

互斥量接口

初始化互斥量
  • 静态分配

    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
    
  • 动态分配

    int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrictattr);
    /mutex:要初始化的互斥量
    /attr:NULL
    
销毁互斥量
  • 使用 PTHREAD_MUTEX_INITIALIZER 初始化的互斥量不需要销毁

  • 不要销毁一个已经加锁的互斥量

  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁

    int pthread_mutex_destroy(pthread_mutex_t *mutex);
    
互斥量的加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);		/加锁
/返回值:成功返回0,失败返回错误号
  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
  • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
int pthread_mutex_unlock(pthread_mutex_t *mutex);	/解锁
/返回值:成功返回0,失败返回错误号

改进抢票代码:

int tickets = 10000;

pthread_mutex_t mymutex;

/ 抢票
void *function(void *argc)
{
    while (true)
    {
        pthread_mutex_lock(&mymutex);
        if (tickets > 0)
        {
            usleep(3000);
            cout << (char *)argc << " ticket:" << tickets << endl;
            tickets--;
            pthread_mutex_unlock(&mymutex);
        }
        else
        {
            pthread_mutex_unlock(&mymutex);
            cout << "failed ticket:" << tickets << endl;
            break;
        }
    }
}

int main()
{
    pthread_mutex_init(&mymutex,nullptr);
    pthread_t tid1, tid2, tid3, tid4;
    / 创建线程
    pthread_create(&tid1, nullptr, function, (void *)"thread1");
    pthread_create(&tid2, nullptr, function, (void *)"thread2");
    pthread_create(&tid3, nullptr, function, (void *)"thread3");
    pthread_create(&tid4, nullptr, function, (void *)"thread4");
    / 主线程
    cout << tickets << endl;
    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    pthread_join(tid3, nullptr);
    pthread_join(tid4, nullptr);
    pthread_mutex_destroy(&mymutex);
    return 0;
}

互斥量实现原理

image-20230209210511641

如上图代码:

加锁:

  • 将0写入寄存器
  • 交换寄存器和内存中mutex的数据
  • 如果寄存器大于零,加锁成功,否则,阻塞等待
  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值