Linux线程(二)

分离线程

默认情况下,新创建的线程是可等待的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统内存泄漏。 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。此时需要调用的函数如下:

int pthread_detach(pthread_t thread);

当使用上述函数之后,就不再需要将pthread_join函数进行阻塞等待回收线程资源。

#include <iostream>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <cstring>
#include <cstdio>
#include <memory>
#include "PThread.hpp"
using namespace std;
//int g_val = 100; //全局变量是所有线程都能看见的,就算修改了也都能看见
__thread int g_val=100;   //加上__thread可以将内置类型设置为线性局部存储
//分离线程:让父线程不在等待子线程执行完,就可以回收资源
//把pthread_seif()的线程ID改成16进制的
string changeId(const pthread_t &thread_id)
{
    char name[128];
    snprintf(name,sizeof name,"0x%x",thread_id); 
    return name;
}
void* start_routine(void* args)
{
    string thread_name = static_cast<const char*>(args);  //安全的强制类型转化
    //(pthread_self()); //线程分离,线程分离之后就可以不用join阻塞等待了
    int n=5;
    while(n--)
    {
        cout<<"son_pthread_ID="<<changeId(pthread_self())<<" g_val = "<<(int)g_val<<" &g_val = "<<&g_val<<endl;
        sleep(1);
        g_val++;
    }
    return nullptr;
}

int main()
{
    pthread_t pit;
    int n =  pthread_create(&pit,nullptr,start_routine,(void*)"thread_one");
    // pthread_detach(pit);
    int t=6;
    while(t--)
    {
        cout<<"son thread_ID="<<changeId(pit)<<" main thread_id = "<<changeId(pthread_self())<<" g_val = "<<(int)g_val<<" &g_val = "<<&g_val<<endl;
        sleep(1);
    }

    // //一个线程默认是jionable的,如果设置了分离状态,不能够进行等待了
    //此时如果在使用pthread_join函数进行等待,则会报错Invalid system
    //int flag = pthread_join(pit,nullptr);
    //cout<<"result: "<<flag<<" : "<<strerror(flag)<<endl;
    return 0;
}

注意在进行分离的时候,有时候因为子线程和主线程的执行顺序是根据OS自己决定的,就会导致如果紧接着线程创建的后面进行pthread_join,这时候就会导致分离失败,同样能够正常回收。

线程资源的管理

对于线程的ID查看

 pthread_t pthread_self(void);

可以返回一长串的线程ID,可以将这个线程ID打印成16进程的

string changeId(const pthread_t &thread_id)
{
    char name[128];
    snprintf(name,sizeof name,"0x%x",thread_id);  //
    return name;
}

这个线程地址是在原生线程库中有关线程的结构体对象的地址,原生线程库中可能存在多个线程,使用接口创建线程的时候,其他进程也有可能在调用这个库,所以需要对线程进行管理。

 对于整个用户而言,虽然是通过pthread库去创建线程的,但是在OS层面,还是需要一个函数去创建这些轻量化进程,也就是clone函数

 #include <sched.h>
 int clone(int (*fn)(void *), void *child_stack,
           int flags, void *arg, ...
           /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );

其中child_stack参数就是获得子线程起始地址的参数。

那么这些子线程究竟是存放在那个位置的呢?-------共享区(mmap)区域

 地址空间中栈区只有一个,每个线程都有自己独立的栈结构,那么这些独立的栈结构在哪儿?这个栈结构在线程对应库对应的TCB中的私有站,每个轻量级进程都有自己的栈结构,主线程的栈在主进程的地址空间中,用的是主进程的栈,其余的轻量化进程的栈结构是在共享区中,也就是在线程库中,在线程库中维护好栈结构即可。

封装线程创建代码

#include <iostream>
#include <pthread.h>
#include <string.h>
#include <cstdio>
#include <cstdlib>
#include <cassert>
using namespace std;
//模仿c++的多线程,对多线程进行封装
/*
    Thread thrad(new Thread(func(),(void*)"string",1))

*/
class Thread;
//用一个结构体去当作pthread_create的第四个参数
class Context{
public:
    Thread *this_;
    void* args_;
public:
    Context():this_(nullptr),args_(nullptr)
    {
        
    }
    ~Context()
    {}
};
class Thread{
public:
    const int num =1024;
    //using func_t = function<void*(void*)>;   //包装器,把void*(void*)的函数指针包装成func_t
    typedef function<void*(void*)> func_t;
public:
//构造函数
    Thread(func_t func,void* args=nullptr, int number=0)
    :func_(func)
    ,args_(args)
    {
        char buffer[num];
        snprintf(buffer,sizeof buffer,"thread-%d",number);
        name_ = buffer;
        Context* ctx = new Context();
        ctx->this_ = this;
        ctx->args_ = args_;
        int n = pthread_create(&tid_,nullptr,static_routine,ctx);
        assert(n==0);  //在意料之中的错误使用assert,在release(会对代码进行一些优化)的版本下,assert不执行
        //异常 ==if  意料之外用异常或者if判断
        (void)n;
    }
    //在类内创建,想让线程执行对应的方法,需要将方法设置为static
    static void* static_routine(void* args)   
    //非静态成员的话,类内第一个参数是this指针,这里会报错因为对应pthread_create的函数指针只允许有一个参数
    //静态方法不能调用成员成员函数和成员变量
    {
        //需要对ctx进行强转
        Context* ctx = static_cast<Context*>(args);
        void* ret = ctx->this_->run(ctx->args_);
        delete ctx;
        return ret;
    }

