Linux多线程编程:pthread线程创建、退出、回收、分离、取消

Linux线程

1、简单了解一下线程

  线程也被称为轻量级进程,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,因为进程切换时需要更新cache和tlb,而线程就不用。由于多个线程访问的都是同一地址空间,所以线程间通信可以通过全局变量,但最好需要需要加入同步或互斥等机制来确保数据访问的安全性,本篇文章探讨线程的创建、退出、回收、取消、分离等函数,此外,pthread并非Linux系统的默认库,而是POSIX线程库,所以编译时需要加上-pthread

2、线程创建:pthread_create

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
				   void *(*start_routine) (void *), void *arg);

参数解析

参数 说明
thread 传出参数,用于获得线程的ID
attr 设置线程的属性,通常为默认属性NULL
start_routine 函数指针,线程开始执行的函数
arg 传递给线程的参数

看下例子就明白了:

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


struct test{
    
    
	int i;
};

void *mypthreadFunction1(void *pvoid)
{
    
    
	int i = 0;
	
	while(1)
	{
    
    
		printf("thread function 1, i: %d\n", i++);
		sleep(1);
	}
}

void *mypthreadFunction2(void *pvoid)
{
    
    
	int i = *((int*)pvoid);
	
	while(1)
	{
    
    
		printf("thread function 2, i: %d\n", i++);
		sleep(1);
	}
}

void *mypthreadFunction3(void *pvoid)
{
    
    
	struct test *pt = (struct test *)pvoid;
	
	while(1)
	{
    
    
		printf("thread function 3, i: %d\n", (pt->i)++);
		sleep(1);
	}
}

int main()
{
    
    
	int i = 10;
	pthread_t tid[3];
	struct test *pt = malloc(sizeof(struct test));
	pt->i = 100;
	
	pthread_create(&tid[0], NULL, mypthreadFunction1, NULL);
	pthread_create(&tid[1], NULL, mypthreadFunction2, (void *)&i);// 传变量地址
	pthread_create(&tid[2], NULL, mypthreadFunction3, (void *)pt);// 传结构体地址,还可以传函数地址等
	
	while(1)
	{
    
    
		sleep(1);
	}
	
	return 0;
}

输出结果:

thread function 1, i: 0
thread function 2, i: 10
thread function 3, i: 100
thread function 1, i: 1
thread function 2, i: 11
thread function 3, i: 101
thread function 3, i: 102
thread function 2, i: 12
thread function 1, i: 2
thread function 2, i: 13
thread function 1, i: 3
thread function 3, i: 103
^C

可以看到,线程不是严格按顺序打印的,是因为线程之间存在资源竞争调度等,不过可以使用线程同步实现顺序输出。

3、线程传参注意事项

  《UNIX环境高级编程》第三版中说到线程:一个进程的所有信息对该进程的所有线程都是共享的,包括可执行程序的代码、程序的全局内存和堆内存、栈以及文件描述符。现在我们回到上面创建第2个线程的这里,由于前面说栈是共享的,但栈帧是独立的呀,也就是说函数之间不能直接访问内部的变量,所以我们需要把main函数里面的局部自动变量 i 作为线程2的参数传递进去,线程2再读出它的值。上面示例中只是读取它的值就没有使用其他操作了,读取它的值倒不会造成严重的影响,问题就在于如果我们在线程2使用它的地址进行一些操作又会是怎样的呢?看以下例子:

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


void *mypthreadFunction(void *pvoid)
{
    
    
        int *i = (int*)pvoid;
        int j = 0;

        // 此时i指向的main函数的栈帧
        printf("thread i addr: %p, val: %d\n", i, *i);

        while(j < 10)
        {
    
    
                printf("thread function, i: %d\n", (*i)++);
                j++;
                sleep(1);
        }
}

int main()
{
    
    
        int i = 10;
        int j = 0;
        pthread_t tid;

        printf("-main- i addr: %p, val: %d\n", &i, i);
        pthread_create(&tid, NULL, mypthreadFunction, (void *)&i);// 传局部自动变量地址

        while(j < 6)
        {
    
    
                printf("-main- function, i: %d\n", i);
                j++;
                sleep(1);
        }
        printf("-main- function exit!\n");
        pthread_exit(NULL);
}

