线程ID与互斥

在这里插入图片描述

线程ID

给用户提供的线程ID不是内核中的lwp,而是自己维护的一个唯一值(pthread库),库内部也要承担对线程的管理。

在这里插入图片描述

将tid通过十六进制打印出来,发现tid实际上是一个地址:

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

using namespace std;

string ToHex(pthread_t tid)
{
    char id[128];
    snprintf(id,sizeof(id),"0x%lx",tid);
    return id;
}

void* threadrun(void *args)
{
    string name=static_cast<const char*>(args);
    while(true)
    {
        string tid=ToHex(pthread_self());
        cout<<name<<" is running,tid: "<<tid<<endl;
        sleep(1);
    }
}

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

    cout<<"new thread tid: "<<ToHex(tid)<<endl;
    pthread_join(tid,nullptr);
    
    return 0;
}

在这里插入图片描述

理解库

动态库在没有被加载之前,处于计算机磁盘上,此时磁盘中就有一个库文件。有一个可执行文件mythread,也处于磁盘中。

在这里插入图片描述
可执行文件mythread需要加载到内存中,有自己的代码和数据,对应的页表、PCB都会被创建出来。对应的进程会被CPU调度,执行程序内部的代码,调用库函数,比如调用pthread_create创建一个新的线程。多线程在启动之前必须先是一个进程,然后动态的创建新线程。

创建线程,前提是把库加载到内存,然后映射到进程的地址空间(堆栈之间的共享区)。此时pthread_create就能调用库里面的方法,创建一个新的线程。

理解库对线程的管理

库对线程做管理,跟操作系统对进程进行管理是一个道理,先描述,再组织。
为了更好的管理线程,在动态库里,每当创建一个新线程,就会创建一个内存块,包含struct pthread(线程在用户级的最基本属性)、线程局部存储、线程栈。整个内存块包含了用户级所需要的线程所有属性,这个内存块相当于一个大的结构体。

在这里插入图片描述

这里的先描述,实际上是在库中创建描述线程的相关结构体字段属性,再组织可以理解为一个“数组”。
用户想要找到线程的所有属性只要找到线程控制块的地址即可,也就是说,pthread_t就是一个(线程控制块的)地址。

用户层面是对库进行操作,实际上操作系统内部有一个lwp,它们是1:1的。

总结:Linux线程=pthread库中线程的属性集+LWP

线程局部存储

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

using namespace std;

int gval = 100;

string ToHex(pthread_t tid)
{
    char id[128];
    snprintf(id, sizeof(id), "0x%lx", tid);
    return id;
}

void *threadrun(void *args)
{
    string name = static_cast<const char *>(args);
    while (true)
    {
        string id = ToHex(pthread_self());
        cout << name << " is running,tid: " << id << ",gval: " << gval << ",&gval: " << &gval << endl;
        sleep(1);
        gval++;
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadrun, (void *)"thread-1");
    while (true)
    {
        cout << "main thread,gval: " << gval << ",&gval: " << &gval << endl;
        sleep(1);
    }
    pthread_join(tid, nullptr);

    return 0;
}

gval是一个全局变量,新线程中每次循环gval++,主线程中并没有。
但是根据运行结果,主线程中的gval也随之发生变化,这种现象称之为一个全局变量会被所有线程所共享,因为两个现成的地址是一样的。
在这里插入图片描述

如何避免共享gval变量,让两个线程各自有一个gval变量?
在C/C++中,有一个编译选项__thread,可完成分离,用__thread修饰一下gval,其中__thread只能修饰自置类型。

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

using namespace std;

__thread int gval = 100;

string ToHex(pthread_t tid)
{
    char id[128];
    snprintf(id, sizeof(id), "0x%lx", tid);
    return id;
}

void *threadrun(void *args)
{
    string name = static_cast<const char *>(args);
    while (true)
    {
        string id = ToHex(pthread_self());
        cout << name << " is running,tid: " << id << ",gval: " << gval << ",&gval: " << &gval << endl;
        sleep(1);
        gval++;
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadrun, (void *)"thread-1");
    while (true)
    {
        cout << "main thread,gval: " << gval << ",&gval: " << &gval << endl;
        sleep(1);
    }
    pthread_join(tid, nullptr);

    return 0;
}

在这里插入图片描述

为什么使用__thread修饰后,明明定义的都是同一个变量,为什么地址会不一样?

gval在编译的时候,在其中一个线程的局部存储里有一个gval,在另一个线程的局部存储里也有一个gval,线程在使用的时候是各自的gval,这种现象称之为线程的局部存储。这种做法只在Linux下有效。

