linux: pthread库的使用和理解

一. pthread线程库的基本使用

1.1 简单的crate和join接口&&线程的工作函数

首先我们线程作为进程的执行流的一部分,我们要自己拟定一个routine的工作函数,有的版本叫做(work,run函数),pthread_create的作用是 根据你传入的tid输出型参数,

第二个参数是关于对线程属性的初始化(我一般设置为nullptr),第三个参数是工作函数,线程专门的工作函数,其次是 arg它表示你可以传递任意参数给线程的工作函数(包括一个对象之后在强转即可)

简单来说就是,当你调用该接口,系统会帮你创建线程然后去到你指定的routine函数中去执行代码

线程的工作函数也可以叫做线程函数

cpp 复制代码
// 函数类型 如下 返回值和参数 
void* work(void*arg){

}

// 简单的使用 返回值是表示线程正常返回的结果
// void*是指传参给线程的参数 
void*work(void*arg){
    // 加入传入了一个简单的int 表示年龄 我打印一下
    int age = static_cast<int>(arg);
    cout<<"my age is: "<<age<<std::endl;
     // 加入你不想返回值也行 但是你想返回值 表示这是正常执行完
    //你的线程你想返回的值 void*表示你可以返回任意类型的值 如 字符串吧
    const char* ret = "正常返回了哈";
   return (void*)ret;

}

你要想理解线程的工作函数,这里有一个很秒的点就是void*的使用,传参传递到一定境界,我们的业界大佬就提出,干脆用void*表示你可以传递任意对象,哪怕你是一个内置类型我都给你强制为void* 到时候我转回去就行。

具体的体现如上在work函数传参的时候你可以传递一个对象,也可以传递一个内置类型。

但是它的返回值有要求的是需要你返回非栈变量的,如果你要返回最好是一个动态内存的指针或者全局变量,当然如果是 共享资源:全局变量和静态变量也可以啊。

关于线程的返回值处理可以参考下文他是很有讲究的哦!

线程的回收,线程回收函数返回值就是: 成功则返回0,失败则返回错误码,是不是跟进程不一样,进程之间exit退出了同时设置了错误码,但是线程咋能退出进程呢,线程是进程的一部分所以它只敢弱弱的返回一下错误码。 线程的第一个参数就是 把tid给过去,这里输入型参数哦,其次第二个参数就是对于线程执行后的返回值处理: 这里的返回值一共有三种:

我们先看这个返回值是正常退出的时候如何获取,只有为什么要在这里来获取线程的返回值,读者可以思考一下,文中我会一一解决的。

看如下代码吧:

cpp 复制代码
void *work(void *arg)
{
	// 加入传入了一个简单的int 表示年龄 我打印一下
	int age = (long int)arg;
	std::cout << "my age is: " << age << std::endl;
	// 加入你不想返回值也行 但是你想返回值 表示这是正常执行完
	// 你的线程你想返回的值 void*表示你可以返回任意类型的值 如 字符串吧
	const char *ret = "正常返回了哈";
	int a = 2;
	

}

int main()
{

	pthread_t tid;
	task *mytask1 = new task(100, 21);
	pthread_create(&tid, nullptr, work, (void *)21);
	// 简单的回收 以及获取返回值
	void *ret;
	pthread_join(tid, &ret);
	std::cout << "返回值 ret: " << (char*)ret;

	return 0;
}

代码中值得提及的是关于这的返回值的介绍,我们创建的变量都是 void*ret,传值&ret表示他是输出型参数,其次就是我们使用它的时候其实是前面介绍的它可以指向任意类型来使用,真正想使用的时候强制即可。

输出:

1.2 线程的退出

1.2.1 正常退出与pthread_exit()方法

先说正常退出,对于正常退出的值,是我们自己定义的,可以是内置类型也可以是一共对象返回了,总之void* 可以表示任意类型,你获取之后根据你的需要获取就行了但是有一点就是关于作用域的事情,线程也是有自己独立栈空间的,你可以理解为这是你自己调用了一共函数但是空间是不同的但是确是不同与主线程的执行流。

