百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 优雅编程 > 正文

Linux系统多线程应用API详解(一)---线程的创建和销毁

sinye56 2024-11-06 12:01 5 浏览 0 评论

一、线程介绍:

1. 进程是资源管理的基本单元,而线程是系统调度的基本单元, 线程是操作系统能够进行调度运算的最小单位,它被包含在进程之中.

2. 一个完整的线程/进程包括三部分,代码+数据+内存栈. 子线程和子进程在被创建的时候,对于fork()创建子进程, 三部分都要复制一份,数据包括比如文件描述符, 虚拟内存, 子进程关闭文件描述符不会影响父进程中的描述符; 对于pthread_create()创建子线程的时候,只有内存栈被复制,其他的部分(代码,数据都是共享的), 如果一个线程改变了某变量的值, 其他所有的线程都调用的是改变之后的值;

3. 所以在Linux系统中,创建多线程是一种非常重要且常见的并发编程技术, 可以实现多个任务的同时执行,提高程序的性能和效率。

4. 编程涉及到的内容:

数据类型:pthread_t, pthread_attr_t, pthread_cond_t, pthread_mutexattr_t, void* (*)(void*),

头文件: #include <pthread.h>

编译参数: -lpthread

下面将详细介绍在Linux环境下与多线程编程开发相关的一些API接口。

二、线程的创建和销毁:

---pthread_create. 线程的创建:

int pthread_create(pthread_t *tid, const pthread_arrt_t* attr,

void*(*start_routine)(void *), void* arg);

1)说明:此函数用于线程的创建,实际上就是确定调用该线程函数的入口点,在线程创建以后,就开始运行相关的线程函数;

2)参数:

---tid指向线程标识符的指针,由函数创建成功后填充此值;

---attr:用于手动设置新建线程的属性,例如线程的调用策略、线程所能使用的栈内存的大小等。

通常情况下将 attr 参数赋值为 NULL,函数会采用系统默认的属性值创建线程。

---void *(*start_routine) (void *):以函数指针的方式指明新建线程需要执行的函数,

使用此类型指针时,我们自定义的函数格式(参数和返回值)要与函数指针保持一致;

---void *arg:线程运行函数的参数;

---返回值:如果成功创建线程,函数返回数字0,反之返回非零值,有以下几种情况:

EAGAIN:系统资源不足,无法提供创建线程所需的资源。

EINVAL:传递给 pthread_create() 函数的 attr 参数无效。

EPERM:传递给 pthread_create() 函数的 attr 参数中,某些属性的设置为非法操作,没有相关的设置权限。

注:以上这些宏都声明在 <errno.h> 头文件中,需提前引入此头文件。

3)例子:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <errno.h>
			
void *thread_func(void *lpvoid)
{
	int val = *((int *)lpvoid);
	printids("new thread! get param:%d. \n", val);
	return NULL;
}
			 
int main(int argc, char **argv)
{
	int err;
	pthread_t threadId;
	int   val = 100;
	err = pthread_create(&threadId, NULL, thread_func, (void*)&val);
	if (err != 0)
		printf("can't create thread: %s\n", strerror(err));
	pthread_join (threadId, NULL);
	return EXIT_SUCCESS;
}		

---pthread_join. 一个线程等待另一个线程结束

int pthread_join(pthread_t thread, void **retval);

1)说明: 等待子线程的结束,非分离的线程在结束后只有执行join才会释放全部资源。 在很多情况下,主线程生成并启动一个子线程,用来处理一些耗时的工作,所以主线程往往会在子线程之前结束,

另外如果主线程需要获取子线程的处理结果, 也会等待子线程执行完成之后再结束,这种场景就要用到pthread_join()方法。

2)参数:

--pthread_t thread: 被连接线程的线程号

--void **retval : 指向一个指向被连接线程的返回码的指针的指针;具体值由函数来填充;

--返回值:线程连接的状态,0是成功,非0是失败。

3)例子:

