【Linux】多线程(一)

线程的概念

  • 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
  • 一切进程至少都有一个执行线程
  • 线程在进程内部运行,本质是在进程地址空间内运行
  • 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
  • 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流

教材观点:
线程是一个执行分支,执行粒度比进程更细,调度成本更低。
线程是进程内部的一个执行流
内核观点:
线程是cpu调度的基本单位,进程是分配资源的基本实体。

在这里插入图片描述

Linux内核设计者,复用了pcb的结构体,用进程的pcb模拟线程的tcb很好的复用了pcb的设计方案。Linux并没有正真意义上的线程,而是用进程的方案模拟线程。
复用代码和结构———简单,好维护,效率更高,更安全———Linux可以不简单的运行
os最频繁的功能除了本身就是进程了。

而windows的设计就是TCB(线程控制块)进程调度里面还有线程调度。
为什么说线程是轻量级进程?是因为在切换线程的时候除了虚拟地址表,页表等不用更换,cache也是不应更换的,它就是我们在调度进程时所加载的周边数据,(预加载附近的数据,局部性原理)缓冲区。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里简单的解释一下上图的页目录为什么是1024,首先虚拟地址空间是共有2^32个地址,从全零到全一。
而这里是取它的前十个比特位,所以就是这十个比特位从全零到全一。后面的页表项也是如此。

当我们在malloc申请内存时,是先进行虚拟内存的申请,然后在真正需要访问时,缺页中断,os才会自动的填充页表和申请物理内存。

线程优点

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

在这里插入图片描述

线程缺点

  • 性能损失
  • 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型
    线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的
    同步和调度开销,而可用的资源不变。
  • 健壮性降低

*编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了
不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。

  • 缺乏访问控制
  • 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
  • 编程难度提高
  • 编写与调试一个多线程程序比单线程程序困难得多
#include <iostream>
#include <pthread.h>
#include <unistd.h>

using namespace std;

int g_val = 0; // 全局变量,在多线程场景中,我们多个线程看到的是同一个变量! -- 为什么?

void *threadRun1(void *args)
{
    while (true)
    {
        sleep(1);
        cout << "t1 thread..." << getpid() << " &g_val: " << &g_val << " , g_val: " << g_val << endl;
    }
}

void *threadRun2(void *args)
{
    // char *s = "hello bit";
    while (true)
    {
        sleep(1);
        cout << "t2 thread..." << getpid()  << " &g_val: " << &g_val << " , g_val: " << g_val++ << endl;
        // *s = 'H'; // 让这一个线程崩溃
    }
}

int main()
{
    pthread_t t1, t2, t3;

    pthread_create(&t1, nullptr, threadRun1, nullptr);
    pthread_create(&t1, nullptr, threadRun2, nullptr);

    while (true)
    {
        sleep(1);
        cout << "main thread..." << getpid()  << " &g_val: " << &g_val << " , g_val: " << g_val << endl;
    }
}

在多线程当中只要有一个线程崩溃就会导致整个进程崩溃。
线程是可以共享一个资源的因为他只是创建了个自己的pcb其他的都是一样的,而进程就不是这样的。

线程异常

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

线程用途

合理的使用多线程,能提高CPU密集型程序的执行效率
合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是
多线程运行的一种表现)

创建线程
在这里插入图片描述
等待线程,线程也是和进程一样的,也是会出现类似僵尸进程的情况的,所以也是需要等待的。
在这里插入图片描述

#include<iostream>
#include<unistd.h>
#include<pthread.h>

using namespace std;

#define NUM 10

enum{ OK=0, ERROR };

class ThreadData
{
public:
    ThreadData(const string &name, int id, time_t createTime, int top)
    :_name(name), _id(id), _createTime((uint64_t)createTime),_status(OK), _top(top), _result(0)
    {}
    ~ThreadData()
    {}
public:
    // 输入的
    string _name;
    int _id;
    uint64_t _createTime;

    // 返回的
    int _status;
    int _top;
    int _result;
    // char arr[n]
};


// 线程终止
// 1. 线程函数执行完毕,return void*
// 2. pthread_exit(void*)
void *thread_run(void *args)
{
    // char *name = (char*)args; //?
    ThreadData *td = static_cast<ThreadData *>(args);

    for(int i = 1; i <= td->_top; i++)
    {
        td->_result += i;
    }
    cout << td->_name << " cal done!" << endl;
    // pthread_exit(td);

    return td;
    
    // while (true)
    // {
    //     cout << "thread is running, name " << td->_name << " create time: " << td->_createTime << " index: " << td->_id << endl;
    //     // // exit(10); // 进程退出,不是线程退出,只要有任何一个线程调用exit,整个进程(所有线程)全部退出!
    //     // sleep(4);
        
    //     // break;
    // }
    // delete td;

    // pthread_exit((void*)2);   // void *ret = (void*)1;
    // return nullptr;
}

