【多线程】线程控制 {线程创建,线程异常,在多线程中进行程序替换;线程等待,线程入口函数的参数和返回值;线程终止,线程ID,线程属性结构,线程独立栈结构,线程局部变量;线程分离;pthread库函数}

一、线程创建

1.1 pthread_create函数

pthread_create()函数是一个用于创建线程的函数,它属于pthread线程库(POSIX线程库)。

函数原型如下:

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

参数说明:

  • thread:指向pthread_t类型的指针,用于存储新创建线程的标识符
  • attr:指向pthread_attr_t类型的指针,用于指定线程的属性,通常可以传入NULL使用默认属性。
  • start_routine:指向线程函数的指针,该函数是线程的入口点,线程将从该函数开始执行。
  • arg:传递给线程函数的参数,可以是任意类型的指针。

函数返回值:

  • 成功创建线程时,返回0。
  • 创建线程失败时,返回一个非零的错误码。

需要注意的是,想要调用pthread库函数,必须在g++/gcc命令中指明链接pthread库。

g++ mythread.cc -o mythread -l pthread

测试程序:

void *ThreadRoutine(void *name)
{
    int cnt = 3;
    while (cnt--)
    {
        printf("[%d]: %s\n", getpid(), (char *)name);
        sleep(1);
    }
    cnt /= 0; // 除0错误
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRoutine, (void *)"child thread 1");
    while (1)
    {
        printf("[%d]: main thread\n", getpid());
        sleep(1);
    }
    return 0;
}

运行结果:

在这里插入图片描述

  1. 线程的执行顺序由调度器决定。

  2. 线程出现异常,整个进程随之崩溃。

1.2 线程异常

  • 单个线程如果出现除零,野指针等硬件异常或者收到其他退出信号导致线程崩溃,进程也会随之崩溃。
  • 线程是进程的执行分支,线程出异常,就是进程出异常,进而触发信号机制,终止进程;进程终止,该进程内的所有线程也就随即退出。
  • 我们之前学习的信号是以进程为基本载体的,各线程共享进程收到的信号,和信号的处理方法(pending信号集,handler信号处理方法表)。

1.3 在多线程中进行程序替换

在多线程进程中,如果任何一个线程调用exec函数进行进程程序替换,操作系统会将新的可执行文件加载到当前进程的地址空间中,并重新初始化进程的线程资源。这意味着原有的线程和线程相关的资源都会被销毁,包括线程栈、线程局部变量、线程的上下文等。

测试程序:

//mythread.cc
void *ThreadRoutine(void *name)
{
    cout << "child thread running..." << endl;
    sleep(2);
    execl("./test", "test", nullptr); // 进程程序替换
    return nullptr;
}

int main()
{
    cout << "main thread running..." << endl;
    sleep(2);
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRoutine, nullptr);
    while(1) sleep(1);
}

//test.cc
int main()
{
    while (1)
    {
        cout << "hello world!" << endl;
        sleep(1);
    }
}

运行结果:

在这里插入图片描述


二、线程等待

创建子线程后,主线程同样需要等待子线程退出,获取子线程退出结果,然后操作系统回收子线程PCB。

如果主线程不等待子线程,就会引起类似僵尸进程的问题,从而导致内存泄漏。

2.1 pthread_join函数

pthread_join函数是一个用于等待线程终止的函数。

pthread_join函数的语法如下:

#include <pthread.h>
int pthread_join(pthread_t thread, void **status);

参数:

thread:要等待的线程的线程标识符。pthread_join函数会阻塞等待线程,直到指定的线程终止。

status:指向存储线程退出结果的位置的指针(二级指针)。一旦线程终止,它的退出结果将存储在由status指针指向的位置。如果不关心线程的退出结果,可以将status参数设置为NULL。注意线程函数的返回值是void*,所以要使用二级指针做输出型参数。

返回值:

  • 等待线程成功,返回0。
  • 等待线程失败,返回一个非零的错误码。

注意:与进程等待不同,线程等待不需要关心子线程是否异常,因为一旦子线程出现异常,整个进程就会随之崩溃。线程的异常退出信号就是进程的异常退出信号。

