多线程详解

目录

1.线程的概念

2.进程和线程的区别

3.线程操作

4.线程互斥

5.可重入和线程安全

6.常见锁的概念

7.线程同步


1.线程的概念(lwp)

1.1 线程概念

(1) 线程是cpu调度的基本单位(轻量化的进程), 和进程不同的是进程是承担系统资源的基本实体.

(2) 一个进程里面至少有一个线程.

(3) 线程是在进程的地址空间里面运行的.

1.2 线程的优点 (和进程比较)

(1) 创建线程的代价要小;

(2) 线程的切换对于操作系统来说工作量更少;(因为寄存器少,不需要重新更新cache)

(3) 线程占用得资源要小;

(4) 能充分利用多处理器的并行数量;

(5) 等待i/o操作结束的时候,还可以执行其他任务;

1.3线程的缺点:

(1) 性能降低;

(2) 健壮性降低;

(3) 缺乏访问限定;

(4) 代码编写难度大;

1.3 地址空间

文件i/o的基本单位大小是4KB; 因为一共是32位数据,那么一共可以存下2^32byte数据, 一个页框是4kB大小就是2^12个byte; 那么算下来就是可以分1048576个页框. 那么页表如何能够存下页框这么多的信息的呢? 首先将前10位 的数据存放到页目录里面, 根据这10位数据以及和接着的10位数据 找到在页表的位置找到页框的地址, 最后12位数据用来和页框初始地址找到最后的具体位置.

注意: 划分页表的本质就是在划分地址空间.

1.4 线程异常

(1) 如果单个线程发生了异常, 那么其他线程以及进程也必定会受到崩溃.因为线程是进程执行的分支, 如果线程出错, 导致进程直接终止, 那么进程的其他执行流的线程也会终止.

2.进程和线程的区别

2.1区别

(1) 进程是系统资源分配的基本单位; 线程是cpu调度的基本单位;

(2) 线程共享进程的数据, 但是也拥有自己的一部分数据;

线程id, errno, 栈, 一组寄存器, 信号屏蔽字, 调度优先级.

(3) 进程的多个线程共享同一个地址空间, 如果定义一个函数, 在每个线程内都可以使用, 全局变量也可以. 线程还共享进程的环境和资源.

3.线程操作

3.1线程的创建

头文件: pthread.h; 连接线程函数库的时候要使用到: -lpthread;

首先第一个参数thread就是线程的id;

attr是线程的属性(一般为nullptr);

start_routine是线程需要执行的函数;

arg是传递给线程执行函数的参数.
注意:

**(1)**创建失败不会返回将全局变量errno返回, 是将错误代码通过返回值返回.

线程也提供了errno变量, 但是通过返回值更加好判断错误信息.

(2) pthread_create创建的 线程id和线程id不是一回事; 前面是由第一个参数指向虚拟内存本质就是内存单元的地址, 后续线程的操作都是根据这个id来使用的, 而后面的是采用了一个数值来唯一表示了id.

cpp 复制代码
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<sys/types.h>
#include<string>
using namespace std;

int gcnt = 100;

void* ThreadRountine(void *args)
{
    
    string threadname = (const char*)args;

    while(true)
    {
        cout << "new thread" << threadname << "pid" << getpid() << "gcnt" << gcnt << endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRountine, (void*)"thread 1");
    
    //传参
    //pthread_t tid1;
    //pthread_create(&tid, nullptr, ThreadRountine, (void*)"thread 1");

    sleep(3);
    //主线程;
    while(true)
    {
        cout << "main thread" << getpid() << "gcnt:" << gcnt << endl;
        sleep(1);
    }
}

3.2 获取线程自身id

直接放到线程里面使用即可.

3.3 线程终止

(1) pthread_exit

参数value_ptr表示线程退出的状态; 一般都是给nullptr.

(2) pthread_cancel

作用: 取消进行的线程;

小tips: 为啥要线程等待?