int main()
{
    pthread_t tids[NUM];
    for(int i = 0; i < NUM ;i++)
    {
        char tname[64];
        snprintf(tname, 64, "thread-%d", i+1);
        ThreadData *td = new ThreadData(tname, i+1, time(nullptr), 100+5*i);
        //这里每次都会new一个新的,所以就tname就不会出现覆盖的问题。
        pthread_create(tids+i, nullptr, thread_run, td);
        sleep(1);
    }

    void *ret = nullptr; // int a =  10

    for(int i = 0 ; i< NUM; i++)
    {
        int n = pthread_join(tids[i], &ret);
        if(n != 0) cerr << "pthread_join error" << endl;
        ThreadData *td = static_cast<ThreadData *>(ret);//更安全的类型转换
        if(td->_status == OK)
        {
            cout << td->_name << " 计算的结果是: " << td->_result << " (它要计算的是[1, " << td->_top << "])" <<endl;
        }
        sleep(1);

        delete td;
    }

    cout << "all thread quit..." << endl;
    return 0;
    // while (true)
    // {
    //     cout << "main thread running, new thread id : " << endl;
    //     sleep(1);
    // }
}

在县线程当中不能使用exit,想退出要是使用pthread_exit。pthread_join是用来等待和回收线程的,如果有线程没有退出就一直等待。
pthread_create(tids+i, nullptr, thread_run, td);的最后一个参数是用来传递信息的,可以是类型也可以是结构体。
第一个参数:一个指向 pthread_t 类型的指针,用于保存新线程的标识符,所以他的第一个参数是输出型参数。
最后一个参数是传给第三个参数的。而 pthread_join(tids[i], &ret);他的第二个参数是输出型参数就是函数的返回值由return 或者 pthread_exit进行传递。注意它们之间的类型转换!!!

在这里插入图片描述

上述代码的输出结果
在这里插入图片描述

pthread_cancel线程取消,最终返回-1

在这里插入图片描述

#include<iostream>
#include<unistd.h>
#include<pthread.h>

using namespace std;

//#define NUM 10

void *threadRun(void* args)
{
    const char*name = static_cast<const char *>(args);

    int cnt = 5;
    while(cnt)
    {
        cout << name << " is running: " << cnt-- << " obtain self id: " << pthread_self() << endl;
        sleep(1);
    }

    pthread_exit((void*)11); 

    // PTHREAD_CANCELED; #define PTHREAD_CANCELED ((void *) -1)
    //线程取消返回值是-1,也就是他PTHREAD_CANCELED
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");
    //sleep(3);

    //pthread_cancel(tid);

    void *ret = nullptr;
    pthread_join(tid, &ret);
    cout << " new thread exit : " << (int64_t)ret << "quit thread: " << tid << endl;
    return 0;
}

线程取消的结果
在这里插入图片描述
线程正常进行的结果
在这里插入图片描述

注意在LINUX平台下指针是八个比特位,他是64位机器,所以也就导致为什么上述代码在最后打印ret需要用64位的int了。

获取线程id
在这里插入图片描述

线程分离

在这里插入图片描述

一个分离的线程在其执行结束后会自动被系统回收,无需显式地调用线程的销毁函数或者等待其他线程回收。
分离的线程资源会被系统自动释放,包括线程栈空间和其他由线程分配的资源。
分离的线程不能通过调用pthread_join()或类似的函数来获取线程的终止状态或返回值。
分离的线程不会使进程处于等待状态,它们可以在后台继续执行而不影响其他线程的执行。
线程分离的主要优点是降低了资源泄漏的风险,因为分离的线程在执行结束后会自动释放资源。此外,线程分离还可以提高程序的执行效率,尤其是在需要创建大量临时线程的情况下,可以减少线程管理的开销和内存占用。
总而言之,线程分离是一种对线程进行管理和控制的机制,它可以使线程在后台独立运行,并且在执行结束后自动释放资源。它是多线程编程中的一项重要技术,可以提高程序性能和资源管理的效率。

局部变量和全局变量

#include<iostream>
#include<unistd.h>
#include<pthread.h>

using namespace std;

// __thread int g_val = 100;

int g_val = 100;

std::string hexAddr(pthread_t tid)
{
    g_val++;
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "0x%x", tid);

    return buffer;
}