a. 如果我们在调用pthread_create()函数的时候将属性设置为NULL,则表明我们希望所创建的线程采用默认的属性,也就是joinable。

		-------------------例子1:非分离线程------------
		pthread_t tid;
		pthread_create(&tid, NULL, thread_run, NULL);
		/*创建线程之后直接调用pthread_join方法等待thread_run子线程的结束。*/
		pthread_join(tid,NULL);

b.如果需要将属性设置为detached, 在线程设置为joinable后,可以调用pthread_detach()使之成为detached。但是反相操作则不可以。

如果线程已经调用pthread_join( )后,则再调用pthread_detach( )则不会有任何效果。

	-------------------例子2:分离线程------------
	  void* start_run(void* arg)
	  {
		//dosomework
	  }
 
	 int main()
	 {
		pthread_t thread_id;
		pthread_attr_t attr;
		pthread_attr_init(&attr);
		pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
		pthread_create(&thread_id,&attr,start_run,NULL);
		pthread_attr_destroy(&attr);
		sleep(5);				
		///!!不需要调用这个函数
		//pthread_join (threadId, NULL);				 
		exit(0);
	 }
			

4)补充说明:

a. 子线程中被释放的内存空间仅仅是系统空间,你必须手动清除程序分配的用户空间,比如 malloc() 分配的空间。

b. 一个线程只能被一个线程所连接join;

c. 被连接的线程必须是非分离的,否则连接会出错。

所以可以看出pthread_join()有两种作用:

(1).用于等待其他线程结束:当调用此函数时,当前线程会处于阻塞状态,直到被调用的线程结束后;

(2).对线程的资源进行回收:如果一个线程是非分离的且没有调用此等待函数,线程结束后并不会释放其内存空间, 这会导致该线程变成了“僵尸线程”。


---pthread_self.获取线程的pid

pthread_t pthread_self(void);

1)说明: 新建线程在后续运行中如果想要获取自身的线程ID,可以通过Pthread库提供的pthread_self()函数来返回。此值即为

pthread_create函数新建线程中指向的线程ID;

2)参数:

---返回值:pthread_create函数新建线程中指向的线程ID;

3)例子:

pthread_mutex_t mutex;
void* threadFunc(void* obj)
{
	pthread_mutex_lock(&mutex);
	pthread_t  thread_id =  pthread_self(); 
	printf("2---子线程ID为:%d\n", thread_id);
	pthread_mutex_unlock(&mutex);
	return nullptr;
}

int main(int argc,char* argv[])
{
	pthread_t threadId;
	pthread_create(&threadId, nullptr,&threadFunc,nullptr);
	pthread_mutex_lock(&mutex);
	printf("1---子线程ID为:%d\n", threadId);
	pthread_mutex_unlock(&mutex);
	pthread_join(threadId, NULL);
	return 0;
}		

---pthread_detach.线程分离:

int pthread_detach(pthread_t thread);

1)说明: 线程设置为分离, 当此线程退出时, 不会向任何其他线程传递返回值, 分离的线程无法被 pthread_join();

2)参数:

---thread:需要分离的线程ID

---返回值:成功:0;失败:错误号。

3)例子:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void* threadFunction(void* arg) {
	printf("Thread started\n");
	sleep(5);
	printf("Thread finished\n");
	return NULL;
}

int main() {
	pthread_t thread;
	pthread_create(&thread, NULL, threadFunction, NULL);
	// 将子线程标记为分离状态
	pthread_detach(thread);
	printf("Main thread finished\n");
	return 0;
}
 		

4)注意:

a. 线程分离状态:指定该状态,线程主动与主控线程断开关系。线程结束后,其退出状态不由其他线程获取, 而直接自己自动释放。网络、多线程服务器常用。

b. 不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL错误


---pthread_exit.线程终止函数。

int pthread_exit(void* value_ptr);

1)说明:用于在线程内部显式地退出线程并返回终止状态。

2)参数:

