一.多线程的概念
什么是多线程
线程是从进程的内存资源中切割一部分内存,提供给线程使用,线程也能够实现多任务并发执行。
Windows系统,linux系统,还有STM32它们都是支持多任务的。
线程是能拥有资源和独立运行的最小单位,也是程序执行的最小单位。
任务调度采用的是时间片轮转的抢占式调度方式,
后来,随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,
为了提高系统的性能,于是就发明了线程,许多操作系统规范里引用了轻量级进程的概念,
二.线程与进程的区别
定义上的区别:
线程: 它是比进程更小的能独立运行的基本单位,线程本身是不拥有系统资源的,
但是它可以和属于同一个进程的其他线程共享进程所拥有的全部资源。
调度区别:
共享地址空间:
进程:拥有各自独立的地址空间、资源,所以共享复杂,需要用IPC(进程间通信),但是同步简单。
线程:共享所属进程的资源,因此共享简单,但是同步复杂,需要用加锁等措施。
占用内存和cpu:
互相影响:
三.线程资源
1.文件描述符表
2.每种信号的处理方式 , 不推荐把信号与线程进行结合使用
3.当前工作路径
4.用户ID与组ID
5.内存地址空间 例如 .test / .data / .bss / heap / 共享库
1.线程ID
2.处理器线程和栈指针(内核栈,用于保存寄存器当中的临时值)
3.独立的栈空间(用户空间栈)
4.errno变量
5.信号屏蔽字
6.调度优先级
优点: 1.提高程序的并发性 2.开销小 3.数据通信,共享方便
缺点: 1.库函数,不稳定 2.调试,编写困难 3.对信号支持不太友好
优点相对突出,缺点均不是硬伤,在能够同时使用进程和线程实现某一功能时,优先选择线程。
四.基本相关API
创建一条新线程
NAME
pthread_create - create a new thread
SYNOPSIS
#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. //编译程序时,需要连接线程库.
//参数解析
参数1 pthread_t *thread : 新线程TID. 传出参数,用于保存新创建出来线程的id号.(正整数)
参数2 const pthread_attr_t *attr : 线程属性. 一般设置为NULL,表示使用系统默认的属性.(标准属性)
参数3 void *(*start_routine) (void *) : 线程例程. (函数指针) 返回值为void* 参数为 void* 与 signal函数用法一致.
创建线程需要完成的任务,靠这个函数来实现.称之为线程任务函数.
参数4 void *arg : 传递给线程的参数.
//返回值
成 功 : 返回0;
失 败 : 返回errno;
#注意
在使用多线程函数的时,需要链接我们的线程库 -pthread.
也就是在gcc example.c -o main -pthread.
如果没有链接,在编译时则会报错,必须要链接我们的线程库.
示例代码
基本使用示例:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void *fun(void * arg) //线程任务函数 接口为固定的除了函数名 与 参数名可以改变,其他的不允许改变
{
while(1)
{
printf("11111111\n");
sleep(1);
}
}
int main(int argc, char const *argv[]) //执行main函数的线程 被称为主线程
{
pthread_t thread_id;
//1.创建一个子线程
pthread_create(&thread_id, //线程的ID号
NULL, //表示系统标准属性,不额外设置线程属性
fun, //线程将要执行的任务函数名
NULL); //不传递额外参数给线程
while(1)
{
printf("2222222222\n");
sleep(1);
}
return 0;
}
获取线程的ID
//获取线程的ID
SYNOPSIS
#include <pthread.h>
pthread_t pthread_self(void);
返回值:
成功 : 线程ID号
这个函数是永远不会失败的.
线程的退出
NAME
pthread_exit - terminate calling thread
SYNOPSIS
#include <pthread.h>
void pthread_exit(void *retval);
参数解析:
void *retval : 保存线程的退出值.
配合着 pthread_join()使用.
等待线程退出
NAME
pthread_join - join with a terminated thread
SYNOPSIS
#include <pthread.h>
函数原型 :
int pthread_join(pthread_t thread, void **retval); //阻塞等待子线程退出
该函数会使调用者阻塞等待,直到所指定的线程退出为止。
该返回时系统将回收退出线程的资源,调用线程可以获得退出线程的返回值。
Compile and link with -pthread.
参数解析 :
pthread_t thread : 线程的ID号,(指定等待某个线程退出);
void **retval : 储存线程退出值的指针.(保留线程退出时所返回的数据);
返回值:
成功 : 返回0;
失败 : 返回errno值;
综合示例代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <pthread.h>
void *fun(void * arg) //线程任务函数 接口为固定的除了函数名 与 参数名可以改变,其他的不允许改变
{
int a = *(int *)arg;
printf("%d \n", a);
//不需要返回值写法
//pthread_exit(NULL);
//需要返回值写法
pthread_exit("abcd");
}
int main(int argc, char const *argv[]) //执行main函数的线程 被称为主线程
{
int a = 100;
pthread_t thread_id;
//1.创建一个子线程
pthread_create(&thread_id, //线程的ID号
NULL, //表示系统标准属性,不额外设置线程属性
fun, //线程将要执行的任务函数名
&a); //不传递额外参数给线程
//只等待,不需要返回值
//pthread_join(thread_id,NULL);
//等待以上线程退出,并获取退出值
void *ret;
pthread_join(thread_id, &ret);
printf("%s\n", (char *)ret);
return 0;
}
线程的取消
SYNOPSIS
#include <pthread.h>
int pthread_cancel(pthread_t thread);
参数解析 :
pthread_t thread : 需要取消的线程ID号.
返回值:
成功 : 0;
失败 : 错误码.
注意:线程的取消并不是实时的,而是有一定的延迟,需要等待程到某个检查点,
类似于玩游戏存档,必须要到达指定的位置才能够进行存档,
线程也是如此,是并不能够立即取消的,也是需要到达某个点才能够进行取消
该取消点可以通过man 7 pthreads可以进行查看具备这些取消点系统调用列表
如果该线程当中没有取消点,会导致该线程是无法正常退出的,
为了避免这种情况发生,程序员自身也是可以自己设置取消点.
void pthread_testcancel(void);
那什么是取消点呢?
那你可以把取消点粗略的看成是一个系统调用(调用了某个函数该函数进入了内核进行处理);
如果被 pthread_cancel 函数取消的进程,那么该函数会返回 -1 代表着该子进程并非正常退出
示例代码
使用示例:
/*
线程取消
pthread_cancel()函数使用示例
*/
void *fun(void *arg)
{
while(1)
{
printf("线程正在执行...\n");
sleep(1);
}
}
int main(int argc, char **argv)
{
pthread_t tid;
pthread_create( &tid, NULL, fun, NULL);
//主线程 隔5秒 取消线程.
sleep(5);
pthread_cancel(tid);
pthread_join(tid, NULL);
return 0;
}
多线程属性设置
设置线程能否被取消属性
NAME
pthread_setcancelstate, pthread_setcanceltype - set cancelability state and type
SYNOPSIS
#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate); //设置线程取消的状态
int pthread_setcanceltype(int type, int *oldtype); //设置线程取消的类型
Compile and link with -pthread.
线程取消状态有两种 : 可取消 与 不可取消 系统默认的是可取消
线程取消类型有两种 : 延时取消 与 立即取消 默认是延迟取消
//参数解析:
int state :
PTHREAD_CANCEL_ENABLE //可以取消,默认是可以被取消的.
PTHREAD_CANCEL_DISABLE //不可取消
int *oldstate : 用于保存原本线程的取消状态,不想保存则设置为NULL;
int type :
//延时取消,当线程被取消时,该线程不会立即退出,会继续执行直到遇到取消函数点时,线程才会结束
//取消点函数,是Linux中已经规定好的一系列函数. shell命令查看 man 7 pthreads
//默认的取消状态为延迟取消
PTHREAD_CANCEL_DEFERRED
//当线程被取消的时候,该线程会立即退出.
PTHREAD_CANCEL_ASYNCHRONOUS //立即取消
int *oldtype : 用于保存原本线程的取消类型,不想保存则设置为NULL;
/*
函数使用示例
设置线程不可取消
int pthread_setcancelstate(int state, int *oldstate);
*/
void *fun(void *arg)
{
//设置当前线程不可取消,不保存当前线程取消状态
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
while(1)
{
printf("线程正在执行...\n");
sleep(1);
}
}
int main(int argc, char **argv)
{
pthread_t tid;
pthread_create( &tid, NULL, fun, NULL);
//主线程 隔5秒 取消线程.
sleep(5);
pthread_cancel(tid);
pthread_join(tid, NULL);
return 0;
}
设置线程的取消类型
/*
函数使用示例
设置线程取消的类型 为立即取消
int pthread_setcanceltype(int type, int *oldtype); //设置线程取消的类型
*/
void *fun(void *arg)
{
printf("我将要被取消了...\n");
//设置当前线程立即取消,不保存当前线程取消类型
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
printf("被取消了\n");
while(1)
{
printf("线程正在执行...\n");
sleep(1);
}
}
int main(int argc, char **argv)
{
pthread_t tid;
pthread_create( &tid, NULL, fun, NULL);
pthread_cancel(tid);
pthread_join(tid, NULL);
return 0;
}
线程的分离属性设置
线程的属性结构体
pthread_attr_t 类型变量,是Linux中用于存放线程属性的结构体类型定义如下.
typedef struct
{
int detachstate; //线程的分离状态
int schedpolicy; //线程调度策略
struct sched_param schedparam; //线程的调度参数
int inheritsched; //线程的继承性
int scope; //线程的作用域
size_t guardsize; //线程栈末尾的警戒缓冲区大小
int stackaddr_set;
void* stackaddr; //线程栈的位置
size_t stacksize; //线程栈的大小
}pthread_attr_t;
线程的属性,有很多,设置思路都一样.以设置线程分离属性为例.
线程的分离属性分为两种 : 可分离 与 不可分离
可分离 : 1.线程创建以后,不需要主线程进行回收(想回收也回收不了),线程结束时,会被系统自动回收.
2.在主线程中join回收线程时,主线程将不再阻塞等待该线程.
不可分离 : 线程创建以后,需要主进程去主动回收.(线程被创建后,该线程被系统默认为可分离的状态).
设置分离属性的方式有两种:
方式1:
函数作用: 设置线程属性分离.(在线程服务函数中使用)
函数原型:
#include <pthread.h>
int pthread_detach(pthread_t thread);
参数 pthread_t thread: 需要分离的线程号.
返回值:
成功:返回0
失败:返回错误码
需要配合pthread_self()函数使用,
函数作用:获取自己的线程号.
pthread_t pthread_self(void);
成功:返回自己的ID号.
下列为一个简单的线程服务函数使用示例:
void *func(void *arg)
{
//设置当前线程为分离线程
pthread_detach(pthread_self());
while(1)
{
printf("111\n");
sleep(1);
}
}
方式2:
线程属性设置的基本步骤.
1.定义线程属性变量并初始化.
int pthread_attr_init(pthread_attr_t *attr);
2.根据你想设置的属性,选择对应的函数进行设置. //详情参考书籍 5.4章节 Linux 线程入门 468页
//pthread_attr_setxxxx();
//设置线程分离属性
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
3.创建线程,使用刚才初始化好的程属性变量.填写到pthread_create()函数的第二个参数当中.
4.销毁属性变量
int pthread_attr_destroy(pthread_attr_t *attr);
参数解析:
pthread_attr_t *attr : 为 pthread_attr_t 类型变量.
int detachstate : 宏定义.
PTHREAD_CREATE_DETACHED //表示可分离
PTHREAD_CREATE_JOINABLE //表示不可分离
线程的分离与非分离主要体现在该线程在执行完任务以后,
还需不需要向创建的这个线程的任务发送信息。
如果需要发送,则设置为非分离,不需要则设置为分离.
线程属性分离示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <pthread.h>
/*
设置线程可分离.
*/
void *fun(void *arg)
{
sleep(1);
printf("线程运行起来了...\n");
pthread_exit("线程退出了..");
}
int main(int argc, char **argv)
{
void * arg = NULL;//用于接收线程的退出值
pthread_t tid;
//1.定义线程属性变量 并 初始化
pthread_attr_t myattr;
pthread_attr_init(&myattr);
//2.调用线程设置分离属性函数,设置为可分离.
pthread_attr_setdetachstate(&myattr,PTHREAD_CREATE_DETACHED); //设置线程分离属性
//3.创建线程,使用刚才初始化好的程属性变量,设置为线程属性分离.
pthread_create( &tid, &myattr, fun, NULL);
//pthread_create( &tid, NULL, fun, NULL); //可尝试两种写法的对比.
//回收线程
printf("主函数准备回收线程...\n");
pthread_join(tid, &arg); //join函数本是阻塞等待线程退出后,主线程才会退出.
//设置好了属性分离后,此时的join函数,不再进行阻塞.因为它没有线程可回收
printf("我回收的线程退出信息为 : %s\n", (char *)arg);
//4.销毁线程分离属性.
pthread_attr_destroy(&myattr);
return 0;
}
错误检查示例代码
#include "myhead.h"
void * Fun(void *arg)
{
printf("主线程的PID %d \n",getpid());
pthread_exit(NULL);
}
int main(int argc, char const *argv[])
{
pthread_t tid; //用与主线程与子线程沟通的桥梁
int ret = pthread_create(&tid, NULL, Fun , NULL);
ret = pthread_detach(tid);
if(ret != 0)
{
//printf("detach error : %d\n", ret);
fprintf(stderr,"detach error : %s\n",strerror(ret));
}
void * q = NULL;
ret = pthread_join(tid, &q);
if(ret != 0)
{
fprintf(stderr,"join error : %s\n",strerror(ret));
}
return 0;
}
线程调度优先级
//设置或获取当前线程的优先级继承关系
SYNOPSIS
#include <pthread.h>
int pthread_attr_setinheritsched(pthread_attr_t *attr,
int inheritsched);
int pthread_attr_getinheritsched(const pthread_attr_t *attr,
int *inheritsched);
参数 pthread_attr_t *attr : 需要操作的属性
参数 int inheritsched : 继承的关系
PTHREAD_INHERIT_SCHED 继承主线程的调度方式
PTHREAD_EXPLICIT_SCHED 不继承主线程的调度方式
假设main函数中创建了一条新的线程.
这条新的线程采用了PTHREAD_INHERIT_SCHED该宏定义,那么该线程会继承main函数的优先级调度方式.
如果在某条线程里创建了一条线程,也是使用PTHREAD_INHERIT_SCHED宏定义,那么在线程里创建出来的另一条线程
会继承,之前那条线程的优先级调度方式.
如果你不想使用之前的那条线程的优先级调度方式:
那你就可以使用PTHREAD_INHERIT_SCHED这个宏.
SYNOPSIS
#include <pthread.h>
int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy); //设置优先级调度方式
int pthread_attr_getschedpolicy(const pthread_attr_t *attr, int *policy); //获取优先级调度方式
参数 pthread_attr_t *attr: 需要被设置的属性.
参数 int policy : 需要被调度的方式.有一下三种:
SCHED_FIFO : 表示先进先出的调度方式,设置有静态优先级 0 ~ 99,数字越大优先级越高。
优先级的高的会一直霸占CPU资源一直运行,不会给优先级低的机会,除非高优先级运行完毕,
才会轮到优先级低的线程运行.
如果静态优先级相同,那么一定要等待前一个线程运行结束后,才会轮到下一个线程运行.
SCHED_RR : 时间片轮转调度方法。设置动态优先级 -19~20。数值越小,优先级越高。
为每一个线程设置了时间片,当线程运行完这段时间片以后,会切换到当前相同静态优先级的下一个线程中。
如果某一个线程一直在运行,这个线程的动态优先级会随着时间的增加,逐渐降低。到下一个线程运行.
SCHED_OTHER : 静态优先级为0,所有的关于静态优先级设置都无效.
注意:
只有管理员root权限,才可以使用SCHED_FIFO 和 SCHED_RR 调度方法
设置线程的静态优先级
SYNOPSIS
#include <pthread.h>
//设置线程的静态优先级
int pthread_attr_setschedparam(pthread_attr_t *attr,
const struct sched_param *param);
int pthread_attr_getschedparam(const pthread_attr_t *attr,
struct sched_param *param);
参数 pthread_attr_t *attr: 线程号
参数 const struct sched_param *param: 静态优先级结构体
struct sched_param
{
int sched_priority; /* Scheduling priority */
};