线程的简单封装

//Thread.hpp
#pragma once
#include<pthread.h>
#include<iostream>
#include<string>

namespace gwj
{
    //线程执行的方法
    typedef void(*func_t)(const std::string &name);


    class Thread
    {
    public:
        void Excute()
        {
            _isrunning=true;
            _func(_name);
        }
    public:
        Thread(const std::string &name,func_t func):_name(name),_func(func)
        {
        }
        //void *ThreadRoutine(void* args)实际上参数里面还有一个Thread* this
        static void *ThreadRoutine(void* args)   //加上static后,参数里面就没有Thread* this
        {
            Thread *self=static_cast<Thread*>(args); //获得当前对象
            self->Excute();
            return nullptr;
        }
        
        bool Start()
        {
            int n=::pthread_create(&_tid,nullptr,ThreadRoutine,this);
            if(n!=0) return false;
            return true;
        }

        std::string Status()
        {
            if(_isrunning) return "running";
            else return "sleep";
        }

        void Stop()
        {
            if(_isrunning)
            {
                pthread_cancel(_tid);
                _isrunning=false;
            }
        }

        void Join()
        {
            if(!_isrunning)
            {
                pthread_join(_tid,nullptr);
            }
        }
        std::string Name()
        {
            return _name;
        }

        ~Thread()
        {
            Stop();
        }

    private:
        std::string _name;
        pthread_t _tid;
        bool _isrunning;
        func_t _func; //线程执行的回调函数

    };
}
//Main.cc

#include<iostream>
#include<unistd.h>
#include"Thread.hpp"

using namespace gwj;

void Print(const std::string &name)
{
    int cnt=1;
    while(true)
    {
        std::cout<<name<<" is running,cnt: "<<cnt++<<std::endl;
        sleep(1);
        
    }
}

int main()
{
    Thread t("thread-1",Print);

    t.Start();
    std::cout<<t.Name()<<t.Status()<<std::endl;
    sleep(10);
    std::cout<<t.Name()<<t.Status()<<std::endl;

    t.Stop();
    sleep(1);
    std::cout<<t.Name()<<t.Status()<<std::endl;

    t.Join();
    std::cout<<"join done"<<std::endl;

    return 0;
}

在这里插入图片描述

线程的互斥

多个线程能看到的资源称之为共享资源,需要对这些共享资源进行保护,最有效的方法就是互斥。

临界资源:多线程执行流共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

抢票代码问题分析

为了更加深刻理解多线程访问的问题,这里写一个抢票的代码。

#include<iostream>
#include<vector>
#include<unistd.h>
#include<cstdio>
#include"Thread.hpp"

using namespace gwj;

int tickets=10000;

void route(const std::string &name)
{
    while(true)
    {
        if(tickets>0)
        {
            //抢票
            usleep(1000); //抢票花费的时间:1ms
            printf("who: %s,get a ticket:%d\n",name.c_str(),tickets);
            tickets--;
        }
        else
        {
            break;
        }
    }
}

int main()
{
    Thread t1("thread-1",route);
    Thread t2("thread-2",route);
    Thread t3("thread-3",route);
    Thread t4("thread-4",route);

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

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

}

在这里插入图片描述

代码实现的是当tickets>0时完成抢票过程,但是运行后发现,有抢到票数为负数的情况。这就是多线程并发发生的错误以及异常问题。

为什么票会抢到负数?
if(tickets>0)是一个判断过程,也就是一个计算过程,是由CPU完成。首先将数据移动到寄存器内部,还有一个常数0放在另一个寄存器中,只需要将这两个寄存器的值进行判断即可,得到对应的结果:大于0为真,小于0为假。

在这里插入图片描述

CPU中只有一套寄存器,但是数据可以有很多套,这个数据属于线程私有,看起来是放在一个公共区域,但是本质上是线程私有,当线程被切换的时候,数据是会被带走的。

现在有线程A和线程B,当线程A执行抢票过程时,此时tickets为1,正要进行逻辑运算时,此时线程A被切出去了,不能执行下面的代码,但是自己的上下文数据会被保存,也就是寄存器中的值为1,执行到printf时刚好被切走。此时线程B来了,线程B读到寄存器中的值,为1,进行逻辑运算,为真,继续执行,将票数减一,此时票数变成0,将0返回到内存中,然后线程B被切出去了。线程A被切回来了,由于A线程刚刚将票数为1存到寄存器中,认为还有票,继续执行逻辑运算,执行抢票过程,但是在打印抢票数据的时候,会重新在内存中读取票数,此时内存中的票数已经被线程B修改成0,那么线程A打印出来的票数就是0,接下来线程A对票数进行减减操作,还是先去内存中读取票数,然后完成减减操作,变成-1,然后将-1写入到内存中。
tickets--语句实际上是三条语句:重读数据、减数据、写回数据。