---value_ptr:是一个指向线程的返回值的指针。它可以用于向等待该线程的其他线程传递一个返回值。 如果不需要传递返回值,可以将value_ptr设置为NULL。 当调用pthread_exit()时,当前线程会立即终止,并将value_ptr指向的值作为线程的终止状态传递给等待该线程的其他线程。 如果不等待线程退出,它的终止状态将被丢弃。

3)例子:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

void* threadFunction(void* arg) {
	/*传入的参数作为返回值*/
	int* value_ptr = (int*)arg; 
	printf("Thread function executed\n");

	// 结束线程,并返回传入的值
	pthread_exit((void*)value_ptr);
}

int main() {
	pthread_t thread;
	int retVal = 42;
			
	pthread_create(&thread, NULL, threadFunction, (void*)&retVal);

	// 等待子线程结束,并获取其返回值
	void* exitStatus;
	pthread_join(thread, &exitStatus);
	int* threadRetVal = (int*)exitStatus;
			
	// 输出子线程的返回值
	printf("Thread exit status: %d\n", *threadRetVal);
	return 0;
}
	

---pthread_cancel.线程取消:

int pthread_cancel (pthread_t __th);

1)说明:函数是一个用于取消线程执行的函数,它允许一个线程(调用者线程)请求取消另一个线程的执行。 线程的取消并不是实时的,会有一定的延时。线程的取消要有取消点才可以,不是说取消就取消,线程的取消点主要是阻塞的系统调用。

2)参数:

---thread:线程ID。

---返回值:成功:0;失败:错误号。

3)使用条件:

a. 接收到取消请求的目标线程可以决定是否允许被取消以及如何取消,这分别由如下两个函数完成:

--int pthread_setcancelstate (int __state, int *__oldstate);

-- int pthread_setcanceltype (int __type, int *__oldtype);

b. 这两个函数的第一个参数分别用于设置线程的取消状态(是否允许取消)和取消类型(如何取消),

第二个参数则分别记录线程原来的取消状态和取消类型。

c. state 参数有两个可选值:

--PTHREAD_CANCEL_ENABLE,允许线程被取消。它是线程被创建时的默认取消状态。

--PTHREAD_CANCEL_DISABLE,禁止线程被取消。这种情况下,如果一个线程收到取消请求,则它会将请求挂起,直到该线程允许被取消。

d. t ype参数也有两个可选值:

--PTHREAD_CANCEL_ASYNCHRONOUS,线程随时都可以被取消。它将使得接收到取消请求的目标线程立即采取行动。

--PTHREAD_CANCEL_DEFERRED, 允许目标线程推迟行动,直到它调用了下面几个所谓的取消点函数中的一个: pthread_join、 pthread_testcancel、 pthread_cond_wait、pthread_cond_timedwait、sem_wait 和sigwait。

e. 其它的取消点:

根据POSIX标准,其他可能阻塞的系统调用,比如read、wait也可以成为取消点。不过为了安全起见,我们最好在可能会被取消的代码中调用pthread_testcancel 函数以设置取消点pthread_setcancelstate和pthread_setcanceltype 成功时返回0,失败则返回错误码。

4)例子:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void* threadFunction(void* arg) 
{
	// 设置取消状态为允许取消
	pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);

	// 设置取消类型为异步取消
	pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
	printf("Thread started\n");

	while (1) {
		printf("Thread is running\n");
		sleep(1);
	}

	printf("Thread finished\n");
	return NULL;
}

////////////////////MAIN FUNCTION ///////////////////////////////
int main() 
{
	pthread_t thread;
	pthread_create(&thread, NULL, threadFunction, NULL);

	// 主线程休眠5秒后取消子线程
	sleep(5);
	pthread_cancel(thread);

	// 等待子线程结束
	pthread_join(thread, NULL);
	printf("Main thread finished\n");

	return 0;
}

---pthread_testcancel .设置线程取消点:

void pthread_testcancel (void);

