4

[ Linux ] 线程独立栈,线程分离,Linux线程互斥

 1 year ago
source link: https://blog.51cto.com/xingyuli/5945585
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

1.线程栈

我们使用的线程库是用户级线程库(pthread),我们使用 ldd mythread 可以查看mythread的链接信息。

[ Linux ] 线程独立栈,线程分离,Linux线程互斥_互斥锁

因此对于一个线程(tast_struct)都是通过在共享空间内执行pthread_create执行线程创建的。所有的代码执行都是在进程的进程地址空间内进行执行的。在了解这些基本的概念之后,我们回顾上一篇的一个问题,pthread_t究竟是什么呢?

1.1pthread_t

上篇文章我就提到过pthread_t是一个无符号长整型整数,但是并没有说pthread_t具体是什么?但是我们把他转化为一个16进制数字时,我们发现这个数字特别想一个地址,那么这里我们需要确认的是,pthread_t 线程Id就是一个地址。而是什么地址呢?我们这里需要知道的是,线程的全部实现,并没有全部体现在操作系统内,而是操作系统提供执行流,具体的线程结构由库来进行管理。库可以创建多个线程->因此库也要管理线程。而库要管理线程也是要先描述再组织。因此在共享区里面包含了struct thread_info,里面就会保存pthread_t tid,线程私有栈等。而申请一个新的线程,库就又会在共享区内创建该线程对应的tid,私有栈等等。而返回的就是该结构的地址。因此pthread_t里面保存的就是对应用户级线程的控制结构体的起始地址!

  • 主线程的独立栈结构,用的就是地址空间内的栈区
  • 新线程用的栈结构,用的是库中提供的栈结构
[ Linux ] 线程独立栈,线程分离,Linux线程互斥_临界资源_02

pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL(原生线程库)实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。

[ Linux ] 线程独立栈,线程分离,Linux线程互斥_临界资源_03

Linux中,用户级线程库和LWP是1:1的。

1.2用户级的线程id与内核LWP的对应关系

我们刚刚已经知道了用户级线程id和内核LWP的对应是1:1的。那么我们如果使用代码来验证一下呢?

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h>//仅仅是了解

using namespace std;

// 带__thread 给每个线程拷一份
__thread int global_value = 100;

void *startRoutine(void *args)
{
while (true)
{
cout << "thread " << pthread_self() << " global_value: "
<< global_value << " &global_value: " << &global_value
<< " Inc: " << global_value++ << " lwp: " << ::syscall(SYS_gettid) << endl;
sleep(1);
}
}

int main()
{
pthread_t tid1;
pthread_t tid2;
pthread_t tid3;

pthread_create(&tid1, nullptr, startRoutine, (void *)"thread 1");
pthread_create(&tid2, nullptr, startRoutine, (void *)"thread 2");
pthread_create(&tid3, nullptr, startRoutine, (void *)"thread 3");

pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
pthread_join(tid3, nullptr);

return 0;
}

[ Linux ] 线程独立栈,线程分离,Linux线程互斥_互斥量_04

我们同样使用监控脚本来看看当前系统下的LWP

while :; do ps -aL |head -1 && ps -aL|grep mythread;sleep 1;done

通过打印的结果我们发现 是能够看到用户级线程id和内核LWP的对应是1:1的。

2.分离线程

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

在什么时候下会使用线程分离呢?

我们都知道主线程会join等待新线程,如果新线程一直不退出,主线程就会一直等待,等新线程退出之后释放新线程的资源,这与我们的进程阻塞式等待类似。如果当主线程并不关心或者不需要新线程的退出码时,新线程可以自己退出后自己释放自己的资源。那么主线程就可以不需要等待新线程了。这就完成了线程间的解耦。也叫做线程分离。

2.1 pthread_detch

  • 函数原型:
  • int pthread_detach(pthread_t thread);
  • 可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离
  • pthread_detach(pthread_self());

注意:joinable和分离是冲突的,一个线程不能即是joinable的又是分离的。

[ Linux ] 线程独立栈,线程分离,Linux线程互斥_互斥锁_05
#include <iostream>
#include <cstdio>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h>//仅仅是了解

using namespace std;

// 带__thread 给每个线程拷一份
__thread int global_value = 100;

void *startRoutine(void *args)
{
//线程分离
//pthread_detach(pthread_self());
while (true)
{
cout << "thread " << pthread_self() << " global_value: "
<< global_value << " &global_value: " << &global_value
<< " Inc: " << global_value++ << " lwp: " << ::syscall(SYS_gettid) << endl;
sleep(1);
}
}