认识锁以及接口

为了解决上述临界资源问题,引入互斥量概念,所谓的对临界资源的保护,本质上是对临界区代码进行保护。在内存中开辟的各种资源以及外设都称之为资源,用户访问所有资源都是通过代码访问所有资源的。保护资源本质就是想办法把访问资源的代码保护起来。

初始化互斥量

函数接口:

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

pthread_mutex_init是一个互斥类型,任何时刻只允许一个线程进行资源访问
attr是锁的属性,一般设置成nullptr
mutex:要初始化的互斥量

如果锁是全局的或者静态分配的:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

销毁互斥量

#include <pthread.h>

int pthread_mutex_destroy(pthread_mutex_t *mutex);

使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁(锁是全局的或者静态的可以不需要销毁)
不要销毁一个已经加锁的互斥量
已经销毁的互斥量,要确保后面不会有线程再尝试加锁

加锁、解锁

加锁:

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);

参数就是所初始化的锁


解锁:

#include <pthread.h>
 
int pthread_mutex_unlock(pthread_mutex_t *mutex);

在这里插入图片描述

加锁的范围一定要尽量小

优化抢票代码

  • 加锁的范围一定要尽量小
  • 任何线程要进行抢票都得先申请,不应该有例外
  • 所有线程申请锁,前提是所以线程看到这个锁,因此锁本身也是共享资源,加锁的过程必须是原子的
  • 原子性:要不不做,要做做完,没有中间状态,这种类型叫做原子性。
  • 如果申请锁失败了,线程会被阻塞
  • 如果申请成功了,继续向后运行
  • 如果一个线程申请锁成功了,就可以执行临界区的代码,执行临界区代码期间,线程可以被切走,只不过是拦住其他线程。虽然线程被切走了,但是没有释放锁。

对于其他线程,要么没有申请锁,要么释放锁,对其他线程才有意义。也就是说,访问临界区,对其他线程是原子的

代码1

在这里插入图片描述

//Main.cc

#include<iostream>
#include<vector>
#include<unistd.h>
#include<cstdio>
#include"Thread.hpp"

using namespace gwj;

int tickets=10000;
pthread_mutex_t gmutex=PTHREAD_MUTEX_INITIALIZER;

void route(const std::string &name)
{
    while(true)
    {
        pthread_mutex_lock(&gmutex);
        if(tickets>0)
        {
            //抢票
            usleep(1000); //抢票花费的时间:1ms
            printf("who: %s,get a ticket:%d\n",name.c_str(),tickets);
            tickets--;
            pthread_mutex_unlock(&gmutex);  
        }
        else
        {
            break;
        }
    }
}

int main()
{
    Thread t1("thread-1",route);
    Thread t2("thread-2",route);
    Thread t3("thread-3",route);
    Thread t4("thread-4",route);

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

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

}

运行结果:
在这里插入图片描述

代码2

局部性加锁:

// Thread.hpp
#pragma once
#include <pthread.h>
#include <iostream>
#include <string>

namespace gwj
{
    class ThreadData
    {
    public:
        ThreadData(const std::string &name,pthread_mutex_t *lock):_name(name),_lock(lock)
        {}
    public:
        std::string _name;
        pthread_mutex_t *_lock;
    };
    
    // 线程执行的方法
    typedef void (*func_t)(ThreadData* td);

    class Thread
    {
    public:
        void Excute()
        {
            std::cout << _name << "is running" << std::endl;
            _isrunning = true;
            _func(_td);
            _isrunning = false;
        }

    public:
        Thread(const std::string &name, func_t func,ThreadData* td) : _name(name), _func(func),_td(td)
        {
            std::cout << "create " <<" "<< name << " done" << std::endl;
        }
        // void *ThreadRoutine(void* args)实际上参数里面还有一个Thread* this
        static void *ThreadRoutine(void *args) // 加上static后,参数里面就没有Thread* this
        {
            Thread *self = static_cast<Thread *>(args); // 获得当前对象
            self->Excute();
            return nullptr;
        }

        bool Start()
        {
            int n = ::pthread_create(&_tid, nullptr, ThreadRoutine, this);
            if (n != 0)
                return false;
            return true;
        }