1)说明:上面提到,被终止的线程必须要有取消点,如果没有取消点,这里可以自己手动设置一个取消点,这样的话无需 sleep等这样的阻塞函数也可以实现线程取消了。

2)参数:无

3)例子:

void *thread_run(void *arg)
{
	printf("This is child thread\n");
	pthread_setcancelstate(PTHREAD_CANCEL_DISABLE,NULL);    //禁止取消
	// while(1)
	{
		sleep(5);
		pthread_testcancel();			         //设置取消点
	}
	pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,NULL);	   //使能取消
	while(1){
	      sleep(1);
	}			 
			 
	pthread_exit("thread return");
}
 
 
int main()
{
	pthread_t tid;
	pthread_create(&tid, NULL, thread_run, NULL);
			 
	pthread_cancel(tid);        // 取消子线程
}


---.检测线程是否退出:

int pthread_tryjoin_np (pthread_t threadId, void **__thread_return);

1)说明:

函数与线程线程执行非阻塞连接,并在*retval中返回线程的退出状态。如果线程尚未终止,则调用将返回错误,而不是像pthread_join(3)那样阻塞。

2)参数:与pthread_join参数一样;

3)例子:使用方法与 pthread_joing 一样,注意区别是非阻塞的。

       /* 创建新线程*/
	pthread_t t_id = -1;
	pthread_create( &t_id, NULL, func, NULL);  54 
 
	/* 取消线程请求 */
	sleep(1);
	pthread_cancel(t_id);
     
	/* 取消线程后,看看能否再结合。(结果是不能结合)*/
	int *retval;
	/* tryjoin不阻塞,错误直接返回*/
	int ret_tryjoin = pthread_tryjoin_np(t_id, (void *)&retval); 
	if(ret_tryjoin)
	{
               fprintf(stderr, "线程被取消,结合失败!\n返回值:%s\n", strerror(ret_tryjoin) );
	} 
    	else 
	{
		printf("线程结合,func 退出值:%d\n", *retval);
        }

---.线程等待结束:

int pthread_timedjoin_np (pthread_t threadId, void **thread_return, const struct timespec *__abstime);

1)说明:

函数执行超时联接。如果线程尚未终止,则调用将阻塞直到abstime中指定的最大时间。

如果超时在线程终止之前到期,则调用将返回错误。

2)参数:

---threadId: 线程ID号;

---thread_return: 线程退出时的返回值;

---abstime:是struct timespec形式的结构体,用于指定自大纪元以来测量的绝对时间。

---返回值:成功时,这些函数返回0;否则,返回0。错误时,它们返回错误号。

3)例子:

	struct timespec ts;
	int s;
	...
	if (clock_gettime(CLOCK_REALTIME, &ts) == -1) {
		/* Handle error */
	}

	ts.tv_sec += 5;

	s = pthread_timedjoin_np(thread, NULL, &ts);
	if (s != 0) {
		/* Handle error */
	}

---.线程的清理:

void pthread_cleanup_push(void (*routine) (void *), void *arg)

void pthread_cleanup_pop(int execute)

1)说明:当线程非正常终止,需要清理一些资源。

routine 函数被执行的条件:

a. 被pthread_cancel取消掉。

b. 执行pthread_exit

c. 非0参数执行pthread_cleanup_pop()

2)参数:

---void (*routine) (void *):执行退出前的清理函数;

---arg: 清理函数所带的参数;

---execute: 当为1时执行,为0时不执行;

3)例子

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
void cleanup(void *arg){
	printf("cleanup,arg=%s\n",(char*)arg);		 
}
void *func(void *arg){
	printf("This is child thread\n");
	pthread_cleanup_push(cleanup, "abcd");
	pthread_exit("thread return");
	while(1){    
		sleep(1);
	}
	pthread_cleanup_pop(0);
}		 
		 