输出结果:

-main- i addr: 0x7fff81066ab8, val: 10
-main- function, i: 10
thread i addr: 0x7fff81066ab8, val: 10
thread function, i: 10
-main- function, i: 11
thread function, i: 11
-main- function, i: 12
thread function, i: 12
-main- function, i: 13
thread function, i: 13
-main- function, i: 14
thread function, i: 14
-main- function, i: 15
thread function, i: 15
-main- function exit!
thread function, i: 16
thread function, i: 0
thread function, i: 1
thread function, i: 2

  可以看到,线程函数里面可以操作到main函数的栈帧喔。需要知道线程函数和main函数不是调用关系,他们是同级别的线程函数。把栈地址传递是很危险的,可以看到上面的例子,线程里面改变了 i 的值,main函数里面也跟着改变,而main函数结束之后 i 被释放了(值变为0),而线程还在操作刚才的地址,而且如果下一次其他函数分配的变量又是在这个地址呢?是不是很危险、很难把握这个地址的值什么时候被改变了?这种情况可以概括为:在C/C++中,使用指针能够做到把线程A的栈地址传递给线程B去操作。这种做法其实是很危险的,给线程传参的时候需要考虑到栈内存的特殊性

4、线程退出:pthread_exit

void pthread_exit(void *value_ptr);

  线程内部调用该函数用于退出当前线程,参数value_ptr是用于存储线程的退出值传递出去,也可以设为NULL,调用接下来的pthread_join回收线程就可以传出到参数void **value_ptr中。

区分

函数/关键字 作用
continue 退出本次循环进入下一轮循环
break 退出该层循环
exit 退出当前进程
return 结束函数,返回到调用者
pthread_exit 退出当前线程

5、线程回收:pthread_join

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

  阻塞等待id为thread的线程退出,并且可以通过传出参数value_ptr获取线程退出状态,如果不需要查看线程退出状态也可以设为NULL,可以类比于进程中的wait/waitpid()函数,这里用于回收线程资源。

结合线程退出与回收查看例子:

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

void *mypthreadFunction1(void *pvoid)
{
    
    
	pthread_exit("mypthreadFunction1 exit!");
}

void *mypthreadFunction2(void *pvoid)
{
    
    
	static int i = 0;

	while(1)
	{
    
    
		printf("thread function 2, i: %d\n", i++);
		sleep(1);
		if(i == 2)
		{
    
    
			pthread_exit((void*)&i);
		}
	}
}

int main()
{
    
    
	int *reVal;
	char *str;
	pthread_t tid1;
	pthread_t tid2;

	pthread_create(&tid1, NULL, mypthreadFunction1, NULL);
	pthread_create(&tid2, NULL, mypthreadFunction2, NULL);

	pthread_join(tid1, (void **)&str);
	printf("thread1 return a str: %s\n", str);

	pthread_join(tid2, (void **)&reVal);
	printf("thread2 return a value: %d\n",  *reVal);

	return 0;
}

输出结果:

thread function 2, i: 0
thread1 return a str: mypthreadFunction1 exit!
thread function 2, i: 1
thread2 return a value: 2

6、线程分离:pthread_detach

int pthread_detach(pthread_t thread);

  POSIX线程的一个特点是:除非线程是被分离了的,否则在线程退出时,它的资源是不会被释放的,需要调用pthread_join回收(注意:进程结束后,占用的资源全部会被释放的)。
  调用该函数分离线程,子线程主动与主线程断开关系,线程结束后,其退出状态不能由其他线程获取,而自己自动释放资源,多用于网络、多线程服务器。
  返回值为0成功或非零错误码。

7、线程取消:pthread_cancel

