Linux复习之线程管理

目录

一.线程介绍

线程的优点

线程的缺点

线程的私有资源

二.线程的基本操作

pthread_create创建

ps -aL查看线程pid

 线程退出

return

pthread_exit(void* value_ptr)

线程取消

线程的等待

线程等待的必要性

线程的分离

三.互斥量

进程线程间的互斥的相关概念

linux互斥量(锁)

 互斥量(锁)的原理探究

互斥量的优点

互斥量缺点

死锁

 死锁的4个必要条件

避免死锁

四.条件变量

1. 同步概念

2.条件变量概念

条件变量函数

条件变量的操作函数

3.生产与消费者模型

生产者消费者模型优点

生产者消费者的321原则

生产者消费者问题

BlockingQueue的生产者消费者模型

 五.线程池

线程池的实现


一.线程介绍

线程是进程里面单一的执行控制序列(执行流),也是cpu调度的基本单位(分配给cpu执行最小单位),假如进程的运行是为了完成某个任务,线程是该任务的许多子任务之一,线程的创建,操作系统并不会给它分配内存资源,进程中所有线程共享所属进程地址空间上的资源。如果进程里只有一个线程,也就是只有一个线程在cpu中执行该进程的代码和数据。如果有多个线程,那么这些线程轮流在cpu中执行该进程的代码和数据

在linux中,线程是在进程的基础改的,线程的控制块也是pcb(进程控制块是pcb),只是当我们在一个进程内部多创建一个线程的时候,不需要为线程创建进程地址空间,只需要多创建一个pcb,然后线程的pcb指向原本的进程所属的资源,这样线程就可以共享进程空间上的所有资源。

在linux内核中,是区分不出来进程还是线程的,因为线程和进程的控制块都是pcb

 多线程的进程:多个pcb

栈区是主线程的私有栈,其它线程的私有栈在共享区域中的线程库里面,各个线程运行的代码都在代码区中,在动态库中,每个新创建的线程都有struct pthread和线程局部存储和线程的私有栈struct pthread存储的是线程基本属性,线程的局部存储的是寄存器的数据。

线程的优点

1.提高程序的并发性,例如我们在使用迅雷的时候,我们可以边看边下载,这个原理就是让迅雷其中一个线程去执行播放的任务,让一个线程去执行下载任务。
2.线程的创建比进程的创建开销小,在linux中,线程的创建只需要多创建一个pcb,然后指向所属进程的资源,而进程的创建,需要为进程创建pcb和进程的地址空间,页表和文件描述符等资源。
3.线程切换比进程切换要容易,在同一个进程中,线程只需要切换pcb即可,而进程的切换需要切换pcb和进程的地址空间,文件描述符等资源。
4.通信更加方便,因为进程内的所有线程共有一张进程地址空间,所以它们可以看到进程内中的大部分资源。而进程通信需要创建内核创建缓冲区(共享内存,管道等)进行通信。

线程的缺点

  1. 线程不好调试(gdb支持不好,只能依赖程序员的经验)
  2. 不能支持信号
  3. 若进程内的其中一个线程崩溃,则整个进程崩溃,例如,在迅雷内有一个线程在执行下载任务,一个线程在执行观看任务,其中下载任务的线程崩溃了,那么整个迅雷也就退出了。

线程的私有资源

线程共享进程中的数据,但是它也有自己的数据:

  1. 私有栈;
  2. 寄存器;
  3. 线程ID;
  4. 调度的优先级;

二.线程的基本操作

pthread_create创建

每个进程创建的时候都会伴随着一个线程的产生,这个线程叫做主线程,如果我们想要在一个进程里面多创建一个新的线程去执行其它任务,那么我们可以用pthread_create进行创建。

       #include <pthread.h>
 
       int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);

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

thread:返回线程的id

attr:设置线程的属性,默认属性传入没NULL

start_routine:创建线程执行路线的起始函数,该函数默认的参数是void* 参数是void*

arg:传给 start_routine函数的参数。

ps -aL查看线程pid

查看LWP,每个线程都有一个LWP,这个LWP是cpu调度的基本单位,之前我们没学过线程,会认为pid是cpu调度基本单位,实际cpu调度是按LWP进行调度的。有些线程LWP不同,但是pid是相同的,这些线程同属于一个进程。

 线程退出

return

在线程的起始函数中遇到return则该线程退出,线程调用的函数不算线程退出。如果是在main函数中return,则整个进程都退出。 