测试程序:

void *ThreadRoutine(void *name)
{
    int cnt = 3;
    while (cnt--)
    {
        printf("[%d]: %s\n", getpid(), (char *)name);
        sleep(1);
    }
    cout << "child thread quit!" << endl;
    // 线程函数返回,线程退出
    return (void *)10; //返回字面常量10的地址(只读常量区)
}

int main()
{
    printf("[%d]: main thread\n", getpid());
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRoutine, (void *)"child thread 1");
   	void *ret;
    pthread_join(tid, &ret); // 阻塞等待子线程退出
    cout << "main thread quit!" << endl;
    cout << "child thread return value: " << (long long)ret << endl;
    return 0;
}

运行结果:

在这里插入图片描述

主线程调用pthread_join阻塞等待子线程,让线程退出具有一定的顺序性,将来可以让主线程进行更多的收尾工作


2.2 pthread_tryjoin_np函数(拓展)

在C++中,可以使用pthread_tryjoin_np函数来进行非阻塞等待线程函数。

#include <pthread.h>
int pthread_tryjoin_np(pthread_t thread, void **retval);

pthread_tryjoin_np函数会尝试非阻塞地等待线程函数的结束。

  • 如果线程函数已经结束,pthread_tryjoin_np会返回0,表示线程成功结束;
  • 如果线程函数尚未结束,pthread_tryjoin_np会返回EBUSY,表示线程尚未结束;
  • 如果出现其他错误,pthread_tryjoin_np会返回相应的错误代码。

测试代码:

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

void *threadFunction(void *arg)
{
	// 线程函数的逻辑
	sleep(5);
	return nullptr;
}

int main()
{
	pthread_t thread;
	// 创建线程并执行线程函数
	pthread_create(&thread, nullptr, threadFunction, nullptr);
	int res;
	// 尝试非阻塞等待线程函数
	while (res = pthread_tryjoin_np(thread, nullptr))
	{
		if (res == EBUSY)
		{
			std::cout << "线程尚未结束" << std::endl;
		}
		else
		{
			std::cout << "出现错误" << std::endl;
		}
		sleep(1);
	}

	std::cout << "线程成功结束" << std::endl;

	return 0;
}

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


2.2 线程入口点函数的参数和返回值

线程入口点函数的参数通过pthread_create函数传入;返回值通过pthread_join接收。

测试程序

void *ThreadRoutine(void *data)
{
    printf("[%d]: %s\n", getpid(), "child thread running!");
    // 子线程处理堆空间数据
    for (int i = 0; i < 10; ++i)
    {
        ((int *)data)[i] = i;
    }
    printf("[%d]: %s\n", getpid(), "child thread quit!");
    // 线程函数返回,线程退出
    return (void *)data; // 返回堆空间的指针
}

int main()
{
    printf("[%d]: main thread running!\n", getpid());
    // 创建一批堆区数据
    int *data = new int[10]{0}; 
    cout << "before: ";
    for (int i = 0; i < 10; ++i)
    {
        cout << data[i] << " ";
    }
    cout << endl;
    
	// 将堆空间数据传递给子线程处理
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRoutine, (void *)data); 
    int *ret;
    pthread_join(tid, (void **)&ret); // 阻塞等待子线程退出

    //打印处理后的堆空间数据
    cout << "after: ";
    for (int i = 0; i < 10; ++i)
    {
        cout << data[i] << " ";
    }
    cout << endl;
    for (int i = 0; i < 10; ++i)
    {
        cout << ret[i] << " ";
    }
    cout << endl;
    
    printf("[%d]: main thread quit!\n", getpid());
    // data和ret指向的堆空间相同,地址相同
    printf("[%d]: child thread return value: %p:%p\n", getpid(), ret, data);
}

运行结果:

在这里插入图片描述

线程入口点函数的参数和返回值,不仅仅可以是字面常量的地址,还可以是堆空间的地址。因为各线程共享进程的地址空间,也包括堆空间。所以我们可以通过堆空间将一批数据传入子线程,当然子线程也可以将一批数据传递给主线程。