        std::string Status()
        {
            if (_isrunning)
                return "running";
            else
                return "sleep";
        }

        void Stop()
        {
            if (_isrunning)
            {
                pthread_cancel(_tid);
                _isrunning = false;
                std::cout << _name << " Stop" << std::endl;
            }
        }

        void Join()
        {
            pthread_join(_tid, nullptr);
            std::cout << _name << " Join" << std::endl;
        }
        
        std::string Name()
        {
            return _name;
        }

        ~Thread()
        {
            Stop();
        }

    private:
        std::string _name;
        pthread_t _tid;
        bool _isrunning;
        func_t _func; // 线程执行的回调函数
        ThreadData* _td;
    };
}
//Main.cc

#include<iostream>
#include<vector>
#include<unistd.h>
#include<cstdio>
#include"Thread.hpp"

using namespace gwj;

int tickets=10000;
pthread_mutex_t gmutex=PTHREAD_MUTEX_INITIALIZER;

void route(ThreadData* td)
{
    while(true)
    {
        pthread_mutex_lock(td->_lock);
        if(tickets>0)
        {
            //抢票
            usleep(1000); //抢票花费的时间:1ms
            printf("who: %s,get a ticket:%d\n",td->_name.c_str(),tickets);
            tickets--;
            pthread_mutex_unlock(td->_lock);  
        }
        else
        {
            pthread_mutex_unlock(td->_lock);  
            break;
        }
    }
}

// void route(ThreadData* td)
// {
//     std::cout<<td->_name<<": "<<"mutex address: "<<td->_lock<<std::endl;    
// }

static int threadnum=4;

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

    std::vector<Thread> threads;
    for(int i=0;i<threadnum;i++)
    {
        std::string name="thread-"+std::to_string(i+1);
        ThreadData *td=new ThreadData(name,&mutex);
        threads.emplace_back(name,route,td);
    }

    for(auto &thread:threads)
    {
        thread.Start();
    }

        for(auto &thread:threads)
    {
        thread.Join();
    }

    pthread_mutex_destroy(&mutex);

    return 0;
}

在这里插入图片描述

封装锁

//LockGuard.hpp
#pragma once

#include<pthread.h>

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
    {
        pthread_mutex_lock(_mutex);

    }

    ~LockGuard()
    {
        pthread_mutex_unlock(_mutex);
    }

private:
    pthread_mutex_t *_mutex;
};
#include<iostream>
#include<vector>
#include<unistd.h>
#include<cstdio>
#include"Thread.hpp"
#include"LockGuard.hpp"

using namespace gwj;

int tickets=10000;
pthread_mutex_t gmutex=PTHREAD_MUTEX_INITIALIZER;

void route(ThreadData* td)
{
    while(true)
    {
        LockGuard lockguard(td->_lock);   //调用构造函数,自动加锁,当循环结束,临时对象被释放,调用析构函数
        if(tickets>0)
        {
            //抢票
            usleep(1000); //抢票花费的时间:1ms
            printf("who: %s,get a ticket:%d\n",td->_name.c_str(),tickets);
            tickets--;

        }
        else
        {
            break;
        }
    }
}

static int threadnum=4;

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

    std::vector<Thread> threads;
    for(int i=0;i<threadnum;i++)
    {
        std::string name="thread-"+std::to_string(i+1);
        ThreadData *td=new ThreadData(name,&mutex);
        threads.emplace_back(name,route,td);
    }

    for(auto &thread:threads)
    {
        thread.Start();
    }

        for(auto &thread:threads)
    {
        thread.Join();
    }

    pthread_mutex_destroy(&mutex);

    return 0;
}

这种锁的风格称之为RAII风格的锁

锁的原理

申请锁成功,允许进入临界区:申请成功,pthread_mutex_lock()函数会返回,然后执行pthread_mutex_unlock()
申请锁失败,不允许进入临界区:申请失败,pthread_mutex_lock()函数不返回,线程就阻塞了,在pthread_mutex_lock()内部被重新唤醒,重新申请锁。

现实角度理解

单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题
为了实现互斥锁操作,大多数体系结构都提供了swapexchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

在这里插入图片描述

cpu中的寄存器只有一套,被所有的线程共享,但是寄存器里面的数据属于执行流的上下文吗,是执行流私有的数据。
CPU在执行的时候,一定要有对应的载体(线程、进程)。
数据在内存中,是被线程共享的。
结论:把数据从内存移动到CPU寄存器中,本质是把数据从共享变成线程私有。

在这里插入图片描述

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值