int main(){
	pthread_t tid;
	void *retv;
	int i;
	pthread_create(&tid, NULL, func, NULL);
	sleep(1);
	pthread_cancel(tid);
	pthread_join(tid,&retv);
	//printf("thread ret=%s\n",(char*)retv);
	while(1){    
		sleep(1);
	} 			
}		

4)注意事项:

a. 必须成对使用,即使pthread_cleanup_pop不会被执行到也必须写上,否则编译错误。

b. pthread_cleanup_pop()被执行且参数为0,pthread_cleanup_push回调函数routine不会被执行.

c. pthread_cleanup_push 和pthread_cleanup_pop可以写多对,routine执行顺序正好相反

d. 线程内的return可以结束线程,也可以给pthread_join返回值,但不能触发pthread_cleanup_push里面的回调函数,所以我们结束线程尽量使用pthread_exit退出线程。

---.线程比较:

int pthread_equal (pthread_t __thread1, pthread_t __thread2);

1)说明:比较两个线程ID是否相等。

2)参数:

__thread1: 其中一个线程的ID号;

__thread2: 待比较的线程ID号;

---返回值:0--不相等;非0--相等;

3)例子:

	pthread_t  thread1, thread2; 
	pthread_create(&thread1, NULL, func, NULL);
	pthread_create(&thread2, NULL, func, NULL);	
	int ret = pthread_equal(thread1, thread2);
        if(ret)
	       printf ("The to threads ID is equal! \n");
	else 
	       printf ("The to threads ID is not equal! \n");	

---.创建/ 删除线程的私有数据

int pthread_key_create(pthread_key_t *key, void(*destructor)(void*));

int pthread_key_delete(pthread_key_t key);

1)说明: 这个函数的作用主要是创建一个全局的,所有线程可见的一个 key ,然后所有的线程可以通过这个 key 创建一个线程私有的数据,并且我们可以设置一个析构函数 .

2)参数:

---key: 为指向一个键值的指针;

---destructor:指明了一个destructor函数,如果这个参数不为空,那么当每个线程结束时,系统将调用这个函数来释放绑定在这个键上的内存块。

3)例子:

	#include <stdio.h>
	#include <pthread.h>
	#include <stdlib.h>
	#include <unistd.h>
	pthread_key_t key;
			
	void key_destructor1(void* arg)
	{
		printf("arg = %d thread id = %lu\n", *(int*)arg, pthread_self());
		free(arg);
	}
			
	void thread_local()
	{
		int* q = pthread_getspecific(key);
		printf("q == %d thread id = %lu\n", *q, pthread_self());
	}
			
	void* thread_func1(void* arg)
	{
		printf("Entering thread_func1\n");
		int* s = malloc(sizeof(int));
		*s = 100;
				
		pthread_setspecific(key, s);
		thread_local();
		printf("Leaving thread_func1\n");
		return NULL;
	}
			
	void* thread_func2(void* arg)
	{
		printf("Entering thread_func2\n");
		int* s = malloc(sizeof(int));
		*s = -100;
				
		pthread_setspecific(key, s);
		thread_local();
		printf("Leaving thread_func2\n");
		return NULL;
	}
			
	/*------------main function */
	int main(int argc, char **argv) 
        {
		pthread_key_create(&key, key_destructor1);
		pthread_t t1, t2;
		pthread_create(&t1, NULL, thread_func1, NULL);
		pthread_create(&t2, NULL, thread_func2, NULL);
		pthread_join(t1, NULL);
		pthread_join(t2, NULL);
				
		pthread_key_delete(key);
		return 0;
	}
	 		

---.设置/获取对应key的具体数据

int pthread_setspecific(pthread_key_t key, const void * value);

void * pthread_getspecific(pthread_key_t key);

1)说明: 通过这两个函数可以设置/获取对应 key 的具体的数据。

2)参数:

---key: 为前面函数pthread_key_create创建的pthread_key_t变量;

---value: 这是一个void*无类型变量,用来存储任何类型的值。

3)例子:(具体使用方法见上面例子)


(下一篇介绍线程相关的属性设置API接口)

