0、代码总结
线程创建
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
// Compile and link with -pthread, 线程库的名字叫pthread, 全名: libpthread.so libptread.a
//创建线程
pthread_t tid;
pthread_create(&tid, NULL, working, NULL);
线程退出
#include <pthread.h>
void pthread_exit(void *retval);
//线程退出
pthread_exit(NULL);
线程回收
#include <pthread.h>
// 这是一个阻塞函数, 子线程在运行这个函数就 阻塞
// 子线程退出, 函数解除阻塞, 回收对应的子线程资源, 类似于回收进程使用的函数 wait()
int pthread_join(pthread_t thread, void **retval);
/
pthread_t tid;
pthread_create(&tid, NULL, working, NULL);
void* ptr = NULL;
// ptr是一个传出参数, 在函数内部让这个指针指向一块有效内存
// 这个内存地址就是pthread_exit() 参数指向的内存
pthread_join(tid, &ptr);
线程分离
#include <pthread.h>
// 参数就子线程的线程ID, 主线程就可以和这个子线程分离了
int pthread_detach(pthread_t thread);
pthread_t tid;
pthread_create(&tid, NULL, working, NULL);
pthread_detach(tid); 设置子线程和主线程分离
// 让主线程自己退出即可
pthread_exit(NULL);
一、多线程的创建
每一个线程都有一个唯一的线程ID,ID类型为pthread_t,这个ID是一个无符号长整形数,如果想要得到当前线程的线程ID,可以调用如下函数:
pthread_t pthread_self(void); // 返回当前线程的线程ID
在一个进程中调用线程创建函数,就可得到一个子线程,和进程不同,需要给每一个创建出的线程指定一个处理函数,否则这个线程无法工作。
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
// Compile and link with -pthread, 线程库的名字叫pthread, 全名: libpthread.so libptread.a
参数:
thread: 传出参数,是无符号长整形数,线程创建成功, 会将线程ID写入到这个指针指向的内存中
attr: 线程的属性, 一般情况下使用默认属性即可, 写NULL
start_routine: 函数指针,创建出的子线程的处理动作,也就是该函数在子线程中执行。
arg: 作为实参传递到 start_routine 指针指向的函数内部
返回值:线程创建成功返回0,创建失败返回对应的错误号
// pthread_create.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
// 子线程的处理代码
void* working(void* arg)
{
printf("我是子线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<9; ++i)
{
printf("child == i: = %d\n", i);
}
return NULL;
}
int main()
{
// 1. 创建一个子线程
pthread_t tid; ///
pthread_create(&tid, NULL, working, NULL); //
printf("子线程创建成功, 线程ID: %ld\n", tid);
// 2. 子线程不会执行下边的代码, 主线程执行
printf("我是主线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<3; ++i)
{
printf("i = %d\n", i);
}
// 休息, 休息一会儿...
// sleep(1);
return 0;
}
$ gcc pthread_create.c -lpthread 编译代码
二、线程退出函数
在编写多线程程序的时候,如果想要让线程退出,但是不会导致虚拟地址空间的释放(针对于主线程),我们就可以调用线程库中的线程退出函数,只要调用该函数当前线程就马上退出了,并且不会影响到其他线程的正常运行,不管是在子线程或者主线程中都可以使用。
#include <pthread.h>
void pthread_exit(void *retval);
参数: 线程退出的时候携带的数据,当前子线程的主线程会得到该数据。如果不需要使用,指定为NULL
示例代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
// 子线程的处理代码
void* working(void* arg)
{
sleep(1);
printf("我是子线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<9; ++i)
{
if(i==6)
{
pthread_exit(NULL); // 直接退出子线程 /
}
printf("child == i: = %d\n", i);
}
return NULL;
}
int main()
{
// 1. 创建一个子线程
pthread_t tid;
pthread_create(&tid, NULL, working, NULL);
printf("子线程创建成功, 线程ID: %ld\n", tid);
// 2. 子线程不会执行下边的代码, 主线程执行
printf("我是主线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<3; ++i)
{
printf("i = %d\n", i);
}
// 主线程调用退出函数退出, 地址空间不会被释放
pthread_exit(NULL); ///
return 0;
}
三、线程回收函数
在线程编程中,线程回收是指对已经结束执行的线程进行资源的释放和清理操作。它的作用主要有以下几点:
1.释放内存资源 防止内存泄漏
2.清理资源句柄 如文件句柄等,防止资源泄露
3.避免僵尸线程
4.释放锁资源 如果线程执行过程中获得了锁但是退出时没有释放锁 导致内存一直被占用别的线程无法工作
三.1.线程函数
线程和进程一样,子线程退出的时候其内核资源主要由主线程回收,线程库中提供的线程回收函叫做pthread_join(),这个函数是一个阻塞函数,如果还有子线程在运行,调用该函数就会阻塞,子线程退出函数解除阻塞进行资源的回收,函数被调用一次,只能回收一个子线程,如果有多个子线程则需要循环进行回收。
另外通过线程回收函数还可以获取到子线程退出时传递出来的数据,函数原型如下:
#include <pthread.h>
// 这是一个阻塞函数, 子线程在运行这个函数就阻塞
// 子线程退出, 函数解除阻塞, 回收对应的子线程资源, 类似于回收进程使用的函数 wait()
int pthread_join(pthread_t thread, void **retval);
参数:
thread: 要被回收的子线程的线程ID
retval: 二级指针, 指向一级指针的地址, 是一个传出参数, 这个地址中存储了pthread_exit() 传递出的数据,如果不需要这个参数,可以指定为NULL
返回值:线程回收成功返回0,回收失败返回错误号。
3.2.回收子线程数据
在子线程退出的时候可以使用pthread_exit()的参数将数据传出,在回收这个子线程的时候可以通过phread_join()的第二个参数来接收子线程传递出的数据。接收数据有很多种处理方式,下面来列举几种:
3.2.1 使用子线程栈
通过函数pthread_exit(void *retval);可以得知,子线程退出的时候,需要将数据记录到一块内存中,通过参数传出的是存储数据的内存的地址,而不是具体数据,由因为参数是void*类型,所有这个万能指针可以指向任意类型的内存地址。先来看第一种方式,将子线程退出数据保存在子线程自己的栈区:
// pthread_join.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
// 定义结构
struct Persion
{
int id;
char name[36];
int age;
};
// 子线程的处理代码
void* working(void* arg)
{
printf("我是子线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<9; ++i)
{
printf("child == i: = %d\n", i);
if(i == 6)
{
struct Persion p;
p.age =12;
strcpy(p.name, "tom");
p.id = 100;
// 该函数的参数将这个地址传递给了主线程的pthread_join()
pthread_exit(&p);
}
}
return NULL; // 代码执行不到这个位置就退出了
}
int main()
{
// 1. 创建一个子线程
pthread_t tid;
pthread_create(&tid, NULL, working, NULL);
printf("子线程创建成功, 线程ID: %ld\n", tid);
// 2. 子线程不会执行下边的代码, 主线程执行
printf("我是主线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<3; ++i)
{
printf("i = %d\n", i);
}
// 阻塞等待子线程退出
void* ptr = NULL;
// ptr是一个传出参数, 在函数内部让这个指针指向一块有效内存
// 这个内存地址就是pthread_exit() 参数指向的内存
pthread_join(tid, &ptr);
// 打印信息
struct Persion* pp = (struct Persion*)ptr;
printf("子线程返回数据: name: %s, age: %d, id: %d\n", pp->name, pp->age, pp->id);
printf("子线程资源被成功回收...\n");
return 0;
}
通过打印的日志可以发现,在主线程中没有没有得到子线程返回的数据信息,具体原因是这样的:
如果多个线程共用同一个虚拟地址空间,每个线程在栈区都有一块属于自己的内存,相当于栈区被这几个线程平分了,当线程退出,线程在栈区的内存也就被回收了,因此随着子线程的退出,写入到栈区的数据也就被释放了。
因此可以创建全局数据区的数据
位于同一虚拟地址空间中的线程,虽然不能共享栈区数据,但是可以共享全局数据区和堆区数据,因此在子线程退出的时候可以将传出数据存储到全局变量、静态变量或者堆内存中。在下面的例子中将数据存储到了全局变量中:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
// 定义结构
struct Persion
{
int id;
char name[36];
int age;
};
struct Persion p; // 定义全局变量
// 子线程的处理代码
void* working(void* arg)
{
printf("我是子线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<9; ++i)
{
printf("child == i: = %d\n", i);
if(i == 6)
{
// 使用全局变量
p.age =12;
strcpy(p.name, "tom");
p.id = 100;
// 该函数的参数将这个地址传递给了主线程的pthread_join()
pthread_exit(&p);
}
}
return NULL;
}
int main()
{
// 1. 创建一个子线程
pthread_t tid;
pthread_create(&tid, NULL, working, NULL);
printf("子线程创建成功, 线程ID: %ld\n", tid);
// 2. 子线程不会执行下边的代码, 主线程执行
printf("我是主线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<3; ++i)
{
printf("i = %d\n", i);
}
// 阻塞等待子线程退出
void* ptr = NULL;
// ptr是一个传出参数, 在函数内部让这个指针指向一块有效内存
// 这个内存地址就是pthread_exit() 参数指向的内存
pthread_join(tid, &ptr);
// 打印信息
struct Persion* pp = (struct Persion*)ptr;
printf("name: %s, age: %d, id: %d\n", pp->name, pp->age, pp->id);
printf("子线程资源被成功回收...\n");
return 0;
}
3.2.3使用主线程栈
虽然每个线程都有属于自己的栈区空间,但是位于同一个地址空间的多个线程是可以相互访问对方的栈空间上的数据的 。由于很多情况下还需要在主线程中回收子线程资源,所以主线程一般都是最后退出,基于这个原因在下面的程序中将子线程返回的数据保存到了主线程的栈区内存中:
pthread_create(&tid, NULL, working, &p); //向子线程传入主线程中的数据这样子线程退出回收后数据依旧存在在主线程的堆区中。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
// 定义结构
struct Persion
{
int id;
char name[36];
int age;
};
// 子线程的处理代码
void* working(void* arg)
{
struct Persion* p = (struct Persion*)arg;
printf("我是子线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<9; ++i)
{
printf("child == i: = %d\n", i);
if(i == 6)
{
// 使用主线程的栈内存
p->age =12;
strcpy(p->name, "tom");
p->id = 100;
// 该函数的参数将这个地址传递给了主线程的pthread_join()
pthread_exit(p);
}
}
return NULL;
}
int main()
{
// 1. 创建一个子线程
pthread_t tid;
struct Persion p;
// 主线程的栈内存传递给子线程
pthread_create(&tid, NULL, working, &p);
printf("子线程创建成功, 线程ID: %ld\n", tid);
// 2. 子线程不会执行下边的代码, 主线程执行
printf("我是主线程, 线程ID: %ld\n", pthread_self());
for(int i=0; i<3; ++i)
{
printf("i = %d\n", i);
}
// 阻塞等待子线程退出
void* ptr = NULL;
// ptr是一个传出参数, 在函数内部让这个指针指向一块有效内存
// 这个内存地址就是pthread_exit() 参数指向的内存
pthread_join(tid, &ptr);
// 打印信息
printf("name: %s, age: %d, id: %d\n", p.name, p.age, p.id);
printf("子线程资源被成功回收...\n");
return 0;
}
在上面的程序中,调用pthread_create()创建子线程,并将主线程中栈空间变量p的地址传递到了子线程中,在子线程中将要传递出的数据写入到了这块内存中。也就是说在程序的main()函数中,通过指针变量ptr或者通过结构体变量p都可以读出子线程传出的数据。
四、线程分离
线程分离的作用:线程的分离(Detached Thread)是一种线程属性,用于告诉操作系统,在线程执行结束后,操作系统可以立即回收线程的相关资源,而不需要等待主线程或其他线程显式调用pthread_join
等等来回收资源
在线程库函数中为我们提供了线程分离函数pthread_detach(),调用这个函数之后指定的子线程就可以和主线程分离,当子线程退出的时候,其占用的内核资源就被系统的其他进程接管并回收了。线程分离之后在主线程中使用pthread_join()就回收不到子线程资源了。
源自以下学习记录