分离线程
默认情况下,新创建的线程是可等待的,线程退出后,需要对其进行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;
}