Linux多线程(二):线程控制

一、前言

 上篇博客谈到,Linux并没有真线程,而是通过复用进程的数据结构来模拟实现线程的。因此 Linux 自然不会提供线程操作函数,只提供了创建轻量级级进程的系统接口,例如clone、vfork函数。
 但是对于用户来说,创建一个线程还需要自己维护和管理,使用成本太高,于是有人基于Linux在应用层编写了 pthread库 用于方便创建和管理多线程。通过调用OS提供的创建轻量级进程的系统接口,为上层用户提供应用级的线程接口。
 由于pthread不是C原生的库,在使用g++编译链接的时候需要带上 -lpthread 选项。不理解为什么要带上这个选项的同学,可以参见之前的博客:Linux基础IO(四):动静态库的制作与使用
在这里插入图片描述

二、认识线程控制函数

1.线程创建

image-20221201152205788
[作用]: 线程创建
[参数说明]:

  • thread:返回型参数。用于返回线程ID
  • attr:设置线程的属性,attr为NULL表示使用默认属性,一般使用默认即可
  • start_rountine:指定线程启动后要执行的函数。注意该函数的参数和返回值类型都是 void*
  • arg:传递给线程的参数,注意要类型要强转为 void*

[返回值]: 成功返回0,失败返回错误码

 我们在前一篇博客里提到,线程只执行进程的部分代码。这如何实现的呢?此时你就理解了:我们通过传入函数指针的方式,让一个线程只执行该函数的代码

2.线程退出

在这里插入图片描述
[作用]: 退出线程,并通过 retval 返回线程的退出状态

在这里插入图片描述
【作用】: 用于终止指定线程

使用 return 也可以终止线程,return的结果就作为线程的退出状态:

void* rouRoutine(void* arg)
{
    
    
	// ……
	return (void*)10;
}

3.线程等待

在这里插入图片描述
[作用]: 线程等待,获取线程的退出状态;回收资源,避免内存
[返回值]: 成功返回0,错误返回错误码
[参数说明]:

  • thread:线程ID
  • retval:返回性参数。当retval不为NULL时,返回指定线程的退出状态(不能获取退出信号)。由于线程退出状态为void*,因此需要用 void** 的指针去接收

[使用说明]: 如果指定线程已经被退出了,函数会立即返回;否则将会阻塞等待

void* rouRoutine(void* arg)
{
    
    
	// ……
	return (void*)10;
}

int main()
{
    
    
	// ……
	void* retval;
	pthread_join(tid, &retval);
    cout << (long int)retval << endl; ;
}

(说明:Linux系统为64位,指针大小为8字节。64位平台下,long int 也是8字节,因此我们可以将指针强转为long int输出打印线程的退出状态)

[问题一]: 线程出异常了怎么办?不用获取退出信息吗?
 线程就是进程的一部分,一个线程出异常,整个进程都会退出,也就是说线程异常 = 进程异常。当线程发生异常时,退出信息由父进程获取。

4.查看线程id

在这里插入图片描述
[作用]: 返回调用线程的id

// 使用案例:
void* runRoutine(void* arg)
{
    
    
    pthread_t tid = pthread_self();
    printf("%p\n", tid);
    return nullptr;
}
// 某一次的输出结果:0x7ffb5ccb2700

[问题二]:使用 ps -aL指令也可以查看线程id,两者有什么区别?
在这里插入图片描述
LWP (light weight process - 轻量级进程) 对应的就是每个线程的标识符,类似于 PID,是个操作系统使用的,用于标识每个线程的唯一性。
 使用 pthread_self() 函数查看到的用户级线程id,本质上是一个地址,是给用户操作使用的。至于为什么是地址,这里埋个伏笔,我们将在后文中具体阐述。

[问题三]:能不能编写代码查看LWP
 可以使用函数gettid()获取,但是该函数是个半成品,不能直接使用,必须通过系统调用来使用:

int main()
{
     
     
   pthread_t thread;
   pthread_create(&thread, nullptr, func, (void*)"1");
   cout << syscall(SYS_gettid) << endl;
   pthread_join(thread, nullptr);
}   

5.线程分离

 默认情况下,新创建的线程是 joinable可结合的。在线程退出后,需要对其进行 pthread_join 操作,否则无法释放资源,从而造成内存泄漏。但是当我们不关心线程的返回值时,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源 。使用下面的函数就可以实现线程分离,传入线程id即可使用。
在这里插入图片描述

[问题四]:分离后的线程是不能被等待的,但下面的代码为什么没有报错?

void* func(void* arg)
{
     
     
  	pthread_detach(pthread_self());
  	for(int i = 0; i < 5; i++)
 	{
     
     
   		cout << "我是子线程" << endl;
	}
	return nullptr;
}

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

		int n = pthread_join(thread, nullptr);
		cout << strerror(n) << endl;
}

image-20221203163522315
 哪个线程先执行由调度器决定,是不确定的。在子线程运行 pthread_detach() 之前,主线程可能已经可能已经往下执行,开始阻塞式地等待线程退出了。如果在 join 之前 sleep(1) 就能看到正确的看到错误。
image-20221203163913801
借这个问题,我也想提醒大家:在分离线程的时候,尽可能在主线程中分离

6.综合demo

void* startRoutine(void* arg)
{
    
    
    char* name = static_cast<char*>(arg);
    cout << name << " tid = " << pthread_self() << endl;
    return (void*)1;
}

int main()
{
    
    
    pthread_t tid1;   // 线程分离
    pthread_t tid2;   // 线程不分离
    void* retval;

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

    pthread_detach(tid1);    
    pthread_join(tid2, &retval);
    cout << (long int) retval << endl;
    return 0;
}

三、线程id本质是地址?

线程是对进程空间资源的划分,具体是如何划分的呢,我们不难形成这样的认识:

  1. 代码区以函数的形式划分
  2. 全局数据各个线程之间共享
  3. 申请的堆空间对于其他线程来说是可见的

 但是,线程具有独立的栈空间,那么如何理解线程的独立栈呢?我们在前言中提到,pthread库通过调用OS提供的创建轻量级进程的系统接口,为上层用户提供应用级的线程接口。
 因此,线程的全部实现,并没有完全体现在OS内,OS只是提供了执行流,具体的线程结构是由库来进行管理。既然要管理线程,依照先描述后组织的设计思想,库需要设计出线程相应的数据结构来实现对线程的管理:

union pthread_attr_t
{
    
    
  char __size[__SIZEOF_PTHREAD_ATTR_T];  // 私有栈
  long int __align;                      // tid
};

 动态库是加载到进程地址空间的共享区中的,因此我们每创建一个线程,库就在共享区中为我们创建一个线程结构体,并将线程结构体的起始地址返回供用户操作使用。因此pthread_t本质就是该结构体的起始地址。拿到线程的起始地址后,我们就可以访问结构体的各个成员了。image-20221203154447726

猜你喜欢

转载自blog.csdn.net/whc18858/article/details/128340222