Linux_多线程(Linux入门到精通)

目录

[1. 了解线程概念,理解线程与进程区别与联系。](#1. 了解线程概念,理解线程与进程区别与联系。)

[2. 学会线程控制,线程创建,线程终止,线程等待。](#2. 学会线程控制,线程创建,线程终止,线程等待。)

[3. 了解线程分离与线程安全概念。](#3. 了解线程分离与线程安全概念。)

1分离线程

[2 Linux线程互斥](#2 Linux线程互斥)

3可重入VS线程安全

4条件变量

[5生产者消费者模型(CP问题 ---consumer producter)](#5生产者消费者模型(CP问题 ---consumer producter))

[4. 学会使用互斥量,条件变量,posix信号量,以及读写锁。](#4. 学会使用互斥量,条件变量,posix信号量,以及读写锁。)

[1 信号量的初始化](#1 信号量的初始化)

[2 信号量的销毁​编辑](#2 信号量的销毁编辑)

[3 信号量的P操作​编辑](#3 信号量的P操作编辑)

[4 信号量的V操作​编辑](#4 信号量的V操作编辑)

5线程池

1. 了解线程概念,理解线程与进程区别与联系。

线程是一种特殊的进程

线程:线程是进程内的一个执行分支,线程的执行粒度要比进程更细

其实线程也是一个子进程,当然他也有自己的task_struct,但是他没有自己独立的地址空间和页表,他和父进程共用一个地址空间和页表,正文代码部分分一部分给这个进程,这个进程被称为线程

1 重新定义线程和进程

什么叫做线程?

线程是操作系统调度的基本单位

重新理解进程?

内核观点:进程是承担分配系统资源的实体(进程=内核数据结构task_struct+代码和数据--->(这是我们之前的理解))

所以进程是包含线程的,线程是我们进程内部的执行流资源

如何理解以前的进程?

操作系统以进程位单位,给我们分配资源,我们当前进程内部只有一个执行流(执行流其实也是资源)

对于这个线程,按照我们之前的理解,要先描述再组织,要创建他的进程控制块(struct tcb //struct ctrl block),Windows是这样做的,但是Linux中,服用了进程数据结构和管理算法,所以就用struct task_struct 去模拟线程

所以Linux没有真正意义上的线程(没有自己的进程控制块),而是用"进程"模拟的线程

所以linux操作系统是比较简洁的,他的稳定性比Windows强的不是一点

cpu执行的时候部分进程和线程,这个进程和线程的概念是操作系统层面上的,Linux中的执行流称为轻量级进程

可以把进程比作--家庭,把线程比作--家庭成员
下面这张图就是cpu中得到的虚拟地址如何转化为物理地址(读进cpu内部的地址是虚拟地址)

32位机器的地址大小为4字节,32个比特位,cpu将这32个比特位分为10-10-12 三个部分,

然后通过一级页表和二级页表,二级页表存储的就是物理内存的地址,物理内存是分页的,每一页大小一般都是4kb,2^12大小就是4096,正好就可以进行偏移,能够访问每一页的任何地方

起始地址+类型 = 起始地址+偏移量 (x86的特点)

线程分配资源的本质,就是分配地址空间范围

2 线程为什么比进程更轻量化?

a,创建和释放更加轻量化(生死)

b,切换更加轻量化(运行)

线程切换的时候,cpu中缓存的catch数据不会改变(因为线程公用这些catch),而进程之间的切换时肯定要加载新的catch数据(即线程切换不需要热缓存)

因为内核当中没有线程的概念,所以不会给我们提供线程的系统调用,只会给我们提供轻量级进程的系统调用

但是我们用户需要线程的接口0---->pthread线程库(应用层) 这是轻量级进程接口进行封装,为用户提供直接线程的接口

几乎所有的linux平台,都是自带这个库的

Linux中编写线程代码需要使用第三方pthread库

3 线程的优缺点

4 进程VS线程的线程

进程是资源分配的基本单位

线程是调度的基本单位

线程共享进程数据,但也拥有自己的一部分数据:

线程ID

一组寄存器

errno

信号屏蔽字

调度优先级

进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中 都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:

文件描述符表

每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)

当前工作目录

用户id和组id

进程和线程的关系如下图:

2. 学会线程控制,线程创建,线程终止,线程等待。

1 线程创建

因为要链接库,所以编译时要用 -lpthread 这个选项就可以了

ldd +你生成的可执行程序 ,就可以看到这个可执行程序链接的动态库

ps -aL : 显示所有轻量级进程(线程)

LWD: light weight process 轻量级进程的ID,当一个轻量级进程的PID == LWD ,那这个线程就是主线程,所以线程之间,PID相同,LWD不同

注意:任何一个线程被kill -9 掉之后,所有相关的线程都挂,因为这个信号是发给进程的,所有的线程都归属于进程的一部分

这个传递的东西也可以传递对象,返回也可以返回对象(都是用对象的指针),接收过来的参数式void* 将这个指针强转为对象的指针就可以了

功能:创建一个新的线程 原型

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

参数

thread:返回线程ID

attr:设置线程的属性,attr为NULL表示使用默认属性

start_routine:是个函数地址,线程启动后要执行的函数(返回值void* ,参数为void* args)

arg:传给线程启动函数的参数

返回值:

成功返回0;失败返回错误码

指针的大小时跟平台有关的,linux下,大小是8字节

2 线程等待

当然线程也有回收机制,主线程也要对创建的线程去等待

这个等待是阻塞的

这个二级指针式输出型参数,通过这个来获得进程执行完成函数的返回值(类型式void*)

为啥要用这个二级指针?

只有这个函数里面可以访问到函数退出的时候的返回值,通过你传入相应的类型指针void**,就可以把这个返回值带出来

3 线程终止

而且在线程执行函数当中不能用exit()终止线程,这个是用来终止进程的

通过pthread_exit()可以终止线程


主线程也可以去提前终止目标线程

C++ 语言本身也支持多线程,引入<thread>就可以使用了(在linux环境下,g++编译的时候仍然要加 -lpthread,重这个就可以看出来,c++11里面的多线程就是封装原生线程库)

我们刚刚讲的是原生的线程库

4 获取线程id

获取当前线程自己的tid

5 线程ID及进程地址空间布局

pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是 一回事。

前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要 一个数值来唯一表示该线程。

pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于 NPTL线程库(原生线程库)的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。

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

线程库是加载到内存的(动态库)

每一个线程的库级别的tcb(线程控制块,用数组组织起来)的起始地址,叫做线程的tid(就是地址空间的一块地址,也是我们创建线程获得的)

这个就是线程的tcb线程控制块,包含很多属性

所以除了主线程,所有的其他线程的独立栈,都在共享区,具体来讲是在pthread库中,tid指向的是用户的tcb中!

注意:这个栈结构式必须的,是(独立)调用的关键

Linux中的线程是用户级线程,内核级线程是在内核中实现线程的概念(tcb)

每一个线程都有自己的栈结构,线程线程之间,没有密码,线程栈上面的数据也是可以被其他线程访问的

全局变量是被所有的线程同时看到并访问,如果线程想要一个私有的全局变量呢?

把全局变量定义前面加一个__thread ,每一个线程访问的这个全局变量就会每个线程各自私有一份,但是他们的值是同步的(一样的,哪个线程改变这个变量,所有线程各自私有的变量都会变化)

而且这个__thread只能用于定义内置类型,不能用来修饰自定义结构体

这个存储方法叫做线程局部存储

3. 了解线程分离与线程安全概念。

1分离线程

默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资 源,从而造成系统泄漏。

如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程 资源。

2 Linux线程互斥

共享数据的数据不一致问题是由多线程并发访问造成的

1 创建互斥锁

因为修改一个变量的步骤 : tickets--

1 先将变量tickets读取到cpu中的寄存器中

2 cpu内部进行--操作

3 将计算结果写回到内存中

解决方法---对共享数据的任何访问,保证任何时候只有一个执行流访问!---->互斥

通过加锁!mutex(互斥锁)

2 使用互斥锁
加锁的本质:以时间换取安全

加锁的表现:线程对于临界区的代码时串行执行

加锁的原则:尽量要保证临界区代码,越少越好

如果定义是全局的或者是静态的锁,那么就可以不用初始化,只要附一个初始值就可以了

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr); 参数:

cond:要初始化的条件变量

attr:NULL

而且线程对于锁的竞争能力可能不同(高频调度的时候)

纯互斥环境,如果锁的分配不够合理,容易导致其他线程的饥饿问题(这个问题真一定存在的)

让所有的线程获取锁,按照一定的顺序 ----按照一定的顺序获取资源--->同步

有一个问题-->锁本身就是共享资源

所以,申请锁和释放锁本身就被设计成原子性操作

设计成原子的方法:通过把锁这个资源交换到线程的上下文中,因为线程的栈是私有的,所以就变成了原子的了

3可重入VS线程安全

概念

线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作, 并且没有锁保护的情况下,会出现该问题。

重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们 称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重 入函数,否则,是不可重入函数。

死锁

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资 源而处于的一种永久等待状态。

简单来说就是有两个锁,一个进程申请不释放自己申请过的锁而去申请另外一个进程持有锁,另外一个进程也是同样的样子,这就造成了死锁

条件(必要)

互斥条件:一个资源每次只能被一个执行流使用

请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放

不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺

循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

解决死锁问题:理念就是破坏四个必要条件就可以了

同步问题就是保证数据安全的情况下,让我们的线程访问资源具有一定大的顺序性

4条件变量

当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。

例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情 况就需要用到条件变量。

1条件变量函数 初始化

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);

参数:

cond:要初始化的条件变量

attr:NULL

条件变量的使用必须依赖锁

2条件变量的使用

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);

参数:

cond:要在这个条件变量上等待(其实就是一个队列,线程在上面排队哦)

mutex:互斥量,后面详细解释

1调用的时候,自动释放锁

2 因为唤醒二返回的时候,重新持有锁

3唤醒条件变量下等待的线程

唤醒一个线程

唤醒所有线程

注意:判断临界资源的时候,要把判断放在加锁之后

因为判断资源是否是就绪状态也是访问临界资源,所以这样

5生产者消费者模型(CP问题 ---consumer producter)

生产者-消费者问题 (也称有限缓冲问题 ,Bounded Buffer Problem)是操作系统与并发编程中的一个经典同步问题,用于描述多个线程或进程如何安全、高效地共享一个固定大小的缓冲区

那么生产者和消费者就有并发访问问题

生产者vs生产者:互斥

生产者vs消费者:互斥(为了安全),同步

消费者vs消费者:互斥

321原则

三种关系

两种角色:生产者和消费者

一个交易场所:特定结构的内存空间

BlockingQueue 在多线程编程中阻塞队列(Blocking Queue)是一种常 用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会 被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取 出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)

生产和消费模型也是高效的(可以并发访问数据)

生产者:1 获取数据 2 生产数据到队列

消费者: 1 消费数据 2 加工处理数据

1生产消费模型代码示例

BlockQueue.hpp

cpp 复制代码
#pragma once
#include <iostream>
using namespace std;
#include <queue>
#include <pthread.h>

template <class T>
class BlockQueue
{
    static const int defaultnum = 20;

public:
    BlockQueue(int maxcap = defaultnum)
        : maxcap_(maxcap)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&c_cond_, nullptr);
        pthread_cond_init(&p_cond_, nullptr);
    }

    T pop()
    {
        pthread_mutex_lock(&mutex_);
        while (q_.size() == 0)
        {
            pthread_cond_wait(&c_cond_, &mutex_); // 没有数据的时候就等待
        }
        T out = q_.front();
        q_.pop();
        pthread_cond_signal(&p_cond_);
        pthread_mutex_unlock(&mutex_);

        return out;
    }
    void push(const T &in)
    {
        pthread_mutex_lock(&mutex_);
        while (q_.size() == maxcap_)
        {
            pthread_cond_wait(&p_cond_, &mutex_); // 没有数据的时候就等待
        }
        q_.push(in);
        pthread_cond_signal(&c_cond_);
        pthread_mutex_unlock(&mutex_);
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&c_cond_);
        pthread_cond_destroy(&p_cond_);
    }

private:
    queue<T> q_;
    int maxcap_; // 队列的极值
    pthread_mutex_t mutex_;
    pthread_cond_t c_cond_; // 消费和生产用不同的条件变量
    pthread_cond_t p_cond_;
};

main.cc

cpp 复制代码
#include "BlockQueue.hpp"
#include <unistd.h>
void *Consumer(void *args)
{
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);
    while (true)
    {
        // 消费
        int t = bq->pop();
        cout << "消费者获得: " << t << endl;
    }
}
void *Productor(void *args)
{
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);
    int i = 0;
    while (true)
    {
        bq->push(i);
        cout << "生产者生产: " << i << endl;
        i++;
        sleep(1);
    }
}
int main()
{
    // BlockQueue 内部可以传递任务和其他东西
    BlockQueue<int> *bd = new BlockQueue<int>();
    pthread_t p, s;
    pthread_create(&p, nullptr, Productor, bd);
    pthread_create(&s, nullptr, Consumer, bd);

    pthread_join(p, nullptr);
    pthread_join(s, nullptr);
    delete bd;
    return 0;
}

4. 学会使用互斥量,条件变量,posix信号量,以及读写锁。

临界资源可以分为多份,份数可以通过信号量来控制,多份之间互不干扰

信号量的本质是一把计数器,用来描述资源数目的,把资源是否就绪放在了临界区外

基于环形队列的生产消费模型

在程序中用的是数组来模拟环形队列

生产者与消费者之间的互斥和并发运行由信号量来维持了

消费者与消费者,生产者与生产者之间是互斥的关系,那么这两个之间就要用互斥锁来保持互斥关系,即用两把锁即可

1 信号量的初始化

2 信号量的销毁

3 信号量的P操作

获取信号量(信号量减少)

4 信号量的V操作

释放信号量(信号量增加)

5线程池

池化技术:以空间换时间

线程池其实就是一个生产消费者模型,主线程发布任务,这个线程池自动去调用线程去执行任务

线程池代码

ThreadPool.hpp

cpp 复制代码
#pragma once
#include <iostream>
using namespace std;
#include <vector>
#include <queue>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include <cstdbool>

struct ThreadInfo
{
    pthread_t tid;
    string name;
};
static const int defaultnum = 5;

template <class T>
class ThreadPool
{
public:
    void Lock()
    {
        pthread_mutex_lock(&mutex_);
    }
    void Unlock()
    {
        pthread_mutex_unlock(&mutex_);
    }
    void Wakeup()
    {
        pthread_cond_signal(&cond_);
    }
    void ThreadSleep()
    {
        pthread_cond_wait(&cond_, &mutex_);
    }

    bool IsQueueEmpty()
    {
        return tasks_.empty();
    }
    T Pop()
    {
        T t = tasks_.front();
        tasks_.pop();
        return t;
    }

public:
    ThreadPool(int num = defaultnum)
        : threads_(num)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
    }

    static void *HanderTask(void *args) // 静态成员函数没有this指针,就可以对应上了
    {
        ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);

        while (true)
        {
            tp->Lock();
            if (tp->IsQueueEmpty())
            {
                tp->ThreadSleep();
            }
            T t = tp->Pop();
            tp->Unlock();
            t();
            cout << "result: " << t.GetResult() << endl;
        }
    }

    void Start()
    {
        int num = threads_.size();
        for (int i = 0; i < num; i++)
        {
            threads_[i].name = "threads-" + to_string(i);
            pthread_create(&(threads_[i].tid), nullptr, HanderTask, this);
        }
    }

    void Push(const T &t)
    {
        Lock();
        tasks_.push(t);
        Wakeup();
        Unlock();
    }

    ~ThreadPool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }

private:
    vector<ThreadInfo> threads_;
    queue<T> tasks_;

    pthread_mutex_t mutex_;
    pthread_cond_t cond_;
};
相关推荐
晴天¥2 小时前
操作系统由MBR->GPT,导致系统黑屏是怎么回事?
linux
王九思2 小时前
Linux cgroup 简介
linux·运维·服务器
皓月盈江2 小时前
个人计算机Linux Debian桌面操作系统上网安全防护措施
linux·ubuntu·网络安全·debian·桌面操作系统·上网安全防护措施
zl_dfq2 小时前
Linux 之 【文件】(动静态库的制作与使用、ar、ldconfig)
linux
久绊A2 小时前
磁盘故障处理
linux·运维·服务器
JANG10243 小时前
【Linux】进程通信
linux·运维·chrome
viqjeee3 小时前
RK3288设备树介绍和配置
linux·设备树
末日汐3 小时前
Linux进程信号
linux·运维·服务器
无垠的广袤4 小时前
【工业树莓派 CM0 NANO 单板计算机】YOLO26 部署方案
linux·python·opencv·yolo·树莓派·目标识别