pthread_exit(void* value_ptr)

void* value_ptr:退出后的返回值。

线程取消

       #include <pthread.h>
 
       int pthread_cancel(pthread_t thread);

 int pthread_cancel(pthread_t thread);

pthread_t thread:线程id

功能:取消线程,使该线程退出,可以让一个线程取消另一个线程,也可以线程自己取消自己。

被取消的线程的退出码是-1 .

线程的等待

       #include <pthread.h>
 
       int pthread_join(pthread_t thread, void **retval);

int pthread_join(pthread_t thread, void **retval);

thread: 等待线程的id;

retval:被等待的线程退出后的退出信息保存在retval,如果不关心可以设置为NULL。

线程等待是阻塞的,假设主线程在等待新线程退出并获取它的退出码之前,主线程是不会运行的

线程等待的必要性

1.新线程的创建是为了让它执行某个任务,当新线程退出后,就需要有一个线程等待是为了获取该线程的退出码,然后通过线程退出码,判断新线程的代码运行的结果是否正确,运行正确,则返回return 后面的值,或者pthread_exit()里面的值,若运行错误,则返回。
2.已经退出的线程,如果没有被其它线程等待获取它的退出码,那么其空间不会被释放,仍然在进程的地址空间内,创建新的线程不会复用刚才的线程的地址空间。当然,被分离的线程退出后会自动的释放空间,不需要被等待。
 

线程的分离

线程等待是阻塞的,假设主线程在等待新线程退出并获取它的退出码之前,主线程是不会运行的。

如果不关心线程的返回值,join是一种负担,那么我们可以将线程分离,即告诉操作系统,被分离的线程退出后自动释放资源。

       #include <pthread.h>
 
       int pthread_detach(pthread_t thread);

int pthread_detach(pthread_t thread);

thread:线程id

    static void *Routine(void *arg) // 线程执行流
    {
        pthread_detach(pthread_self());
        ThreadPool *p = (ThreadPool *)arg;
        while (1)
        {
            p->LockQueue();
            while (p->Empty())
            {
                p->Wait(); // 为空就睡觉
            }
            T data;
            p->Pop(data);
            p->UnLockQueue();
            cout << pthread_self() << "# ";
            // 处理任务
            data.Run();
            // 任务仿函数
            sleep(1);
        }
    }

pthread_self() 获取自身的线程id

三.互斥量

进程线程间的互斥的相关概念

  • 临界资源:多线程执行流共享的资源叫做临界资源
  • 临界区:每个线程内部访问临界资源的代码叫做临界区

以线程池为例

    static void *Routine(void *arg) // 线程执行流
    {
        pthread_detach(pthread_self());
        ThreadPool *p = (ThreadPool *)arg;
        while (1)
        {
            p->LockQueue();
            while (p->Empty())
            {
                p->Wait(); // 为空就睡觉
            }
            T data;
            p->Pop(data);
            p->UnLockQueue();
            cout << pthread_self() << "# ";
            // 处理任务
            data.Run();
            // 任务仿函数
            sleep(1);
        }
    }

当我们取出队列中的任务时,因为队列中的任务是,多线程所共享的,是临界资源。并且这段代码是临界区。如果我们没有加锁,那么队列中的任务会被线程“疯抢”,有可能会照成数据混乱,

linux互斥量(锁)

互斥量本质是一把"锁",对临界区加锁后,所有的线程进入临界区前,必须先拿到该锁才能进入临界区,如果拿不到锁的线程,将会被阻塞在锁的位置上,直到当前线程释放该互斥锁。这样做保证不会有两个线程同时访问临界区,解决了线程之间数据不一致的问题,保证了临界资源数据的正确性。

       #include <pthread.h>
       //锁的销毁
       int pthread_mutex_destroy(pthread_mutex_t *mutex);
       //锁的动态初始化
       int pthread_mutex_init(pthread_mutex_t *restrict mutex,
              const pthread_mutexattr_t *restrict attr);
       //锁的静态初始化
       pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
       函数执行成功则返回0,否则返回错误编号

mutex:要初始化的互斥量或者要销毁的互斥量,互斥量类型都必须为pthread_mutex_t类型。

attr:初始化锁的属性,默认设置为NULL

       #include <pthread.h>
 
       int pthread_mutex_lock(pthread_mutex_t *mutex);//加锁
       int pthread_mutex_unlock(pthread_mutex_t *mutex);//解锁

mutex:pthread_mutex_t类型锁。

