【Linux】线程控制

前言
本节我们将对线程id进行了解,线程私有栈等,学会线程控制,线程创建,线程终止,线程等待…

1. 线程的id

在这里插入图片描述

  • 线程创建和进程一样也得有线程id。注意: pthread_t tid,是个地址。

1.1 pthread_self:

线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID.

pthread_t pthread_self(void);

注意:

  • pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址
    在这里插入图片描述

1.2 线程独立栈结构:

libpthread. so是Linux系统中用于支持多线程编程的共享库,其中包含了与线程管理相关的函数和数据结构

  • 线程的全部实现,并没有全部体现在OS内,而是OS提供执行流,具体的线程结构由库来进行管理

Linux的线程库,虽然是原生的,在系统当中会内置的线程库,但是依旧是用户级线程库。
因为Linux没有真线程,就没办法提供真线程的调用接口。
共享库(Shared Library)是被所有线程共享的。

库可以创建多个线程,那么库要不要管理线程呢?
答案:是的。

  • 要想管理就要,先描述,再组织
  • 再库里面有一个struct thread_info结构体,是用来描述创建的线程的。
  • 这个结构体中有线程的tid,还有一个重要的就是指向线程私有栈的栈顶指针。
    在这里插入图片描述
  • pthread_t对应的就是用户级线程的控制结构体的起始地址:
  • 可以通过该地址找到线程相关属性。
  • 用户的所有创建线程、等待线程、获取线程的各种属性,都是这个库帮我们去做的,控制进程内的轻量级进程来完成的。
  • 每一个线程都有一个thread_info,而它的属性里面就有私有栈
  • 主线程的独立栈结构,用的就是地址空间中的栈区,新线程用的栈结构,用的是库中提供的栈结构

备注:

用户级线程:栈是由库来维护,但是还是用户提供的
pthread_info不是在库里面直接创建的,而是由操作系统内核在创建线程时生成的线程控制块的信息
Linux中,线程库用户级线程库,和内核的LWP是1 : 1

1.3 线程的局部存储:

线程的局部存储,代表的是,我们的线程除了保存临时数据时有自己的线程栈,我们的ptread库还提供了一种能力:

  • 如果我们定义了一个全局的变量,但是这个全局的变量想让每个线程各自私有,那么就可以使用线程局部存储这样的概念。
  • 在全局变量前加上__thread即可。
  • 可以理解成将这个全局变量都拷贝一份给对应的线程,所以每个线程都有自己的。

2. 线程互斥

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

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

  • 进程代码中,有大量的代码,只有一部分代码,会访问临界资源

  • 多个进程对临界资源做读写的代码,我们称之为临界区。

2.1 互斥性与原子性:

  • 原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完
  • 互斥: 任何时刻,只允许一个进程/线程,访问临界资源。

互斥量mutex

  1. 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个 线程,其他线程无法获得这种变量。
  2. 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之 间的交互。
  3. 多个线程并发的操作共享变量,会带来一些问题

2.2 线程安全:

线程安全是指在多线程环境下,对共享资源的访问不会导致数据不一致或者出现意料之外的结果。

当多个线程同时访问共享资源时,如果没有适当的同步机制或保护措施,可能会导致以下问题:

  1. 竞态条件(Race Condition):多个线程对同一资源进行读写操作,由于执行顺序不确定,可能导致结果的不确定性、错误的计算结果或数据丢失等问题。
  2. 数据竞争(Data Race):多个线程同时对同一数据进行读写操作,由于缺乏同步机制,可能导致数据的不一致性或错误的结果。

为了确保线程安全,需要采取合适的并发控制措施,如加锁机制、原子操作、信号量等。这些机制可以保证在任意时刻只有一个线程能够访问共享资源,避免数据竞争和竞态条件的发生。

补充知识点:

  • 一个线程对全局变量的恶意修改 ,可能会影响其他线程安全。
  • 一个线程有bug导致线程退出,导致其他线程也退出,也叫做线程退出。
  • 绝大多数的系统自带的库(比如C++的STL库)都是不可重入的,并非所有容器都是线程安全的。

2.3 线程加锁与解锁:

  • 加锁:
    定义全局的互斥锁,所有线程能访问

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

方法一:静态分配

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

方法二:动态分配

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const
pthread_mutexattr_t *restrict attr); // 参数: // mutex:要初始化的互斥量 //
attr:NULL

  • 解锁 / 销毁锁:

在这里插入图片描述

int pthread_mutex_destroy(pthread_mutex_t *mutex)

销毁互斥量需要注意:

  • 使用 PTHREAD_MUTEX_INITIALIZER 初始化的互斥量不需要销毁。
  • 不要销毁一个已经加锁的互斥量。
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁。

2.4 锁的加锁范围与锁的原子性讨论

  • 加锁的范围

  • 只要对临界区加锁,而且加锁的粒度越细越好。

  • 锁保护的是临界区,任何线程执行临界区代码访问临界资源,都必须先申请锁,前提是都必须先看到锁.

  • 临界区中存在if条件时,需要在其前加锁

while ( 1 ) 
{
       pthread_mutex_lock(&mutex);
        if ( ticket > 0 ) {
             usleep(1000);
             printf("%s sells ticket:%d\n", id, ticket);
             ticket--;
           pthread_mutex_unlock(&mutex);
            // sched_yield(); 放弃CPU
         }
      else {
          pthread_mutex_unlock(&mutex);
          break;
           }
}
  • 锁的原子性讨论