void *threadRoutine(void* args)
{
    // static int a = 10;
    string name = static_cast<const char*>(args);
    int cnt = 5;
    while(cnt)
    {
        // cout << name << " : " << cnt-- << " : " << hexAddr(pthread_self()) << " &cnt: " << &cnt << endl;
        cout << name << " g_val: " << g_val++ << ", &g_val: " << &g_val << endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t t1, t2, t3;
    pthread_create(&t1, nullptr, threadRoutine, (void*)"thread 1"); // 线程被创建的时候,谁先执行不确定!
    pthread_create(&t2, nullptr, threadRoutine, (void*)"thread 2"); // 线程被创建的时候,谁先执行不确定!
    pthread_create(&t3, nullptr, threadRoutine, (void*)"thread 3"); // 线程被创建的时候,谁先执行不确定!

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);

    return 0;
}

在上述代码当中在函数当中的变量是每一个进程都拥有独立的,而在全局当中的变量就是所以有线程共有的,要是想要全局变量变成每个线程都是私有的,就在变量的前面加上__thread

互斥锁

#include<iostream>
#include<unistd.h>
#include<pthread.h>

using namespace std;

int tickets = 10000; // 加锁保证共享资源的安全
pthread_mutex_t mutex; // 后面说

void *threadRoutine(void *name)
{
    string tname = static_cast<const char*>(name);

    while(true)
    {
        pthread_mutex_lock(&mutex); // 所有线程都要遵守这个规则
        if(tickets > 0) // tickets == 1; a, b, c,d
        {
            //a,b,c,d
            usleep(2000); // 模拟抢票花费的时间
            cout << tname << " get a ticket: " << tickets-- << endl;
            pthread_mutex_unlock(&mutex);
        }
        else
        {
            pthread_mutex_unlock(&mutex);
        }

        // 后面还有动作
        usleep(1000); //充当抢完一张票,后续动作
    }

    return nullptr;
}

int main()
{
    pthread_mutex_init(&mutex, nullptr);

    pthread_t t[4];
    int n = sizeof(t)/sizeof(t[0]);
    for(int i = 0; i < n; i++)
    {
        char *data = new char[64];
        snprintf(data, 64, "thread-%d", i+1);
        pthread_create(t+i, nullptr, threadRoutine, data);
    }

    for(int i = 0; i < n; i++)
    {
        pthread_join(t[i], nullptr);
    }

    pthread_mutex_destroy(&mutex);
    return 0;
}

当我们不使用锁时,会出现线程并发的问题,就以上述代码为例在在没有锁的情况下,最后的共享资源并不会像理想的样子,一直减到一,他会确界。而加上锁了之后就不会出现该问题,之后就是串行的。锁没有创建成功的话就会一直等待。如果发现一直是同一个线程就行处理的话,是因为没有后续的任务导致该线程一直抢,所以就一直是他。

// 细节:
// 1. 凡是访问同一个临界资源的线程,都要进行加锁保护,而且必须加同一把锁,这个是一个游戏规则,不能有例外
// 2. 每一个线程访问临界区之前,得加锁,加锁本质是给 临界区 加锁,加锁的粒度尽量要细一些
// 3. 线程访问临界区的时候,需要先加锁->所有线程都必须要先看到同一把锁->锁本身就是公共资源->锁如何保证自己的安全?-> 加锁和解锁本身就是原子的!
// 4. 临界区可以是一行代码,可以是一批代码,a. 线程可能被切换吗?当然可能, 不要特殊化加锁和解锁,还有临界区代码。
// b. 切换会有影响吗?不会,因为在我不在期间,任何人都没有办法进入临界区,因为他无法成功的申请到锁!因为锁被我拿走了!
// 5. 这也正是体现互斥带来的串行化的表现,站在其他线程的角度,对其他线程有意义的状态就是:锁被我申请(持有锁),锁被我释放了(不持有锁), 原子性就体现在这里
// 6. 解锁的过程也被设计成为原子的!
// 7. 锁 的 原理的理解

对锁的封装代码

mythread.cc

#include<iostream>
#include<unistd.h>
#include"thread.hpp"
#include"lockguard.hpp"

using namespace std;

// // 临界资源
int tickets = 1000;                                // 全局变量,共享对象
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 这是我在外边定义的锁


void threadRoutine(void *args)
{
    std::string message = static_cast<const char *>(args);
    while (true)
    {
        // pthread_mutex_lock(&mutex); // 加锁,是一个让不让你通过的策略
        //通过使用空括号来确定锁的范围
        {
            LockGuard lockguard(&mutex); // RAII 风格的锁
            if (tickets > 0)
            {
                usleep(2000);
                cout << message << " get a ticket: " << tickets-- << endl; // 临界区
            }
            else
            {
                break;
            }
        }

        // 我们抢完一张票的时候,我们还要有后续的动作
        // usleep(13);
    }
}