int main()
{
pthread_t tid1;
pthread_t tid2;
pthread_t tid3;

pthread_create(&tid1, nullptr, startRoutine, (void *)"thread 1");
pthread_create(&tid2, nullptr, startRoutine, (void *)"thread 2");
pthread_create(&tid3, nullptr, startRoutine, (void *)"thread 3");

sleep(1);

//倾向于让主线程分离其他线程
pthread_detach(tid1);
pthread_detach(tid2);
pthread_detach(tid3);

//一旦分离不能join
int n1 = pthread_join(tid1, nullptr);
cout<<"strerror(n1): "<< strerror(n1)<<endl;
int n2 = pthread_join(tid2, nullptr);
cout<<"strerror(n2): "<< strerror(n2)<<endl;

int n3 = pthread_join(tid3, nullptr);
cout<<"strerror(n3): "<< strerror(n3)<<endl;

return 0;
}
[ Linux ] 线程独立栈,线程分离,Linux线程互斥_互斥锁_06

通过这个实验也验证了一个线程不能即是detach又被join的。

3.线程互斥

3.1互斥相关概念

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

3.2 互斥量mutex

  • 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
  • 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
  • 多个线程并发的操作共享变量,会带来一些问题。

3.3 售票系统案例验证共享变量会有问题

为了验证共享变量会出问题的情况,我们模拟实现一个售票系统的案例。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h> //仅仅是了解

using namespace std;

int tickets = 10000; // 临界资源

void *getTickets(void *args)
{
const char *name = static_cast<const char *>(args);
while (true)
{
//临界区
if (tickets > 0)
{
cout << name << " 抢到了票,票的编号是:" << tickets << endl;
tickets--;
}
else
{

cout << name << " 已经放弃抢票了,因为没有票了....." << endl;
break;
}
}
return nullptr;
}

int main()
{
pthread_t tid1;
pthread_t tid2;
pthread_t tid3;

pthread_create(&tid1, nullptr, getTickets, (void *)"thread 1");
pthread_create(&tid2, nullptr, getTickets, (void *)"thread 2");
pthread_create(&tid3, nullptr, getTickets, (void *)"thread 3");

// sleep(1);

// // 倾向于让主线程分离其他线程
// pthread_detach(tid1);
// pthread_detach(tid2);
// pthread_detach(tid3);

// 一旦分离不能join
int n1 = pthread_join(tid1, nullptr);
cout << "strerror(n1): " << strerror(n1) << endl;
int n2 = pthread_join(tid2, nullptr);
cout << "strerror(n2): " << strerror(n2) << endl;
int n3 = pthread_join(tid3, nullptr);
cout << "strerror(n3): " << strerror(n3) << endl;

return 0;
}
[ Linux ] 线程独立栈,线程分离,Linux线程互斥_互斥量_07

执行结果我们可以发现,看似好像没有什么问题,但是其实是存在bug的。在这段代码中

[ Linux ] 线程独立栈,线程分离,Linux线程互斥_互斥量_08

这一段代码是既对票做判断,又对票做--,--是并不是由一条语句执行的,而是被翻译成3条语句执行的。

CPU对tickets--这句话,要翻译成:

  1. 取数据。将数据从内存取到cpu寄存器内
  1. load :将共享变量ticket从内存加载到寄存器中
  1. 做运算。在寄存器内对数据进行运算。
  1. update : 更新寄存器里面的值,执行-1操作
  1. 写回数据。将数据从寄存器写回内存。
  1. store :将新值,从寄存器写回共享变量ticket的内存地址

我们可以看看ticket--部分的汇编代码:

取出ticket--部分的汇编代码

objdump -d a.out > test.objdump

152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34 <ticket>

153 400651: 83 e8 01 sub $0x1,%eax

154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 600b34 <ticket>

而这3个步骤中,线程在任何地方都有可能切换走,而CPU内的寄存器是被所有的执行共享的,但是寄存器里面的数据是属于当前执行流的上下文数据。因此线程要被切换的时候,需要保存上下文;线程要被换回的时候,需要恢复上下文。

因此为了从程序中看到可能错误的数据,我们需要加一个usleep来模拟漫长的业务过程,可能有很多个线程会进入该代码段。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h> //仅仅是了解

using namespace std;

// // 带__thread 给每个线程拷一份
// __thread int global_value = 100;

// void *startRoutine(void *args)
// {
// //线程分离
// //pthread_detach(pthread_self());
// while (true)
// {
// cout << "thread " << pthread_self() << " global_value: "
// << global_value << " &global_value: " << &global_value
// << " Inc: " << global_value++ << " lwp: " << ::syscall(SYS_gettid) << endl;
// sleep(1);
// }
//}

int tickets = 10000; // 临界资源

void *getTickets(void *args)
{
const char *name = static_cast<const char *>(args);
while (true)
{
//临界区
if (tickets > 0)
{
usleep(1000);//模拟漫长的业务
cout << name << " 抢到了票,票的编号是:" << tickets << endl;
tickets--;
}
else
{

cout << name << " 已经放弃抢票了,因为没有票了....." << endl;
break;
}
}
return nullptr;
}

int main()
{
pthread_t tid1;
pthread_t tid2;
pthread_t tid3;

pthread_create(&tid1, nullptr, getTickets, (void *)"thread 1");
pthread_create(&tid2, nullptr, getTickets, (void *)"thread 2");
pthread_create(&tid3, nullptr, getTickets, (void *)"thread 3");

// 一旦分离不能join
int n1 = pthread_join(tid1, nullptr);
cout << "strerror(n1): " << strerror(n1) << endl;
int n2 = pthread_join(tid2, nullptr);
cout << "strerror(n2): " << strerror(n2) << endl;
int n3 = pthread_join(tid3, nullptr);
cout << "strerror(n3): " << strerror(n3) << endl;

return 0;
}
[ Linux ] 线程独立栈,线程分离,Linux线程互斥_互斥锁_09

此时我们确实看到了,产生了脏数据。

3.4 解决抢票问题

要解决以上的问题,我们需要做到三点:

  1. 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  2. 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  3. 如果线程不在临界区中执行,那么该线程不能组织其他线程进入临界区。

要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫做互斥量。

在临界区内,只能够允许一个线程执行,不允许多个线程同时执行,因此一旦我们给买票的过程加上一把锁,在某一时刻,只能够允许一个线程买票,因此可以保证整个买票的过程是原子的。

[ Linux ] 线程独立栈,线程分离,Linux线程互斥_互斥量_10

3.5互斥量的接口

3.5.1初始化互斥量

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

  • 方法一:静态分配
  • pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER、
  • 方法二:动态分配
  • int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
  • mutex:要初始化的互斥量
  • attr:NULL
[ Linux ] 线程独立栈,线程分离,Linux线程互斥_临界资源_11

3.5.2 销毁互斥量

销毁互斥量需要注意:

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

函数原型:int pthread_mutex_destroy(pthread_mutex_t *mutex);

3.5.3 编码

在我们上述的售票系统中,其中很明显的是,票数tickets属于临界资源,我们需要对其进行加锁。

在我们申请锁成功之后,我们对互斥量进行加锁和解锁,我们将使用pthread_mutex_lock和pthread_mutex_unlock。

int pthread_mutex_lock(pthread_mutex_t *mutex);

int pthread_mutex_unlock(pthread_mutex_t *mutex);

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

注意:我们加锁只需要对临界区加锁,而且加锁的粒度越细越好。而加锁的本质是让线程执行临界区的代码串行化。

调用pthread_mutex_lock时,可能会遇到一下情况:

  • 互斥量处于未锁状态,该函数将会互斥量锁定,同时返回成功
  • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_mutex_lock调用会陷入阻塞(执行流被挂起),等待互斥锁解锁。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h> //仅仅是了解

using namespace std;

int tickets = 10000; // 临界资源
pthread_mutex_t mutex;//定义锁
void *getTickets(void *args)
{
const char *name = static_cast<const char *>(args);
while (true)
{
//临界区
pthread_mutex_lock(&mutex);
if (tickets > 0)
{
usleep(1000);
cout << name << " 抢到了票,票的编号是:" << tickets << endl;
tickets--;
pthread_mutex_unlock(&mutex);
}
else
{

cout << name << " 已经放弃抢票了,因为没有票了....." << endl;
pthread_mutex_unlock(&mutex);
break;
}
}
return nullptr;
}

int main()
{
pthread_mutex_init(&mutex,nullptr);
pthread_t tid1;
pthread_t tid2;
pthread_t tid3;

pthread_create(&tid1, nullptr, getTickets, (void *)"thread 1");
pthread_create(&tid2, nullptr, getTickets, (void *)"thread 2");
pthread_create(&tid3, nullptr, getTickets, (void *)"thread 3");

// 一旦分离不能join
int n1 = pthread_join(tid1, nullptr);
cout << "strerror(n1): " << strerror(n1) << endl;
int n2 = pthread_join(tid2, nullptr);
cout << "strerror(n2): " << strerror(n2) << endl;
int n3 = pthread_join(tid3, nullptr);
cout << "strerror(n3): " << strerror(n3) << endl;

pthread_mutex_destroy(&mutex);
return 0;
}

3.5.4 互斥锁的相关问题

  • 加锁是一套规范,通过临界区对临界资源进行访问的时候,要加就都需要加锁,不能有的线程加锁有的线程不加锁。
  • 锁保护的是临界区,任何线程执行临界区代码访问临界资源,都必须现申请锁,前提是都必须看到锁!那么这把锁本身也是临界资源!而锁的设计者也考虑了这个问题,pthread_mutex_lock线程竞争锁的过程,就是原子的!

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK