C_高阶
多线程
-
概念
-
并发
-
多个线程在宏观上同时执行,但在微观上可能是交替执行的;
-
-
并行
-
多个线程真正地同时执行,通常需要多核处理器的支持。
-
线程函数
-
每一个线程都有一个唯一的线程ID,ID类型为pthread_t,这个ID是一个无符号长整形数,如果想要得到当前线程的线程ID,可以调用如下函数
pthread_t pthread_self(void);
printf("我是主线程, 线程ID: %ld\n", pthread_self()); -
#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: 作为实参传递到 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; }
结果:子线程创建成功, 线程ID: 139712560109312
我是主线程, 线程ID: 139712568477440
i = 0
i = 1
i = 2线程一直在运行, 执行期间创建出了子线程,说明主线程有CPU时间片, 在这个时间片内将代码执行完毕了, 主线程就退出了。子线程被创建出来之后需要抢cpu时间片, 抢不到就不能运行,如果主线程退出了, 虚拟地址空间就被释放了, 子线程就一并被销毁了。但是如果某一个子线程退出了, 主线程仍在运行, 虚拟地址空间依旧存在。
得到的结论:在没有人为干预的情况下,虚拟地址空间的生命周期和主线程是一样的,与子线程无关。
目前的解决方案: 让子线程执行完毕, 主线程再退出, 可以在主线程中添加挂起函数 sleep();
-
-
线程退出
在编写多线程程序的时候,如果想要让线程退出,但是不会导致虚拟地址空间的释放(针对于主线程),我们就可以调用线程库中的线程退出函数,只要调用该函数当前线程就马上退出了,并且不会影响到其他线程的正常运行,不管是在子线程或者主线程中都可以使用。
-
-
#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; }
-
-
线程回收
-
函数
-
#include <pthread.h> // 这是一个阻塞函数, 子线程在运行这个函数就阻塞 // 子线程退出, 函数解除阻塞, 回收对应的子线程资源, 类似于回收进程使用的函数 wait() int pthread_join(pthread_t thread, void **retval);
-
参数:
-
thread: 要被回收的子线程的线程ID
-
retval: 二级指针, 指向一级指针的地址, 是一个传出参数, 这个地址中存储了pthread_exit() 传递出的数据,如果不需要这个参数,可以指定为NULL
-
-
返回值:线程回收成功返回0,回收失败返回错误号。
-
-
回收子线程数据
-
在子线程退出的时候可以使用pthread_exit()的参数将数据传出,在回收这个子线程的时候可以通过phread_join()的第二个参数来接收子线程传递出的数据。接收数据有很多种处理方式,下面来列举几种:
-
-
-
使用子线程栈
-
-
通过函数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; }
结果:子线程创建成功, 线程ID: 140652794640128
我是主线程, 线程ID: 140652803008256
i = 0
i = 1
i = 2
我是子线程, 线程ID: 140652794640128
child == i: = 0
child == i: = 1
child == i: = 2
child == i: = 3
child == i: = 4
child == i: = 5
child == i: = 6
子线程返回数据: name: , age: 0, id: 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; }
结果:
子线程创建成功, 线程ID: 140652794640128
我是主线程, 线程ID: 140652803008256
i = 0
i = 1
i = 2
我是子线程, 线程ID: 140652794640128
child == i: = 0
child == i: = 1
child == i: = 2
child == i: = 3
child == i: = 4
child == i: = 5
child == i: = 6
name: tom, age: 12, id: 100
子线程资源被成功回收...
-
-
-
-
使用主线程栈
-
-
虽然每个线程都有属于自己的栈区空间,但是位于同一个地址空间的多个线程是可以相互访问对方的栈空间上的数据的。由于很多情况下还需要在主线程中回收子线程资源,所以主线程一般都是最后退出,基于这个原因在下面的程序中将子线程返回的数据保存到了主线程的栈区内存中:
-
-
-
-
#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都可以读出子线程传出的数据。
-
-
-
-
线程分离
在线程库函数中为我们提供了线程分离函数pthread_detach(),调用这个函数之后指定的子线程就可以和主线程分离,当子线程退出的时候,其占用的内核资源就被系统的其他进程接管并回收了。线程分离之后在主线程中使用pthread_join()就回收不到子线程资源了。
-
-
#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); } // 设置子线程和主线程分离 pthread_detach(tid); // 让主线程自己退出即可 pthread_exit(NULL); return 0; }
-
-
其他线程函数
线程取消的意思就是在某些特定情况下在一个线程中杀死另一个线程。使用这个函数杀死一个线程需要分两步:
- 在线程A中调用线程取消函数pthread_cancel,指定杀死线程B,这时候线程B是死不了的
- 在线程B中进程一次系统调用(从用户区切换到内核区),否则线程B可以一直运行。
-
#include <pthread.h> // 参数是子线程的线程ID int pthread_cancel(pthread_t thread);
参数:要杀死的线程的线程ID
返回值:函数调用成功返回0,调用失败返回非0错误号。 -
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <pthread.h> // 子线程的处理代码 void* working(void* arg) { int j=0; for(int i=0; i<9; ++i) { j++; } // 这个函数会调用系统函数, 因此这是个间接的系统调用 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); } // 杀死子线程, 如果子线程中做系统调用, 子线程就结束了 pthread_cancel(tid); // 让主线程自己退出即可 pthread_exit(NULL); return 0; }
关于系统调用有两种方式:
直接调用Linux系统函数
调用标准C库函数,为了实现某些功能,在Linux平台下标准C库函数会调用相关的系统函数 -
线程ID比较
在Linux中线程ID本质就是一个无符号长整形,因此可以直接使用比较操作符比较两个线程的ID,但是线程库是可以跨平台使用的,在某些平台上 pthread_t可能不是一个单纯的整形,这中情况下比较两个线程的ID必须要使用比较函数,函数原型如下:
-
#include <pthread.h> int pthread_equal(pthread_t t1, pthread_t t2);参数:t1 和 t2 是要比较的线程的线程ID
返回值:如果两个线程ID相等返回非0值,如果不相等返回0

浙公网安备 33010602011771号