int pthread_cancel(pthread_t thread);

  参数 thread 是要取消的目标线程的线程 ID。该函数并不阻塞调用线程,它发出取消请求后就返回了。类比于进程的kill函数,但是不同的是,调用函数时,并非一定能取消掉该线程,因为这个函数需要线程进到内核时才会被杀掉,所以线程如果一直运行于用户空间,就没有契机进入内核也就无法取消(例while(1){}空循环),此时可以使用pthread_testcancel();进入内核。
  如果成功,pthread_cancel 返回 0,如果不成功,pthread_cancel 返回一个非零的错误码。
  如果线程处于PTHREAD_CANCEL_ENABLE 状态,它就接受取消请求,如果线程处于 PTHREAD_CANCEL_DISABLE状态,取消请求就会被保持在挂起状态。默认情况下,线程处于 PTHREAD_CANCEL_ENABLE状态。

结合线程分离和取消查看例子:

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


void *mypthreadFunction(void *pvoid)
{
    
    
	int i = 0;

	while(1)
	{
    
    
		printf("thread function, i: %d\n", i++);
		sleep(1);
	}
}


int main()
{
    
    
	pthread_t tid;

	pthread_create(&tid, NULL, mypthreadFunction, NULL);
	pthread_detach(tid);	// 将线程分离

	// 2秒钟之后取消线程
	sleep(2);
	pthread_cancel(tid);
	printf("had cancel the thread!\n");

	return 0;
}

运行结果:

thread function, i: 0
thread function, i: 1
had cancel the thread!

8、线程其他函数

获得线程自身ID

pthread_t pthread_self(void);

线程属性初始化与关闭

int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);

  函数 pthread_attr_init 用默认值对一个线程属性对象进行初始化。
  函数pthread_attr_destroy函数将属性对象的值设为无效的。被设为无效的属性对象可以再次被初始化为一个新的属性对象。
  pthread_attr_init 和 pthread_attr_destroy 都只有一个参数,即一个指向属性对象的指针。
线程属性状态

int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

  线程状态的可能取值为PTHREAD_CREATE_JOINABLE和PTHREAD_CREATE_DETACHED 。pthread_attr_getdetachstate函数用来查看一个属性对象中的线程状态。而pthread_attr_setdetachstate函数用来设置一个属性对象中的线程状态。
线程比较

int pthread_equal(pthread_t t1, pthread_t t2);

  两个参数 t1 和 t2 是两个线程 ID,如果它们相等,pthread_equal 就返回一个非零值,如果不相等,则返回 0。
改变线程取消状态

int pthread_setcancelstate(int state, int *oldstate);

  参数 state 表示要设置的新状态,参数 oldstate 为一个指向整形的指针,用于保存线程以前的状态。如果成功,该函数返回 0,如果不成功,它返回一个非 0 的错误码。
改变线程取消类型

int pthread_setcanceltype(int type, int *oldtype);

  当线程将退出作为对取消请求的响应时,取消类型允许线程控制它在什么地方退出。当它的取消类型为 PTHREAD_CANCEL_ASYNCHRONOUS 时,线程在任何时候都可以响应取消请求。当它的取消类型为 PTHREAD_CANCEL_DEFERRED 时,线程只能在特定的几个取消点上响应取消请求。在默认情况下,线程的类型为 PTHREAD_CANCEL_DEFERRED。
  参数 type 指定线程的取消类型,参数 oldtype 用来指定保存原来的取消类型的地址。如果成功,该函数返回 0,如果不成功,它返回一个非 0 的错误码。
  线程还有很多其他函数,这里不一一列举。

9、线程注意事项

  1. 其他线程保留,主线程退出用pthread_exit;
  2. 避免僵尸线程考虑使用:
      pthread_join
      pthread_detach
      pthread_create指定分离属性;
  3. malloc和mmap的内存其他线程可以使用;
  4. 应多避免多线程调用fork,除非马上exec,子进程中只有调用fork的线程存在,其他线程都已退出;
  5. 信号的复杂语义很难和多线程共存,应避免多线程引入信号机制,因为信号来了哪个线程先抢到就是它处理,各线程有独立的mask屏蔽字,处理方式是共享的,很难指定哪个线程处理。

猜你喜欢

转载自blog.csdn.net/weixin_44498318/article/details/105916311