所以返回值不能是栈空间,如果是指针指向的话最好是动态的,当然如果是静态变量和全局变量,自然也不需要想这些咯。

举一个例子正常退出:

cpp 复制代码
// 我们打算让线程帮我做计算的工作 传递进来一个结构体
class task
{
public:
	task(int x, int y)
		: x_(x), y_(y)
	{
	}

	int add()
	{
		return x_ + y_;
	}

public:
	int x_;
	int y_;
};
int retadd;
void *run(void *arg)
{
	task *mytask = static_cast<task *>(arg);
	// 设置我们正常退出的返回值
	retadd = mytask->add();
	return (void *)retadd;
}

int main()
{

	pthread_t tid;
	task *mytask1 = new task(100, 21);
	pthread_create(&tid, nullptr, run, mytask1);
	// 简单的回收 以及获取返回值
	void *ret;
	pthread_join(tid, &ret);
	std::cout << "返回值 ret: " << (long long int)ret;

	return 0;
}

首先对于线程的终止有三种方式,pthread_exit

有时候我们会有这样的需求,那就是我们主动在这个线程函数中主动退出,但是不想把这个进程都退出了,所以不能用exit他会把整个进程退出了,所以我们可以用这个接口,注意我们观察这里有一共传参,表示你可以传值过去给返回值,然后设置线程退出的返回值,这个返回值在哪里接受,以及线程的返回值有多方式返回了,如何保证的,读者可以思考一下,下午即将提到。

1.2.2 让线程取消退出

这个方法一般是其它线程调用它,然后把指定tid的线程退出了,这个时候该线程的整个返回值,会被设置为PTHREAD_CANCELED 这个值是什么呢 请看如下:

1.3 线程的返回值

首先你应该感受到,我们线程的返回值,似乎有好几个来源,可以从线程的work函数,线程的主动退出等等,但是谁来接受呢,以及如何接受呢?

这里我正式接受线程等等

当一个线程退出以后,你要知道线程是一个执行流,它不仅仅是一个函数,它很像我们的进程一一样也有自己的退出码啊tid等等,所以它的退出情况,我们是要主动知道的,它的返回值我们可以自己设置正常情况的返回值,也可以是任意类型,不过要强制为void*

之后在主函数我们用void*ret 在pthread_join(tid,&ret); 输出型参数来接受,然后把ret强转再访问即可。

cpp 复制代码
void *work(void *arg)
{
	// 加入传入了一个简单的int 表示年龄 我打印一下
	int age = (long int)arg;
	printf("my age is: %d\n", age); // 使用 printf
	sleep(5);
	const char *ret = "正常返回了哈\n";
	return (void *)ret;
}

int main()
{

	pthread_t tid;
	task *mytask1 = new task(100, 21);
	pthread_create(&tid, nullptr, work, (void *)21);
	pthread_cancel(tid);
	void *ret;
	pthread_join(tid, &ret);
	if (ret == PTHREAD_CANCELED)
		std::cout << "返回值 ret: " << (long long int)ret << std::endl;
	else
	{
		std::cout << "返回值 ret: " << (char *)ret << std::endl;
	}

	return 0;
}

线程返回测试样例:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thread1(void *arg)
{
	printf("thread 1 returning ... \n");
	int *p = (int *)malloc(sizeof(int));
	*p = 1;
	return (void *)p;
}
void *thread2(void *arg)
{
	printf("thread 2 exiting ...\n");
	int *p = (int *)malloc(sizeof(int));
	*p = 2;
	pthread_exit((void *)p);
}
void *thread3(void *arg)
{
	while (1)
	{ //
		printf("thread 3 is running ...\n");
		sleep(1);
	}
	return NULL;
}
int main(void)
{
	pthread_t tid;
	void *ret;
	// thread 1 return
	pthread_create(&tid, NULL, thread1, NULL);
	pthread_join(tid, &ret);
	printf("thread return, thread id %X, return code:%d\n", tid, *(int *)ret);
	free(ret);
	// thread 2 exit
	pthread_create(&tid, NULL, thread2, NULL);
	pthread_join(tid, &ret);
	printf("thread return, thread id %X, return code:%d\n", tid, *(int *)ret);
	free(ret);
	// thread 3 cancel by other
	pthread_create(&tid, NULL, thread3, NULL);
	sleep(3);
	pthread_cancel(tid);
	pthread_join(tid, &ret);
	if (ret == PTHREAD_CANCELED)
		printf("thread return, thread id %X, return code:PTHREAD_CANCELED\n", tid);
	else
		printf("thread return, thread id %X, return code:NULL\n", tid);
}

2.4 线程的detach

当你不在乎线程的退出情况,也就是线程退出之后你不主动回收是会占着资源的,这一点跟我们僵尸进程很像,所有你可以detach 分离状态,当然一个i进程不能及时joinable又是detach的 所以如下测试一下:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thread_run(void *arg)
{
	pthread_detach(pthread_self());
	printf("%s\n", (char *)arg);
	return NULL;
}
int main(void)
{
	pthread_t tid;
	if (pthread_create(&tid, NULL, thread_run, (void*)"thread1 run...") != 0)
	{
		printf("create thread error\n");
		return 1;
	}
	int ret = 0;
	sleep(1); // 很重要,要让线程先分离,再等待
	if (pthread_join(tid, NULL) == 0)
	{
		printf("pthread wait success\n");
		ret = 0;
	}
	else
	{
		printf("pthread wait failed\n");
		ret = 1;
	}
	return ret;
}

二.储备知识

2.1 pthread库的底层

我们的linux中本质上是没有线程的概念的,只有轻量级进程的概念,也就是LPB: light procss blcok ,线程是进程的的一部分,是进程的执行流资源。

那作为进程的一部分,我们的线程作为执行流,它会执行我们进程给定的特定执行单元去执行,这就是所谓的: 线程是进程资源的一部分。

到这我们提及的进程还仅仅是轻量级进程,既然linux没有线程的概念,我们自然要有人来维护线程的概念等等是谁呢,pthread_线程库,它底层封装了内核级别的轻量级进程库,也封装了线程操作,在这之后我们使用起来那就是线程咯。

如下图理解: 这就是我们pthread动态库来维护的线程,底层其实封装的是内核的轻量级进程。

2.2 线程具有独立性和线程们通过同一个地址空间访问资源

2.2.1 线程都在同一进程下

通过这个我们来了解线程的几个特征:

所有线程 始终都在同一线程内: 如下代码验证:用指令 ps - aL 来查看轻量级进程的信息

代码: 我们创建了一个线程: 以及让该线程和主线程都循环不退出并且打印自己的当前进程的pid 结果一样证明线程都在同一进程下特点(你也可以多打印几个线程来观察)

cpp 复制代码
// 主线程 和子线程
#include <iostream>
#include <pthread.h>
#include <unistd.h>

void *threadRun(void *args)
{
	while (1)
	{
		std::cout << "new thread: " << getpid() << std::endl;
		sleep(1);
	}
	return nullptr;
}

int main()
{
	pthread_t tid;
	pthread_create(&tid, nullptr, threadRun, nullptr);

	while (1)
	{
		std::cout << "main thread: " << getpid() << std::endl;
		sleep(1);
	}
}

输出结果:

2.2.2 线程有自己的独立局部存储区域

这里主要用到的一共关键字就是 __thread 但是它只能用来定义内置类型,他是独属于每一个线程自己独立的全局变量/静态变量,因为我们知道对于线程来说 全局变量或者静态变量是共享资源所有线程都能直接访问会导致访问冲突的,所以我们用__thread定义的全局变量每一个线程都有一共自己的全局变量 ---但是它存储在线程的局部存储区域 我们可以通过地址空间来验证一下

代码:

cpp 复制代码
__thread int index = 0;
int global_val = 1; // 全局变量
void *work(void *)
{
	pthread_t tid = pthread_self();
	printf("线程: tid: %ld  线程局部栈帧存储index: %d 全局变量:%d \n", tid, index, global_val);
	// 让全局变量改变一下 和局部栈存储的index
	index++;
	global_val++;
	return nullptr;
}

int main()
{

	std::vector<pthread_t> tids;
	int num = 10; // 创建10个线程
	for (int i = 0; i < num; i++)
	{
		pthread_t tid;
		pthread_create(&tid, nullptr, work, nullptr);
		tids.push_back(tid);
	}
	// 让线程运行一会再回收
	sleep(5);
	// 回收
	for (int i = 0; i < num; i++)
	{
		pthread_join(tids[0], nullptr);
	}
}

2.2.3 线程都通过同一地址空间访问

首先线程将来都会映射到我们进程的地址空间当中,资源本质上是与进程是共享的,所以我们线程虽然具有独立性,但是天然的,大家都是在同一块地址空间上,所以线程想要通信那是很轻松的。

但是实际上想让线程看到我们主函数栈上的内容是需要传参的,也就保证了一定的独立性,但是对于全局变量和静态变量,所有线程大家都是共享的。

基于这里我们深刻立即无论是线程还是进程想去内存拿资源大家都是通过虚拟地址空间来访问的,所以线程获取到另一个线程的资源,又或者是另一个线程的pid都是可以的

举例: 我们在全局变量定义一个vector<pthread_t> 将来把所有线程的信息如tid放进去,之后我可以在其中一个线程去调用另一个线程,然后对他控制/

2.2.3 线程的执行

当读者看到这里,其实基本上已经能够对线程的认知有了足够深刻的感受。 我提一些关于线程的执行的特点,线程本质上是我们cpu在调度一个进程的时候把这个进程的执行分为好几个执行流,如果是单核的情况下,cpu可以分配给线程的时间片会被线程瓜分,然后基于进程内部进行线程的切换。

它的优点在于: 一个进程有多个执行流, 如果当前进程处理的是io流存在阻塞的时候是很方便而且高效率的,但是对于计算密集型的它不需要频繁切换执行流的类型来说不太适合多线程

以及线程它够轻,它的粒度更细,它只是我们进程的一个资源,比如cpu中对一个进程的调度存在一共热缓存 chache缓存,如果切换进程则会冷处理,而如果是线程的话始终在一共进程啊自然不会,效率会更高。

在就是线程的缺点了,你想线程始终在同一个进程下,它访问的很多资源都是同一个进程的啊,必然造成的问题,就是同步互斥的问题,我们想实现一共io密集型的多线程程序,自然必要考虑的就是同步互斥问题了,这个也是多执行流最为核心的问题,写者下期就会出哦! 敬请期待

相关推荐
这儿有一堆花3 小时前
Kali Linux:探测存活到挖掘漏洞
linux·运维·服务器
松涛和鸣3 小时前
从零开始理解 C 语言函数指针与回调机制
linux·c语言·开发语言·嵌入式硬件·排序算法
皮小白4 小时前
ubuntu开机检查磁盘失败进入应急模式如何修复
linux·运维·ubuntu
邂逅星河浪漫4 小时前
【CentOS】虚拟机网卡IP地址修改步骤
linux·运维·centos
hhwyqwqhhwy4 小时前
linux 驱动开发相关
linux·驱动开发
IT逆夜5 小时前
实现Yum本地仓库自动同步的完整方案(CentOS 7)
linux·运维·windows
S***26755 小时前
linux上redis升级
linux·运维·redis
赖small强5 小时前
【Linux 网络基础】Linux 平台 DHCP 运作原理与握手过程详解
linux·网络·dhcp
s***4537 小时前
Linux 下安装 Golang环境
linux·运维·golang