对代码进行加锁的过程中,我们需要进行5个步骤:

1.创建一把锁, 也就是定义一个pthread_mutex_t的变量,这个变量一般需要定义在全局区,让所有的线程都访问的到。
2.然后对这把锁初始化。动态初始化,静态初始化。
3.将锁放到需要加锁的代码的前面,用pthread_mutex_lock()进行加锁。
4.运行加锁后的代码,进行解锁,用pthread_mutex_unlock()进行解锁
5.锁用完之后需要对锁的销毁,用pthread_mutex_destroy
()对锁进行销毁。

进行加锁后的代码(临界区),可以保证只有一个线程能访问被锁的临界区原理如下:

没有加锁的情况:不管是临界区还是非临界区,所有线程都可以并发运行

 加锁的情况:在非临界区的时候,所有线程是可以并发执行,当线程进入临界区前,线程之间就需要竞争申请锁(注意:每次只能有一个线程竞争到锁),申请到锁的线程才可以进入进入临界区,申请不到锁的线程就只能在锁外面一直等待,当cpu执行到该线程的时候,没有锁只能阻塞在该锁的队列中,不能向下执行代码。当有锁的线程将释放出来后,其他线程才可以去竞争锁,谁竞争到锁,谁就可以进入临界区。这样就可以避免了多个线程在临界区中同时并发执行(如果锁没有释放掉,则外面的线程将会一直等待)。

 互斥量(锁)的原理探究

锁本身是定义全局变量,也属于临界资源,锁的存在是为了保护临界资源,那么锁需要被保护吗?

答案是不需要,因为申请锁的过程是原子性的,申请的过程不会被调度机制给打断的,锁的实现原理探究:为了实现互斥锁的操作,大多数体系结构都提供了swap或者exchange指令,该指令是能够直接将寄存器上的值与内存上单元上的数据直接交换,由于只有一条指令,保证了原子性。

互斥量的优点

保护临界资源,解决线程与线程之间出现数据不一致的问题,保证一次只能有一个线程进入互斥量保护的区域

互斥量缺点

加锁是会损耗线程的性能,因为加锁需要申请锁,和释放锁的过程,并且加锁之后,临界资源一次只能有一个线程进行运行,因此,加锁的区域破坏了多线程并发的过程,所以建议在编码的时候,非必要的情况下最后不要加锁。

死锁

死锁是一组进程中的各个进程均占有不会释放资源,但都需要互相申请其他进程不会释放的资源导致永久的等待的等待。(有点循环引用哪味)

 死锁的4个必要条件

互斥条件:一个锁只能被一个执行流申请到,例如线程一和线程二不能同时申请到锁1和锁2.
请求与保持条件:一个执行流因申请不到锁而阻塞时,不会释放以获得的锁。例如线程一申请锁2失败被挂起等待时,线程1不会释放以获得的锁1.
不剥夺条件:一个执行流以获得的资源,在未使用完之前,不会被强行剥夺,例如线程1不会强行剥夺线程2的锁。
循环等待条件:若干个执行流之间形成一种头尾循环等待资源的关系。例如线程一和线程二之间的加锁顺序不一致,线程一先加锁1,再加锁二,然而线程二先加锁2,再加锁1.

避免死锁

  • 破坏死锁中必要条件中的其中一个条件。
  • 每个线程的顺序一致。
  • 避免锁未释放的场景。
  • 资源一次性分配。

四.条件变量

1. 同步概念

举个栗子:

 你做你的,我做我的,但是有先后

2.条件变量概念

互斥量可以防止多个线程同时访问临界资源,而条件变量允许一个线程将某个临界资源的状态变化通知其他线程,在共享资源设定一个条件变量,如果共享资源条件不满足,则让线程到该条件变量下阻塞等待,当条件满足时,其他线程可以唤醒条件变量阻塞等待的线程。

在线程之间有一种情况:线程A需要某个条件才能继续往下执行,如果该条件不成立,此时线程A进行阻塞等待,当线程B运行后使该条件成立后,则唤醒该线程A继续往下执行。

在pthread库中,可以通过条件变量中,可以设定一个阻塞等待的条件,或者唤醒等待条件的线程。

条件变量函数

       #include <pthread.h>
 
       //条件变量的销毁
       int pthread_cond_destroy(pthread_cond_t *cond);
       //条件变量的初识化
       int pthread_cond_init(pthread_cond_t *restrict cond,
              const pthread_condattr_t *restrict attr);
       pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