锁,本身不就也是临界资源吗,谁来给它加锁呢?锁的设计者早就想到了。

  • pthread_mutex_lock竞争和申请锁的过程,就是原子的!

为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性。

我们看一下pthread_mutex_lock和pthread_mutex_unlock的伪代码:

在这里插入图片描述

2.5 死锁

2.5.1 死锁的概念
  • 两个线程拿着对方要的锁,自己又抱着一把锁。
  • 线程1拿着A,线程2拿着B,但是这两个线程又都向对方要锁。
  • 互相申请对方的锁,但是自己要的锁已经被对方拿走了
2.5.2 死锁的四个必要条件:
  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

当以上四个条件同时满足时,就可能发生死锁。

避免死锁的条件:

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

2.6 Linux线程同步

互斥有可能会导致饥饿问题:

  • 一个执行流,长时间得不到某种资源
  • 一个线程的优先级很高,每次竞争锁时,都可以优先竞争到
  • 就会导致其他线程一直竞争不到锁,造成饥饿问题。。

Linux中提供了完成同步的重要机制,叫做:条件变量。(最常用,没有之一,最常用的线程同步的策略)

2.6.1 条件变量

条件变量要和mutex互斥锁,一并使用!

  • 初始化互斥量有两种方法:

方法一:全局的初始化方法

> pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

方法二:局部的初始化

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict
attr);
参数:
cond:要初始化的条件变量
attr:NULL
  • 条件变量的销毁:
int pthread_cond_destroy(pthread_cond_t *cond)
  • pthread_cond_wait

在这里插入图片描述

  • 其中timewait接口可以在条件变量下等,设置等待的时间(超时了就不等了)。

  • 条件变量也是临界资源,所以这里需要一个mutex互斥锁来保证条件变量读写的原子性。

  • 唤醒线程

在这里插入图片描述
broadcast是给在当前条件变量等待的所有线程发信号唤醒,而signal是发送信号只唤醒一个线程。

  • 条件变量决定,什么时候叫醒一个线程,以前只要有锁,如果所有线程都被叫醒,大家都去参与竞争,谁抢到了算谁的,这个机制完全是由调度器决定的。

pthread_cond_wait内部已经帮我们想到了这一点,并做了相应的措施:(重点)

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex)
{
	pthread_mutex_unlock(mutex);// 先解锁
    // 避免因为该线程拿着锁去休眠了,导致其他线程无法申请该锁
  
	//条件变量相关代码
	
    pthread_mutex_lock(mutex);// 条件满足后,再加锁
}

条件变量经典错误

pthread_mutex_lock(&mutex);
while (condition_is_false) {
    pthread_mutex_unlock(&mutex);
    pthread_cond_wait(&cond);// 解锁和加锁的操作,该函数会帮我们完成
    pthread_mutex_lock(&mutex);// 二次申请同一把锁,出现死锁!
}
pthread_mutex_unlock(&mutex);

代码演示:

#include <iostream>
#include <vector>
#include <cstring>
#include <functional>
#include <unistd.h>
#include <pthread.h>

using namespace std;

// 定义一个条件变量
pthread_cond_t cond;

// 定义一个互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 当前不用,但是接口需要,所以我们需要留下来

// 定义全局退出变量
volatile bool quit = false;

// 三个线程都会调用这个函数
void *waitCommand(void *args)
{
    string name = static_cast<char*>(args);
    
    pthread_mutex_lock(&mutex);
    // 如果不退出一直去运行
    while (!quit)
    {
        // pthread_cond_wait内部先解锁
        pthread_cond_wait(&cond, &mutex);
        // 被唤醒出来之后锁已经加上了

        cout << "thread id: " << pthread_self() << " running... " << name << endl;
    }
    pthread_mutex_unlock(&mutex);
    cout << "thread quit..." << (char*)args << endl;

    return nullptr;
}

int main()
{
    // 初始化一个条件变量
    pthread_cond_init(&cond, nullptr);

    // 创建三个线程
    pthread_t t1, t2, t3;
    pthread_create(&t1, nullptr, waitCommand, (void*)"thread1");
    pthread_create(&t2, nullptr, waitCommand, (void*)"thread2");
    pthread_create(&t3, nullptr, waitCommand, (void*)"thread3");

    // 主线程控制
    while(true)
    {
        char n;

        // cin和cout在交叉使用的时候,缓冲区会做强制刷新
        cout << "请输入你的command: ";
        cin >> n;

        if(n == 'n') 
        {
            // 唤醒在特定条件变量下等的线程
            pthread_cond_signal(&cond); 
        }
        else if (n == 'x')
        {
            pthread_cond_broadcast(&cond);
        }
        else
        {
            quit = true;
            break;
        }
        
        usleep(100);
    }
    
    // 唤醒所有等待的条件变量
    pthread_cond_broadcast(&cond);
    cout << "main thread quit..." << endl;

    // 释放条件变量
    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);

    // 等待线程
    int m = pthread_join(t1, nullptr);
    cout << strerror(m) << endl;
    m = pthread_join(t2, nullptr);
    cout << strerror(m) << endl;
    m = pthread_join(t3, nullptr);
    cout << strerror(m) << endl;

    return 0;
}

条件变量使用规范

  • 等待条件代码
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
  • 给条件发送信号代码
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex)

尾声
看到这里,相信大家对这个Linux 有了解了。
如果你感觉这篇博客对你有帮助,不要忘了一键三连哦

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值