int main()
{
    Thread t1(1, threadRoutine, (void *)"hellobit1");
    Thread t2(2, threadRoutine, (void *)"hellobit2");
    Thread t3(3, threadRoutine, (void *)"hellobit3");
    Thread t4(4, threadRoutine, (void *)"hellobit4");

    t1.run();
    t2.run();
    t3.run();
    t4.run();

    t1.join();
    t2.join();
    t3.join();
    t4.join();
    return 0;
}

thread.hpp

#include<iostream>
#include<pthread.h>

using namespace std;

class Thread
{
public:
    typedef void(*func_t)(void*);
    typedef enum
    {
        NEW = 0,
        RUNNING,
        EXITED
    } ThreadStatus;
public:
    Thread(int num,func_t func,void *args)
    :_tid(0),_func(func),_args(args),_status(NEW)
    {
        char name[64];
        snprintf(name,64,"thread-%d",num);
        _name=name;
    }
    int status() { return _status; }
    string threadname() { return _name; }
    pthread_t threadid()
    {
        if (_status == RUNNING)
            return _tid;
        else
        {
            return 0;
        }
    }
    // 是不是类的成员函数,而类的成员函数,具有默认参数this,需要static
    // 但是会有新的问题:static成员函数,无法直接访问类属性和其他成员函数
    static void *runHelper(void *args)
    {
        Thread *ts = (Thread*)args; // 就拿到了当前对象
        // _func(_args);
        (*ts)();
        return nullptr;
    }
    void operator ()() //仿函数
    {
        if(_func != nullptr) _func(_args);
    }
     void run()
    {
        int n = pthread_create(&_tid, nullptr, runHelper, this);
        if(n != 0) exit(1);
        _status = RUNNING;
    }
    void join()
    {
        int n = pthread_join(_tid, nullptr);//runHelper没有返回值
        if( n != 0)
        {
            std::cerr << "main thread join thread " << _name << " error" << std::endl;
            return;
        }
        _status = EXITED;
    }
    ~Thread()
    {
    }
private:
    pthread_t _tid;
     string _name;
    func_t _func; // 线程未来要执行的回调
    void *_args;
    ThreadStatus _status;

};

locguard.hpp

#pragma once

#include <iostream>
#include <pthread.h>

class Mutex // 自己不维护锁,有外部传入
{
public:
    Mutex(pthread_mutex_t *mutex):_pmutex(mutex)
    {}
    void lock()
    {
        pthread_mutex_lock(_pmutex);
    }
    void unlock()
    {
        pthread_mutex_unlock(_pmutex);
    }
    ~Mutex()
    {}
private:
    pthread_mutex_t *_pmutex;
};

class LockGuard // 自己不维护锁,有外部传入
{
public:
    LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
    {
        _mutex.lock();
    }
    ~LockGuard()
    {
        _mutex.unlock();
    }
private:
    Mutex _mutex;
};

互斥量的接口

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

方法1,静态分配

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

方法2,动态分配:

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

销毁互斥量
销毁互斥量需要注意

  • 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex)

互斥量加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号

调用 pthread_ lock 时,可能会遇到以下情况:

  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
  • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,
    那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

互斥量实现原理探究

  • 经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题
  • 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单
    元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一
    个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪
    代码改一下

在这里插入图片描述

可重入VS线程安全

概念

线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,
并且没有锁保护的情况下,会出现该问题。
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们
称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重
入函数,否则,是不可重入函数。

常见的线程不安全的情况

不保护共享变量的函数
函数状态随着被调用,状态发生变化的函数
返回指向静态变量指针的函数
调用线程不安全函数的函数

常见的线程安全的情况

每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
类或者接口对于线程来说都是原子操作
多个线程之间的切换不会导致该接口的执行结果存在二义性

常见不可重入的情况

调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构可重入函数体内使用了静态的数据结构

常见可重入的情况

不使用全局变量或静态变量
不使用用malloc或者new开辟出的空间
不调用不可重入函数
不返回静态或全局数据,所有数据都有函数的调用者提供
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

可重入与线程安全联系

函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的

可重入与线程安全区别

可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

常见锁概念

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

死锁四个必要条件

互斥条件:一个资源每次只能被一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

避免死锁

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

避免死锁算法

死锁检测算法
银行家算法

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值