三、线程终止

终止一个线程的方法有:

  • 方法一:在子线程入口点函数中执行return语句,终止子线程。
  • 方法二:在子线程的任意位置,任意函数中调用pthread_exit函数,直接终止子线程。
  • 方法三:主线程调用pthread_cancel函数,终止指定tid的子线程。

注意:不要在子线程中调用exit函数,在子线程中调用exit会止整个进程。

3.1 pthread_exit

pthread_exit函数是一个线程终止函数,用于终止当前线程的执行并返回一个指定的退出结果。

函数原型如下:

void pthread_exit(void *retval);

参数说明:

  • retval:指定线程的退出结果,可以是任意类型的指针。

注意事项:

  • 当线程调用pthread_exit函数时,它会立即终止当前线程的执行,并将retval作为线程的退出结果返回给等待该线程的其他线程。

  • 如果主线程调用pthread_exit函数,那么整个进程将会终止。


3.2 pthread_cancel

pthread_cancel函数用于向指定的线程发送取消请求,以请求终止该线程的执行。

函数原型如下:

int pthread_cancel(pthread_t thread);

参数说明:

  • thread:要取消的线程的标识符。

注意事项:

  • 当调用pthread_cancel函数时,会向指定的线程发送一个取消请求。
  • 被取消的线程可以选择在合适的时机终止自己的执行,并将PTHREAD_CANCELED(-1)作为退出结果返回。
  • 子线程不要调用pthread_cancel向主线程发送取消请求,因为主线程负责等待所有的子线程退出,取消主线程可能还会影响整个进程。
  • 不要向当前线程的tid发送取消请求,该行为属于未定义行为,可能出现各种意想不到的错误。

测试程序:

void *ThreadRoutine(void *arg)
{
    while (true)
    {
        cout << "child thread running..." << endl;
        sleep(1);
    }
    pthread_exit((void *)12); // 子线程不会执行到这里
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRoutine, nullptr);
    sleep(3);
    pthread_cancel(tid); // 向子线程发送取消请求
    void *ret;
    pthread_join(tid, &ret);
    cout << "cancel child thread, tid: " << tid << " retval: " << (long long)ret << endl;
    sleep(2);
    return 0;
}

运行结果:

在这里插入图片描述


3.3 线程ID

3.3.1 线程ID和LWP

观察运行结果中pthread_create获取的线程标识符(tid),并不是我们预想中的LWP的值。

  • LWP属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度的基本单位,所以需要一个数值来唯一表示该线程。
  • pthread_create获取的线程ID(tid)是一个地址,指向一个虚拟内存单元,属于pthread线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。

pthread_self函数是pthread库中的一个函数,用于获取当前线程的线程ID(Thread ID)。

函数原型如下:

pthread_t pthread_self(void);

pthread_self函数没有参数,直接返回当前线程的线程ID,即pthread_t类型的值。

线程ID是一个唯一标识符,用于区分不同的线程。在多线程程序中,每个线程都有自己的线程ID。通过pthread_self函数,可以获取当前线程的线程ID,以便进行线程的标识和管理。

pthread_t类型是一个不透明的数据类型,实际上是一个结构体指针。它通常被用作线程的标识符,用于创建、操作和等待线程。

3.3.2 线程属性结构

在创建和运行线程时,我们调用的是pthread库函数,并不是Linux系统直接提供的系统调用。

Linux系统不提供线程相关接口,没有专门的线程结构,而是统一提供轻量级进程的接口和内核数据结构,只负责调度执行轻量级进程。也就是说在内核看来,进程和线程没有结构上的区别。

但是各线程仍需要有独属于自己的一份属性和资源,如线程ID,线程退出结果,栈结构等。这些属性和资源是在内核数据结构中无法体现的。所以pthread库除了提供线程相关的操作外,还专门为线程设计了一个线程属性结构,作为内核轻量级进程的补充数据。线程ID(pthread_t类型)其实就是该结构体的指针。

在这里插入图片描述

在调用pthread_create创建线程时:一方面,系统会创建轻量级进程的内核数据结构,如task_struct等。另一方面,pthread线程库也会在动态库的虚拟内存中(共享区)创建一个线程属性结构,用于存储该线程的相关属性和资源。其中就包含了该线程的独立栈结构、线程局部存储等。

3.3.3 线程的独立栈结构

pthread库是如何做到指定线程的栈结构的呢?pthread_create函数底层封装了系统调用cloneclone用于创建一个新的进程或线程。其函数原型如下:

int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ...);

其中参数fn是指向新进程或线程要执行的函数的指针;参数child_stack是指向新进程或线程的栈空间的指针。pthread_create会将在动态库中创建的栈结构地址,作为child_stack参数传递给clone系统调用。操作系统会将其作为新线程的栈结构。新线程在调度执行时,就会使用动态库中的栈结构了。

注意:主线程使用的是内核级的栈结构(地址空间中的栈区),而各子线程使用的是动态库中的独立栈结构(地址空间中的共享区)。各线程调用栈,执行函数互不影响。

3.3.4 线程的局部存储

  • 进程的全局变量被所有进程共享。
  • __thread修饰全局变量,使该全局变量被每个线程各自独立拥有,这就是线程的局部存储。
  • 线程的局部存储:
    • 作用域:线程的全局作用域;
    • 存储位置:地址空间共享区 --> pthread_动态库 --> 线程属性结构;
    • 访问权限:被每个线程各自独立拥有。

测试程序:

//int g_val = 0;
__thread int g_val = 0;

void* ThreadRoutine(void* name)
{
    while(true)
    {
        sleep(1);
        cout << (char*)name << pthread_self() << " g_val: " << g_val << " &g_val: " << &g_val << endl;
        ++g_val;   
    }

}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRoutine, (void*)"child thread 1");
    while(true)
    {
        cout << "main thread: " << pthread_self() << " g_val: " << g_val << " &g_val: " << &g_val << endl;
        sleep(1);
    }
}

运行结果:

在这里插入图片描述


四、线程分离

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

pthread_detach函数用于将一个线程标记为可分离的(detached)。可分离的线程在其执行结束后会自动释放其资源,无需其他线程调用pthread_join函数来等待该线程的结束。

函数原型如下:

int pthread_detach(pthread_t thread);

参数:

thread:是一个pthread_t类型的线程标识符,用于指定要分离的线程。

返回值:

  • 等待线程成功,返回0。
  • 等待线程失败,返回一个非零的错误码。

pthread_detach函数的作用是将指定的线程标记为可分离的。一旦线程被标记为可分离,它的资源将在线程结束时自动释放,无需其他线程调用pthread_join函数来等待它的结束。

需要注意的是,pthread_detach函数必须在目标线程尚未被其他线程调用pthread_join函数之前调用。如果目标线程已经被其他线程调用pthread_join函数等待,那么调用pthread_detach函数将会失败。

被分离的线程出现异常,仍然会影响到整个进程,使整个进程崩溃。

测试程序:

void *ThreadRoutine(void *arg)
{
    pthread_detach(pthread_self()); // 线程分离
    cout << "child thread running..." << endl;
    sleep(2);
    pthread_exit((void*)12);
}

int main()
{
    cout << "main thread running..." << endl;
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRoutine, nullptr);
    sleep(1);
    int ret = pthread_join(tid, nullptr);
    cout << "ret = " << ret << ", strerrno: " << strerror(ret) << endl;
}

运行结果:

在这里插入图片描述

等待线程失败,报错:无效参数(tid)。不能调用pthread_join等待已经分离的线程。


五、语言级别的多线程接口

  • 在Linux平台下,C++、Java、Python等语言级别的多线程接口,底层也一定封装了pthread线程库。所以在编译时必须在g++命令中指明链接pthread库。
  • 提供语言级别的多线程接口的原因有二:1. 简化操作,方便用户使用。 2. 为了实现语言的跨平台性
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

芥末虾

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值