(1) 已经退出的线程, 空间还没有释放还在进程地址空间中;

(2) 新的线程不会复用刚才退出的线程的地址空间;

(3) 防止出现僵尸进程.

(3) pthread_ join

作用:等待线程的终止; 挂起等待, 直到线程终止.

第一个参数表示线程id, 后面是错误码的返回值;
总结:

(1) 如果线程是通过return 返回的, 那么retval指向线程函数的返回值;

(2) 如果线程是通过被别的线程异常pthread_cancel返回的, 那么retval是指向常量PTHREAD_CANCEL;

(3) 如果线程是通过pthread_exit返回的, 那么就是指向传给pthread_exit的参数;

(4) 线程是被分离是可以被取消的, 但是不能被join.

3.4 线程分离

对目标线程进行分离; 线程的分离和join只能存在一个, 不能两者都同时出现.

线程自己也可以自己分离自己.

pthread_detach(pthread_self());

小tips:

以上的接口都不是系统直接提供的接口, 是原生线程库pthread提供的接口.

如何理解pthread库管理线程?

线程库是共享的, 并且内部要管理整个系统, 多个用户启动所有进程.

pthread_r tid就是线程属性集合的地址!!!

4.线程互斥

4.1 多线程抢票

先用这个实例情况将多线程出现的问题提出, 然后就可以知道互斥!

看看下面的代码, 结果会是什么?

可能会出现0, -1, -2, 的情况. 这个又是为啥?

因为线程并行地访问了同一个临界资源, 并且--操作还不是原子的, 那么势必会造成由于每个线程的时间片不同对临界资源的修改的结果无人可知, 所以就会出现线程同时大量进入临界资源, 最后修改的数据和理想结果不同.

综上: 我们就会要求每次访问临界资源的只能是一个线程, 那么就是要引入互斥啦!

cpp 复制代码
int ticket = 1000;

void* getticket(void* args)
{
    while(true)
    {
        if(ticket > 0)
        {
            cout << "get a ticket!" << ticket << endl;
            ticket--;
        }
        else
        {
            break;
        }
    }
    return nullptr;
}

int main()
{
    pthread_t tid1;
    pthread_create(&tid1, nullptr, getticket, (void*)"thread-1");

    pthread_t tid2;
    pthread_create(&tid2, nullptr, getticket, (void*)"thread-2");

    pthread_t tid3;
    pthread_create(&tid3, nullptr, getticket, (void*)"thread-3");

    cout << "main thread" << endl;

    return 0;
}

4.2线程互斥锁相关接口

动态分配:

(1)锁的创建: pthread_mutex_t mutex

(2)锁的初始化: pthread_mutex_init

(3)锁的销毁: pthread_destroy

(4) 加锁:pthread_mutex_lock

(5) 解锁: pthread_mutex_unlock

返回值:成功返回0,失败返回错误号

注意:

pthread_mutex_lock: 如果互斥量没有上锁就返回成功, 如果多个线程抢夺锁资源,如果没有抢夺到就会自动阻塞挂起, 等待解锁.

静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
不需要销毁!!!
下面使用封装好的线程, 进行互斥(加锁解锁)操作等来验证线程互斥.

cpp 复制代码
//简单封装一个线程创建一系列的操作
#pragma once

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

// 设计方的视角
//typedef std::function<void()> func_t;
template<class T>
using func_t = std::function<void(T)>;

template<class T>
class Thread
{
public:
    Thread(const std::string &threadname, func_t<T> func, T data)
        :_tid(0)
        , _threadname(threadname)
        , _isrunning(false)
        , _func(func)
        , _data(data)
    {}

    static void *ThreadRoutine(void *args) // 类内方法,
    {
        // (void)args; // 仅仅是为了防止编译器有告警
        Thread *ts = static_cast<Thread *>(args);

        ts->_func(ts->_data);

        return nullptr;
    }

    bool Start()
    {
        int n = pthread_create(&_tid, nullptr, ThreadRoutine, this/*?*/);
        if(n == 0) 
        {
            _isrunning = true;
            return true;
        }
        else return false;
    }

    bool Join()
    {
        if(!_isrunning) return true;
        int n = pthread_join(_tid, nullptr);
        if(n == 0)
        {
            _isrunning = false;
            return true;
        }
        return false;
    }

    std::string ThreadName()
    {
        return _threadname;
    }

    bool IsRunning()
    {
        return _isrunning;
    }
    
    ~Thread()
    {}
private:
    pthread_t _tid;
    std::string _threadname;
    bool _isrunning;
    func_t<T> _func;
    T _data;
};
cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <vector>
#include <cstdio>
#include "Thread.hpp"

std::string GetThreadName()
{
    static int number = 1;
    char name[64];
    snprintf(name, sizeof(name), "Thread-%d", number++);
    return name;
}

void Print(int num)
{
    while (num)
    {
        std::cout << "hello world: " << num-- << std::endl;
        sleep(1);
    }
}

int ticket = 10000;
void GetTicket(pthread_mutex_t *mutex)
{
    while (true)
    {
        pthread_mutex_lock(mutex); 
        if (ticket > 0) 
        {
            usleep(1000);
            printf("get a ticket: %d\n", ticket);
            ticket--;
            pthread_mutex_unlock(mutex);
        }
        else
        {
            pthread_mutex_unlock(mutex);
            break;
        }
    }
}


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

    std::string name1 = GetThreadName();
    Thread<pthread_mutex_t *> t1(name1, GetTicket, &mutex);

    std::string name2 = GetThreadName();
    Thread<pthread_mutex_t *> t2(name2, GetTicket, &mutex);

    std::string name3 = GetThreadName();
    Thread<pthread_mutex_t *> t3(name3, GetTicket, &mutex);

    std::string name4 = GetThreadName();
    Thread<pthread_mutex_t *> t4(name4, GetTicket, &mutex);

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

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

    pthread_mutex_destroy(&mutex);
    return 0;
}

4.3 互斥量mutex

(1) 线程使用的绝大部分的变量是局部的, 它们是存放再栈空间, 这种变量是归属与单个线程, 其他线程不拥有.

(2) 也有些变量是共享变量, 全部线程都可以共享使用.

(3) 锁就是互斥量, 锁的作用: 1.当代码进入临界区, 其他线程无法进入该临界区;

2.多个线程要进入临界区, 现如今没有线程进入时候, 只能允许一个线程进入该区域;

3.如果线程不在临界区, 也不能阻止其他线程进入临界区.

小tips: swap和exchange是将寄存器和内存单元的数据进行交换, 并且这个指令是原子的.

5.可重入和线程安全

可重入: 同一个函数被不同的执行流调用, 当前执行流还没执行完, 其他执行流又来进入, 但是最后执行的结果没有任何不同以及问题;

**线程安全:**线程执行同一块代码, 不会出现不一样的结果.

注意: 可重入函数就是线程安全的一种.

5.1常见线程不安全的情况

(1) 不保护共享变量的函数;

(2) 函数随着被调用发生变化;

(3) 调用线程不安全的函数;

(4) 返回指向静态变量指针的函数;

6.常见锁的概念

6.1死锁

一组线程中都占有不会释放的资源, 当线程和另外一个线程互相申请资源的时候都占用不会释放的资源而永久等待的状态.

6.2死锁的四个条件

(1) 互斥条件: 一个资源只被一个执行流使用;

(2) 请求与保持条件: 一个执行流对资源进行申请并且对已有的资源不释放.

(3) 不剥夺条件: 一个已经获得资源的执行流在没有执行完前都不会剥夺;

(4) 循环等待条件: 多个执行流之间形成循环等待资源的关系.

6.3避免死锁的方法

(1) 破坏死锁四个条件;

(2) 加锁顺序一致;

(3) 避免锁未释放的情况;

(4) 资源一次性分配;

7 线程同步

7.1同步

在数据安全的前提, 让线程进行特定的顺序访问临界资源, 避免饥饿问题(线程始终分配不到资源).

互斥可以保证资源利用的安全性, 同步可以保证充分高效的使用资源.

7.2条件变量

条件变量函数:

(1)条件变量的初始化:

int pthread_cond_init: cond参数是需要初始化的条件变量;

(2)条件变量的销毁:

int pthread_cond_destroy

**(3)**等待条件满足:

int pthread_cond_wait: cond是条件变量, mutex是互斥量.

(4) 唤醒等待:

全部唤醒: int pthread_cond_broadcast:

单个唤醒: int pthread_cond_signal:

7.3线程同步实例

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

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int ticket = 1000;

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

    while(true)
    {
        pthread_mutex_lock(&mutex);
        if(ticket > 0)
        {
            cout << name << ", get a ticket:" << ticket-- << endl;
            usleep(1000);
        }
        else
        {
            cout << "没票了," << name << endl;
            pthread_cond_wait(&cond, &mutex);
        }
        pthread_mutex_unlock(&mutex);
    }
}

int main()
{
   pthread_t t1, t2, t3;
   pthread_create(&t1, nullptr, threadRountine,(void*)"thread-1");
   pthread_create(&t2, nullptr, threadRountine,(void*)"thread-2");
   pthread_create(&t3, nullptr, threadRountine,(void*)"thread-3");
   
   sleep(5);
   while(true)
   {
     /* pthread_cond_signal(&cond); */
     /*   pthread_cond_broadcast(&cond); */
         sleep(6);
         pthread_mutex_lock(&mutex);
         ticket += 100;
         pthread_mutex_unlock(&mutex);
         pthread_cond_signal(&cond);
   }

   pthread_join(t1, nullptr);
   pthread_join(t2, nullptr);
   pthread_join(t3, nullptr);
   return 0;
}

注意:

(1)线程在等待的时候会自动释放锁资源,;

(2)当线程在临界区唤醒进行pthread_cond_wait, 要重新申请并且持有锁;

(3)线程唤醒时候重新申请并持有锁就是参与竞争锁资源.

小tips:

为什么pthread_cond_wait需要互斥量?

因为条件等待是线程同步的手段, 必须要改变原来的共享变量, 然后原来的线程也可以满足条件得到资源. 条件变量的改变必然会牵扯到共享的数据, 那么必定也要使用到互斥量进行保护资源.

后言:

线程部分还涉及到生产消费者模型以及基于环形队列的生产消费者模型,以及线程池,线程安全的单例模型.读者写者问题.这些就放到下一博客具体来讲. 喜欢的大家可以三连一下!!!谢谢大家.

相关推荐
白子寰3 分钟前
【C++打怪之路Lv14】- “多态“篇
开发语言·c++
小芒果_018 分钟前
P11229 [CSP-J 2024] 小木棍
c++·算法·信息学奥赛
gkdpjj13 分钟前
C++优选算法十 哈希表
c++·算法·散列表
王俊山IT15 分钟前
C++学习笔记----10、模块、头文件及各种主题(一)---- 模块(5)
开发语言·c++·笔记·学习
为将者,自当识天晓地。17 分钟前
c++多线程
java·开发语言
-Even-18 分钟前
【第六章】分支语句和逻辑运算符
c++·c++ primer plus
小政爱学习!19 分钟前
封装axios、环境变量、api解耦、解决跨域、全局组件注入
开发语言·前端·javascript
AndyFrank28 分钟前
mac crontab 不能使用问题简记
linux·运维·macos
k093334 分钟前
sourceTree回滚版本到某次提交
开发语言·前端·javascript
神奇夜光杯42 分钟前
Python酷库之旅-第三方库Pandas(202)
开发语言·人工智能·python·excel·pandas·标准库及第三方库·学习与成长