相关推荐

Linux两种光驱自动挂载的方法

环境:CentOS6.4西昆云服务器方式一修改fstab文件/etc/fstab是系统保存文件系统信息?静态文件,每一行描述一个文件系统;系统每次启动会读取此文件信息以确定需要挂载哪些文件系统。参...

linux系统运维,挂载和分区概念太难?在虚机下操作一次全掌握

虚拟机的好处就是可以模拟和学习生产环境的一切操作,假如我们还不熟悉磁盘操作,那先在虚机环境下多操作几次。这次来练习下硬盘扩容操作。虚拟机环境:centos8vm11linux设备命名规则在linux中...

Linux 挂载 NFS 外部存储 (mount 和 /etc/fstab)

mount:手工挂载,下次重启需再重新挂载,操作命令:mount-tnfs-ooptionsserver:/remote/export/local/directory上面命令中,本地目录...

在Linux中如何设置自动挂载特定文件系统(示例)

Linux...

Linux环境中的绑定挂载(bind mount)

简介:Linux中的mount命令是一个特殊的指令,主要用于挂载文件目录。而绑定挂载(bindmount)命令更为特别。mount的bind选项将第一个目录克隆到第二个。一个目录中的改变将会在...

Linux挂载CIFS共享 临时挂载 1. 首先

如何解决服务器存储空间不足的问题?大家好,欢迎回来。在上一期视频中,我为大家介绍了如何利用Linux挂载来扩容服务器存储空间。这一期视频,我将以Linux为例,教大家如何进行扩容。群辉使用的是Linu...

Linux 硬盘挂载(服务器重启自动挂载)

1、先查看目前机器上有几块硬盘,及已挂载磁盘:fdisk-l能够查看到当前主机上已连接上的磁盘,以及已经分割的磁盘分区。(下面以/dev/vdb磁盘进行分区、挂载为例,挂载点设置为/data)df...

linux 挂载磁盘

在Linux中挂载硬盘的步骤如下:...

笨小猪教您Linux磁盘挂载

本教程针对Linux系统比较熟悉或者想学习Linux基础的用户朋友,本教程操作起来比较傻瓜式,跟着步骤就会操作,本文使用的工具是XShell同时多多注意空格(文中会有提示)。【问答】什么是磁盘挂载?答...

Linux 磁盘挂载和docker安装命令

本篇给大家介绍Linux磁盘挂载和docker安装的相关内容,Linux服务器的操作是一个手熟的过程,一些不常用的命令隔断时间就忘记了,熟话说好记性不如烂笔头,还需在平时的工作中多练习记录。...

Linux设置开机自动挂载分区

有时候,我们在安装完Linux系统之后,可能在使用过程中添加硬盘或者分区进行使用,这时候就需要手动把磁盘分区挂载到某个路径,但是开机之后就会消失,需要重新挂载,非常麻烦,那么我们应该如何设置开机自动挂...

在linux挂载一个新硬盘的完整步骤

以下是在Linux中挂载新原始磁盘的完整步骤,包括分区、创建文件系统以及使用UUID在/etc/fstab中启动时挂载磁盘:将新的原始磁盘连接到Linux系统并打开电源。运行以下命令,...

Linux系统如何挂载exFAT分区

简介:Linux系统中不能像Windows系统那样自动识别加载新设备,需要手动识别,手动加载。Linux中一切皆文件。文件通过一个很大的文件树来组织,文件树的根目录是:/,从根目开始录逐级展开。这些文...

Linux系统挂载硬盘

fdisk-l查看可挂载的磁盘都有哪些df-h查看已经挂载的磁盘...

WSL2发布,如何在Win10中挂载Linux文件系统

WSL2是最新版本的架构,它为Windows子系统提供支持,使其能够在Windows上运行ELF64Linux二进制文件。通过最近的更新,它允许使用Linux文件系统访问存储在硬盘中的文件。如果你...

取消回复欢迎 发表评论: