在Linux内核中多线程编程是很常见的一种性能提升手段。所以学习C语言,对于多线程也应该有一定的了解。关于线程与进程,在知乎看到下面这么一段说明,觉得很清晰:
做个简单的比喻:进程=火车,线程=车厢
- 线程在进程下行进(单纯的车厢无法运行)
- 一个进程可以包含多个线程(一辆火车可以有多个车厢)
- 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
- 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
- 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
- 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
- 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
- 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-"互斥锁"
- 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”
创建线程
创建线程的函数是:pthread_create()
,该函数声明在pthread.h
头文件中,其函数声明为:
int pthread_create( pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routing)(void *),
void *arg);
参数
该函数需要四个参数,其含义如下:
pthread_t *thread
一个指向 “新创建线程” 的指针变量
const pthread_attr_t *attr
一个指向 “新创建线程的相关属性” 的指针变量,一般情况下不需要手动更改新建线程的属性,直接传NULL即可
void *(*start_routing)(void *)
一个指向 “新线程要执行的函数” 的函数指针
void *arg
代表传递给新线程要执行的函数的参数
返回值
再次声明pthread_create()
函数的作用就是创建一个新的线程。其返回值就表示是否成功创建。所以其返回值有成功与不成功两种情况:
- 成功创建: 返回 0
- 创建失败: 返回非0值,在
errno.h
头文件中定义了常见的几个异常返回值:
- EAGIN:表示系统自远不足
- EINVAL: 表示attr参数无效
- EPERM: 表示attr参数非法,或者操作存在确权行为
线程创建实例
#include <stdio.h>
#include <unistd.h> // sleep函数的头文件
#include <pthread.h>
void * ThreadFun(void * arg) { // 新线程要执行的函数
if (arg == NULL) {
printf("arg is NULL!\n");
}else {
printf("%s\n", (char*)arg);
}
return NULL;
}
int main() {
int res;
char * url = "https://klelee.com";
pthread_t mythread1, mythread2; //定义两个新线程
/**
* &mythread1 对应的形参是:pthread_t *thread,形参是指针变量,因此需要进行传址
* NULL 对应的形参是:const pthread_attr_t *attr,也就是新线程的属性
* ThreadFun 对应的形参是:void *(*start_routing)(void *), 执行新线程要执行的函数的函数指针
* NULL 对应的形参是:void *arg,新线程要执行的函数的参数
*/
res = pthread_create(&mythread1, NULL, ThreadFun, NULL);
if (res != 0) {
printf("thread create failed!\n");
return 0;
}
/**
* &mythread2 对应的形参是:pthread_t *thread,形参是指针变量,因此需要进行传址
* NULL 对应的形参是:const pthread_attr_t *attr,也就是新线程的属性
* ThreadFun 对应的形参是:void *(*start_routing)(void *), 执行新线程要执行的函数的函数指针
* NULL 对应的形参是:void *arg,新线程要执行的函数的参数
*/
res = pthread_create(&mythread2, NULL, ThreadFun, (void*)url);
if (res != 0) {
printf("thread create failed!\n");
return 0;
}
sleep(5);
return 0;
}
多线程程序的编译
需要注意的地方就是对于多线程程序,在编译的时候需要添加lpthread
选项
gcc thread.c -o thread.out -lpthread
执行结果
终止线程
线程的终止,有三种情况:
- 程序正常结束,线程终止
- 通过return语句或者pthread_exit()终止线程
- 其他线程发送pthread_cancel()
第一种情况就是字面意思,很容易理解。接下来详细说明后面两种情况。
通过return和pthread_exit()终止进程
pthread_exit()
可以理解为是线程中的return语句。该函数也声明在pthread.h
文件中。其声明为:
void pthread_exit(void * retval);
该函数返回值为空,有一个void类型的指针变量作为形式参数。这个参数就类似与return语句返回的数据一样,作为当前线程运行的函数的返回值。当没有返回值的时候,这里可以是NULL.
return和pthread_exit()的区别
关于两者的区别还是直接体现在程序中吧,首先来看一个用return的程序:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void * thread_func (void * arg) {
sleep(5);
printf("https://klelee.com");
}
int main() {
int res;
pthread_t mythread;
res = pthread_create(&mythread, NULL, thread_func, NULL);
if (res != 0) {
printf("new thread failed!\n");
return 0;
}
printf("main thread will return!\n");
return res; //主要体现在这里
}
编译和运行结果如下:
[klelee@arch first]$ gcc exit_return.c -o exit_return.out -lpthread
[klelee@arch first]$ ./exit_return.out
main thread will return!
[klelee@arch first]$
再来看看pthread_exit()
的实例:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void * thread_func (void * arg) {
sleep(5);
printf("https://klelee.com");
}
int main() {
int res;
pthread_t mythread;
res = pthread_create(&mythread, NULL, thread_func, NULL);
if (res != 0) {
printf("new thread failed!\n");
return 0;
}
printf("main thread will return!\n");
pthread_exit(NULL); // 将这里换成了pthread_exit()
}
运行结果:
[klelee@arch first]$ gcc exit_return.c -o exit_return.out -lpthread
[klelee@arch first]$ ./exit_return.out
main thread will return!
https://klelee.com
[klelee@arch first]$
对比如上两段程序的运行结果发现,区别就在于子线程中的程序有没有完全的执行。在主线程中直接使用return语句进行返回之后,由主线程创建的子线程也随之终止。
而使用pthread_exit语句的主线程终止之后,子线程没有终止,而是在继续运行,知道程序结束!
pthread_cancel()函数
与上面两种程序终止方式不同的是,使用cancel信号终止是被动终止。调用cancel函数的其他线程,而不是线程自身调用。其生命如下:
int pthread_cancel(pthread_t *thread);
参数
指向目标线程的指针变量
返回值
有关于pthread_cancel函数的返回值需要注意的是其成功的条件:发送成功即为成功。即:
- cancel信号成功发送给目标线程,返回0
- cancel信号发送失败,返回非0值。若未找到目标线程,返回ESRCH宏,该宏的值为3.
对于接收cancel信号后终止的线程,相当于自己调用了pthread_exit(PTHREAD_CANCELED)
其中宏:PTHREAD_CANCELED定义在pthread.h
头文件中。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void * thread_func (void * arg) {
printf("https://klelee.com\n");
sleep(5); // 在这段时间内程序被终止
printf("cclike.cc\n"); // 主要看这句会不会输出
}
int main() {
int res;
int value;
void * mess;
pthread_t mythread;
res = pthread_create(&mythread, NULL, thread_func, NULL);
if (res != 0) {
printf("new thread failed!\n");
return 0;
} //创建线程之后sleep两秒
sleep(2);
// 然后立即发送cancel信号,在thread_fun函数睡眠的5秒内,强制终止程序运行
res = pthread_cancel(mythread);
if (res != 0) {
printf("stop thread failed!\n");
return 0;
}
res = pthread_join(mythread, &mess);
if (res != 0) {
printf("wait for thread failed!\n");
return 0;
}
if (mess == PTHREAD_CANCELED) {
printf("force stop thread sucess!\n"); // 判断是否有cancel信号终止
}else {
printf("force stop thread failed!\n");
}
printf("main thread will return!\n");
pthread_exit(NULL);
}
编译和运行结果:
[klelee@arch first]$ gcc exit_return.c -o exit_return.out -lpthread
[klelee@arch first]$ ./exit_return.out
https://klelee.com
force stop thread sucess!
main thread will return!
[klelee@arch first]$
通过运行结果可以看到,thread_fun()函数被提前终止了!
cancel信号处理机制
并不是所有的cancel信号都会被线程处理。很常见的现象就是当目标线程接收到cancel信号的时候,正处于循环当中,这种情况就不会立即响应cancel信号去终止线程
事实上,对于默认属性的线程,当有线程借助 pthread_cancel() 函数向它发送 Cancel 信号时,它并不会立即结束执行,而是选择在一个适当的时机结束执行。
所谓适当的时机,POSIX 标准中规定,当线程执行一些特殊的函数时,会响应 Cancel 信号并终止执行,比如常见的 pthread_join()、pthread_testcancel()、sleep()、system() 等,POSIX 标准称此类函数为“cancellation points”(中文可译为“取消点”)。
此外,<pthread.h> 头文件还提供有 pthread_setcancelstate() 和 pthread_setcanceltype() 这两个函数,我们可以手动修改目标线程处理 Cancel 信号的方式。
pthread_setcancelstate()函数
借助 pthread_setcancelstate() 函数,我们可以令目标线程处理 Cancal 信号,也可以令目标线程不理会其它线程发来的 Cancel 信号。其函数声明如下:
int pthread_setcancelstate( int state , int * oldstate );
参数
- state 参数有两个可选值,分别是:
- PTHREAD_CANCEL_ENABLE(默认值):当前线程会处理其它线程发送的 Cancel 信号;
- PTHREAD_CANCEL_DISABLE:当前线程不理会其它线程发送的 Cancel 信号,直到线程状态重新调整为 PTHREAD_CANCEL_ENABLE 后,才处理接收到的 Cancel 信号。
- oldtate 参数用于接收线程先前所遵循的 state 值,通常用于对线程进行重置。如果不需要接收此参数的值,置为 NULL 即可。
返回值
pthread_setcancelstate() 函数执行成功时,返回数字 0,反之返回非零数。
pthread_setcanceltype()函数
当线程会对 Cancel 信号进行处理时,我们可以借助 pthread_setcanceltype() 函数设置线程响应 Cancel 信号的时机。语法格式如下:
int pthread_setcanceltype( int type , int * oldtype );
参数
(1) type 参数有两个可选值,分别是:
- PTHREAD_CANCEL_DEFERRED(默认值):当线程执行到某个可作为取消点的函数时终止执行;
- PTHREAD_CANCEL_ASYNCHRONOUS:线程接收到 Cancel 信号后立即结束执行。
(2) oldtype 参数用于接收线程先前所遵循的 type 值,如果不需要接收该值,置为 NULL 即可。
返回值
pthread_setcanceltype() 函数执行成功时,返回数字 0,反之返回非零数。
实例
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
void * thread_Fun(void * arg) {
printf("新建线程开始执行\n");
int res;
//设置线程为可取消状态
res = pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
if (res != 0) {
printf("修改线程可取消状态失败\n");
return NULL;
}
//设置线程接收到 Cancel 信号后立即结束执行
res = pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
if (res != 0) {
printf("修改线程响应 Cancel 信号的方式失败\n");
return NULL;
}
while (1);
return NULL;
}
int main()
{
pthread_t myThread;
void * mess;
int value;
int res;
res = pthread_create(&myThread, NULL, thread_Fun, NULL);
if (res != 0) {
printf("线程创建失败\n");
return 0;
}
sleep(1);
//向 myThread 线程发送 Cancel 信号
res = pthread_cancel(myThread);
if (res != 0) {
printf("终止 myThread 线程失败\n");
return 0;
}
//等待 myThread 线程执行结束,获取返回值
res = pthread_join(myThread, &mess);
if (res != 0) {
printf("等待线程失败\n");
return 0;
}
if (mess == PTHREAD_CANCELED) {
printf("myThread 线程被强制终止\n");
}
else {
printf("error\n");
}
return 0;
}
编译与运行结果
[root@localhost ~]# gcc thread.c -o thread.exe -lpthread
[root@localhost ~]# ./thread.exe
新建线程开始执行
myThread 线程被强制终止
获取线程函数的返回值
在上面的函数中我们用到了一个新的函数pthread_join
,该函数能够获取某个线程执行结束时返回的数据。接下来我们详细看看它的使用方法。
pthread_join函数
该函数声明在pthread.h
头文件中,其函数声明如下:
int pthread_join(pthread_t thread, void ** retval);
参数
该函数有两个返回值:
thread
参数用于指定接收哪个线程的返回值
retval
这里用一个二级指针直线了返回值地址的地址。如果 thread 线程没有返回值,又或者我们不需要接收 thread 线程的返回值,可以将 retval 参数置为 NULL。
pthread_join() 函数会一直阻塞调用它的线程,直至目标线程执行结束(接收到目标线程的返回值),阻塞状态才会解除。
返回值
该函数拥有int类型的返回值。
- 0 值: 表示函数成功等到目标线程运行结束(成功拿到返回值)。
- 非零值:也就是等待目标线程返回失败,不同原因有不同的返回值,常见原因如下:
- EDEADLK:检测到线程发生了死锁。
- EINVAL:分为两种情况,要么目标线程本身不允许其它线程获取它的返回值,要么事先就已经有线程调用 pthread_join() 函数获取到了目标线程的返回值。
- ESRCH:找不到指定的 thread 线程。
相关宏都在errno.h中做了定义!