函数执行成功返回0,否则返回错误码。

是不是跟锁有亿点点像。

cond:条件变量id。

attr:条件变量属性,一般为默认NULL。

条件变量的操作函数

       #include <pthread.h>
 
       //唤醒该条件变量的所有线程
       int pthread_cond_broadcast(pthread_cond_t *cond);
       //唤醒该条件变量中的一个线程
       int pthread_cond_signal(pthread_cond_t *cond);
 
       int pthread_cond_timedwait(pthread_cond_t *restrict cond,
              pthread_mutex_t *restrict mutex,
              const struct timespec *restrict abstime);
       //让该线程在某个条件变量进行等待
       int pthread_cond_wait(pthread_cond_t *restrict cond,
              pthread_mutex_t *restrict mutex);
 

cond:条件变量id。

mutex:锁id。

abstime:唤醒时间(绝对)。

pthead_cond_wait函数进行三个步骤:

  1. 释放互斥量(为什么会释放互斥量,下面生产者和消费者模型中的代码会讲)。
  2. 阻塞等待
  3. 当被唤醒时,重新获得互斥量并返回。

3.生产与消费者模型

生产者消费者模型是通过一个容器来解决生产者和消费者的强耦合问题,生产者和消费者彼此不通信,生产者不需要等待消费者是否消费者处理,生产者直接往阻塞队列中生产数据,消费者不找生产者要数据,直接从阻塞队列中拿数据并处理数据,阻塞队列是一个数据缓冲区,这个阻塞队列将消费数据和生产数据进行了解耦。

生产者:生产数据的线程或进程

消费者:消费数据的进程或线程

生产者消费者模型优点

  • 生产数据和消费数据进行了解耦,生产者和消费者之间的相互影响较小。
  • 你生产你的,我消费我的,互不干扰。且效率高

  • 支持并发,生产者和消费者可以并发执行。
  • 支持忙闲不均,如果生产者生产数据快,消费者消费数据慢,那么可以创建较多的消费者去消费数据,使消费数据的速度去匹配生产数据的速度。

生产者消费者的321原则

  • 3种关系:生产者与生产者(互斥关系),消费者与消费者(互斥关系),生产者与消费者(互斥和同步的关系)
  • 2种角色:生产者和消费者(指特定的线程和进程)
  • 1个交易场所:有限的空间的缓冲区。

生产者消费者问题

为了保证线程在访问缓冲区出现数据混乱,所以每次只允许一个线程进入缓冲区,即线程与线程之间具有互斥关系,同步关系是生产者和消费者进入缓冲区前需要有条件判断(用条件变量来维持),例如:

当缓冲区为空时,消费者不能进入缓冲区消费数据。
当缓冲区为满时,生产者不能进入缓冲区生产数据。
任何时刻只能有一个线程能进入缓冲区。

 

BlockingQueue的生产者消费者模型

在多线程中,阻塞队列(BlockingQueue)是一种常见的生产者消费者模型,每次只能有一个线程进入阻塞队列中生产或消费数据,当阻塞队列为空时,消费者则挂起等待,当有一定的数据时,消费者才能进入阻塞队列中消费数据,当阻塞队列为满时,则生产者挂起等待,当有一定空间时,生产者才能进入阻塞队列中生产数据。

实现一个阻塞队列,生产者往队列生产任务,该任务包含数据和运算符,消费者从阻塞队列中取出任务,并根据运算符类型对数据进行运算,将结果打印出来。

在实现BlockiingQueue时,我们需要定义3个变量。

  • 互斥量:用于访问缓冲区,一次只能有一个线程进入缓冲区
  • full条件变量:当缓冲区满时,用于阻塞生产者。
  • empty条件变量:当缓冲区为空是,用户阻塞消费者。

BlockingQueue代码实现:

#include <iostream>
#include <queue>
#include <pthread.h>
#include <ctime>
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
using namespace std;
int NUM = 32;
template <class T>
class BlockQueue
{
private:
    queue<T> q;           // 阻塞队列
    int cap = NUM;        // 允许缓冲队列中存储的最多数据
    pthread_mutex_t lock; // 互斥量
    pthread_cond_t full;  // 队列满的条件变量
    pthread_cond_t empty; // 队列空的条件变量
    bool Full()
    {
        return q.size() == cap;
    }
    bool Empty()
    {
        return q.empty();
    }

public:
    BlockQueue()
    {
        pthread_mutex_init(&lock, NULL);
        pthread_cond_init(&full, NULL);
        pthread_cond_init(&empty, NULL);
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&lock);
        pthread_cond_destroy(&full);
        pthread_cond_destroy(&empty);
    }
    void push(const T &data) // 生产者接口
    {
        pthread_mutex_lock(&lock);//限制只能一个线程进入缓冲队列
        while(Full())
        {
            pthread_cond_wait(&full,&lock);//缓冲队列为满,则生产者到_full的条件变量挂起等待
        }
        q.push(data);
        pthread_mutex_unlock(&lock);
        if(q.size()>16)//当缓冲队列有16个空间时,唤醒消费者
        {
            pthead_cond_signal(&empty);
        }
    }
    void pop(T&data)//消费者接口
    {
        pthread_mutex_lock(&lock);//限制只能有一个线程进入缓冲队列
        while(Empty())
        {
            pthread_cond_wait(&empty,&lock);//当缓冲队列为空时,消费者到_empty中挂起等待
        }
        data=q.front();
        q.pop();
        pthread_mutex_unlock();//取出队列中的数据
        pthread_cond_signal(&full);//删除取出的数据
    }
};

细节详解:

在使用pthread_cond_wait函数时,我们都需要传入一个互斥量,因为条件变量总是在互斥量之后使用,当我们让某个线程在条件变量中阻塞等待,我们需要释放互斥量,才能使其他线程能够进入阻塞队列中,如果没有释放互斥量,将会造成死锁的现象。 

 五.线程池

线程池的应用场景 
1,需要大量线程来完成任务,且完成任务的时间较短,WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,但对于长时间的任务,比如一个Telnet连接请求,线程池的优点不明显,因为Telnet会话时间比线程的创建时间太多了。

2.对性能苛刻要求的的应用,比如服务器迅速响应客户请求。

3.接受突发的大量请求时,但不至于使服务器创建大量的线程,在没有线程池情况下,将产生大量的线程,短时间内会产生大量的线程可能使内存达到极限,出现错误。

线程池的实现

线程池的成员变量:

  • 任务队列,quueu<T> q;
  • 线程个数,int num;
  • 互斥量,pthread_mutex_t lock;
  • 条件变量,pthread_cond_t cond;

创建线程池,需要创建一个任务队列和一定数量的线程。由于每次只能有一个线程进入队列中放任务和拿任务,所以此时需要定义一个互斥量进行维护,如果任务队列中的没有任务,此时线程就不能进入任务队列中,所以就创建一个条件变量。

#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
#include <unistd.h>
using namespace std;
#define NUM 2 // 线程池大小
template <class T>
class ThreadPool
{
private:
    queue<T> q;           // 任务队列
    int thread_num;       // 线程池的线程数量
    pthread_mutex_t lock; // 互斥锁
    pthread_con_t cond;   // 条件变量,唤醒线程
public:
    ThreadPool(int num = NUM)
        : thread_num(num);
    {
        pthread_mutex_init(&lock, NULL);
        pthread_con_init(&cond, NULL);
    }
    bool Empty() // 判断队列是否为空
    {
        return q.size() == 0 ? true : false;
    }
    static void *Routine(void *arg) // 线程执行流
    {
        pthread_detach(pthread_self());
        ThreadPool *p = (ThreadPool *)arg;
        while (1)
        {
            p->LockQueue();
            while (p->Empty())
            {
                p->Wait(); // 为空就睡觉
            }
            T data;
            p->Pop(data);
            p->UnLockQueue();
            cout << pthread_self() << "# ";
            // 处理任务
            data.Run();
            // 任务仿函数
            sleep(1);
        }
    }
    void ThreadPoolInit()
    {
        pthread_t tid;
        for (int i = 0; i < thread_num; i++)
        {
            pthread_create(&tid, NULL, Routine, (void *)this);
        }
    }
    void Wait() // 睡觉
    {
        pthread_cond_wait(&cond, &lock);
    }
    void LockQueue()
    {
        pthread_mutex_lock(&lock);
    }
    void UnLockQueue()
    {
        pthread_mutex_unlock(&lock);
    }
    void Push(const T &in)
    {
        LockQueue();
        q.push(in);
        UnLockQueue();
        SignalThread();
    }
    void SignalThread() // 唤醒
    {
        pthread_cond_signal(&cond);
    }
    void Pop(T &out) // 取出任务再删除
    {
        out = q.front();
        q.pop();
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&lock);
        pthread_cond_destroy(&cond);
    }
};

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值