    void join()
    {
        int n = pthread_join(tid_,nullptr);
        assert(n==0);
        (void)n;
    }

    void* run(void* args)
    {
        return func_(args);
    }
    ~Thread()
    {}
private:
    string name_; 
    func_t func_;
    void* args_;
    

    pthread_t tid_;
};

锁概念的引出

对于全局变量而言,对于不同线程都对一个全局变量进行操作的时候,这时候就容易出现处于判断的临界条件时,当从用户态切换到内核态的时候,对于全局变量的值没有来得及修改之时,另外一个线程同样进入这个函数需要对值进行修改,或许还有更多的线程进入,此时,就容易造成数据的错误或者条件的错误。例如

void* getTick(void* args)
{
std::string username = static_cast<const char *>(args);  
while (true)
  {

        if (tickets > 0)
         {
            usleep(1254); // 1秒 = 1000毫秒 = 1000 000 微妙 = 10^9纳秒  
            //调用usleep的时候,会从用户态转向内核态
            std::cout << username << " 正在进行抢票: " << tickets << std::endl;
            tickets--;
        }
        else
        {
            break;
        }
    }
return nullptr;
}

当执行上述代码时就容易出现如下的情况,让我们来分析一下为何会出现这种情况:

 对变量执行++,--操作时,看起来只有一条语句,但是汇编之后,至少是三条语句:

1.从内存中读取数据到CPU中;2.在寄存器中让CPU进行对应的算逻运算;3.将写的结果写回到内存中变量的位置。

但是对于CPU来说,内部的存储数据的寄存器只有一套,是时间片轮转执行每一个进程和线程的,对于执行的代码而言,首先需要将数据放到CPU上,然后CPU进行计算,计算完成之后,返回内存,完成整个的计算过程,如果在返回的途中,线程被轮换了,此时值还没有被写回内存,线程只有带着此时CPU内部寄存器的值(上下文的内容)在旁边等候,此时又进来一个线程,他就会对全局变量在次进行处理,当他处理了很多次之后,时间片轮转,又轮到上一个线程,他会带着他的上下文内容回来继续从断开的地方执行,此时就会导致全局变量的值出错。所以我们定义的全局变量在没有保护的时候往往是不安全的,像上述说的多个线程交替执行造成的数据安全安全问题,发生数据不一致的错误。

此时在介绍几个概念:

1. 多个执行流进行安全访问的共享资源 - 临界资源

2. 我们把多个执行流中,访问临界资源的代码 -- 临界区 -- 往往是线程代码的很小的一部分

3. 想让多个线程串行访问共享资源 -- 互斥

4. 对一个资源进行访问的时候,要么不做,要么做完 -- 原子性 , 不是原子性的情况 -- 一个对资源进行的操作,如果只用一条汇编就能完成 -- 原子性。

这时候就需要用到锁了,其实锁也是一种资源

int tickets=1000;   //共享资源火车票
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;   //该锁如果是局部的,或者静态的必须使用init进行初始化
void* getTick(void* args)
{
// 1. 多个执行流进行安全访问的共享资源 - 临界资源
// 2. 我们把多个执行流中,访问临界资源的代码 -- 临界区 -- 往往是线程代码的很小的一部分
// 3. 想让多个线程串行访问共享资源 -- 互斥
// 4. 对一个资源进行访问的时候,要么不做,要么做完 -- 原子性 , 不是原子性的情况 -- 一个对资源进行的操作,如果只用一条汇编就能完成 -- 原子性
// 反之:不是原子的 -- 当前理解,方便表述

// 提出解决方案:加锁!

// 就需要尽可能的让多个线程交叉执行
// 多个线程交叉执行本质:就是让调度器尽可能的频繁发生线程调度与切换
// 线程一般在什么时候发生切换呢?时间片到了,来了更高优先级的线程,线程等待的时候。
// 线程是在什么时候检测上面的问题呢?从内核态返回用户态的时候,线程要对调度状态进行检测,如果可以,就直接发生线程切换

    std::string username = static_cast<const char *>(args);  
    while (true)
    {
        // 加锁和解锁的过程多个线程串行执行的,程序变慢了!
        // 锁只规定互斥访问,没有规定必须让谁优先执行
        // 锁就是真实的让多个执行流进行竞争的结果
        pthread_mutex_lock(&lock);   //加锁
        if (tickets > 0)
         {
            usleep(1254); // 1秒 = 1000毫秒 = 1000 000 微妙 = 10^9纳秒  
            //调用usleep的时候,会从用户态转向内核态
            std::cout << username << " 正在进行抢票: " << tickets << std::endl;
            tickets--;
            pthread_mutex_unlock(&lock);    //加锁和解锁的过程只针对于对临界资源的访问,也就是临界取的代码
        }
        else
        {
           pthread_mutex_unlock(&lock);
            break;
        }
    }
    return nullptr;

}

int main()
{
    unique_ptr<Thread> thread1(new Thread(getTick,(void*)"usr1",1));
    unique_ptr<Thread> thread2(new Thread(getTick,(void*)"usr2",2));
    unique_ptr<Thread> thread3(new Thread(getTick,(void*)"usr3",3));
    unique_ptr<Thread> thread4(new Thread(getTick,(void*)"usr4",4));

    thread1->join();
    thread2->join();
    thread3->join();
    thread4->join();

    return 0;


}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值