多线程
典型的进程可以看成是只有一个控制线程(主线程)。
一个进程在某一时刻只能做一件事情。有了多个控制线程之后,在程序设计时就可以把程序设计成在某一时刻能够做不止一件事,每个线程处理各自独立的任务。
1、进程的两个基本属性
1、进程是一个可拥有资源的独立单位,一个进程要能独立运行,它必须拥有一定的资源,包括用于存放程序正文、数据的磁盘和内存地址空间,以及它在运行时所需要的I/O设备、已打开的文件。信号量等。
2、进程同时又是一个可独立调度和分派的基本单位;每个进程在系统中都有唯一的PCB,系统可以根据PCB来感知进程的存在,也可以根据PCB中的信息对进程进行调度
总结下来就是
在传统OS中无多线程 进程是资源分配和调度的基本单位,
在有多线程OS中 进程是资源分配的基本单位,线程是调度和分派的基本单位
2、线程作为调度和分配的基本单位
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.
每个线程都包含有标识执行环境所必须的信息,其中包括进程中标识线程的线程ID、一组寄存器值、栈、调度优先级和策略、信号屏蔽字、errno变量以及线程私有数据。
一个进程的所有信息对该进程的所有线程都是共享的,包括可执行程序的代码、程序的全局内存和堆内存、栈以及文件描述符。
3、进程与线程的比较
(1)调度的基本单位
在传统OS中,进程是调度的基本单位,在每次被调度时,都需要进行上下文切换,开销较大。
在引入了线程的OS中,线程是调度的基本单位, 在线程切换时,仅需保存和设置少量寄存器内容,切换代价远低于进程。 同一进程中线程的切换不会引起进程的切换,但从一个进程中的线程切换到另一个进程中的线程时,必然会引起进程的切换
(2)并发性
不仅进程可以并发执行, 同一进程中的多个线程也可以并发执行, 不同进程中的线程也可以并发执行
(3) 拥有资源
进程可以拥有资源,并作为系统中拥有资源的一个基本单位。
线程本身并不拥有系统资源,而是仅有一点必不可少的、能够保证独立运行的资源。比如,在每个线程中都应具有一个用于控制线程运行的线程控制块TCB、用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈
线程除了拥有自己的少量资源外,还允许多个线程共享该进程所拥有的资源。 属于同一个进程的多个线程都具有相同的地址空间。这意味着,线程可以访问改地址空间中的每一个虚地址;此外还可以访问进程所拥有的的资源,如已打开的文件、定时器、信号量等的地址空间和它所申请到的IO设备等
(4)独立性
在同一进程中的不同线程之间的独立性要比不同进程之间的独立性低得多。这是因为,
1、为防止进程之间彼此干扰和破坏,每个进程都拥有独立的地址空间和其他资源,不允许其他进程访问。 进程间不共用资源与变量
2、对于同一进程的多个线程来说,他们共享进程的内存地址和资源
(5)系统开销
进程的创建、撤销、切换的开销都远远大于线程的开销
在创建或撤销进程时,系统都要为之分配和回收进程控制块(PCB)、分配或回收其他资源
在进程切换时,涉及到进程的上下文切换、
(6) 支持多处理机系统
在多处理机中可以将一个进程中的多个线程分配到多个处理机上,使他们并行执行
4、进程与线程的区别与联系
(1) 联系
a.一个进程之中至少有一个线程,此时的线程称之为主线程
b.一个线程可以创建和撤销另一个线程
(2)区别
a.进程是资源分配的基本单位;线程是调度和分配的基本单位
b.进程切换的开销比线程切换的开销大得多
c.进程拥有独立的地址空间和资源,而线程是共享进程的地址空间和资源
d.一个进程挂掉并不会影响其他进程,而一个线程挂掉则有可能整个进程就挂掉了(线程虽然有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉)
5、线程标识
像进程有进程PID一样,线程也有线程ID。线程ID只有在它所属的进程上下文中才有意义
5.1比较两个线程ID是否相等
#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);
//若相等,返回非0, 否则返回0
pthread_t 在linux中是 unsigned long类型
5.2获取自身线程ID
#include <pthread.h>
pthread_t pthread_self(void);
//返回值, 调用线程的线程ID
6、创建线程
程序开始运行时,它也是以单进程中的单个控制线程启动的。
新增的线程可以通过调用pthread_create函数创建
pthread_create
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
//成功返回0 ,否则返回错误编号
参数 | 描述 |
---|---|
thread | 新创建的线程ID,所指向的内存单元 |
attr | 设置线程属性, 默认为NULL |
start_routine | 新创建的线程从start_routine函数的地址开始执行,该函数只有一个参数,如果需要传递的参数为一个以上,则需要使用一个结构体封装 |
arg | 子线程函数的的参数,使用arg进行传入 |
当pthread_create出错时,返回的是errno
线程创建时并不能保证哪个线程会先运行:这与系统的调度算法有关
/*************************************************************************
> File Name: thread.c
> 作者:YJK
> Mail: 745506980@qq.com
> Created Time: 2021年05月08日 星期六 14时08分27秒
************************************************************************/
#include<stdio.h>
#include <pthread.h>
#include <unistd.h>
void * cthread(void *arg)
{
printf("thread id :%lu, pid :%u \n", pthread_self(), getpid());
return (void *)0;
}
int main(int argc,char *argv[])
{
pthread_t tid;
int err = pthread_create(&tid, NULL, cthread, NULL);
if (err != 0)
{
printf("errno :%d\n", err);
return -1;
}
printf("man thread id:%lu, pid :%u\n", pthread_self(), getpid());
sleep(1); // 主线程休眠,因为子线程与主线程处于竞争状态,如果主线程先运行完毕,那么子线程将无法运行
return 0;
}
线程创建时并不能保证哪个线程会先运行:这与系统的调度算法有关
根据运行结果可以看出,tid是不同的,pid是相同的
7、多线程程序链接
使用了多线程的程序,在链接时,需要加上 -pthread参数
8、线程终止
如果进程中的任意线程执行了exit、_Exit或者 _exit,那么整个进程都会终止。与此相类似,如果默认动作是终止进程,那么,发送到线程的信号就会终止整个进程。
单个线程可以通过3种方式退出,因此可以在不终止整个进程的情况下,停止它的控制流。
(1) 线程可以简单地从启动例程中返回,返回值是线程的退出码
(2) 线程可以被同一个进程中的其他线程取消
(3) 线程调用pthread_exit。
8.1pthread_exit函数
#include <pthread.h>
void pthread_exit(void *retval);
参数retval 是一个void 类型的指针,与传递给启动例程中的arg参数类似。进程中的其他线程也可以通过调用pthread_join函数访问这个指针。
8.2pthread_join函数 与指定线程建立连接
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
//返回值,成功返回0, 失败返回错误编号errno
调用pthread_join的线程将一直阻塞,直到指定的线程调用pthread_exit、从启动例程中返回或者被取消。
如果线程简单地从它的启动例程返回,retval就包含返回码
如果线程被取消,有retval指定的内存单元就设置为PTHREAD_CANCELED (void*) -1
如果对线程的返回值并不感兴趣,retval为NULL即可
同样的,pthread_create, pthread_exit的无类型指针参数可以传递的值不止一个,这个指针可以传递包含复杂信息的结构的地址,但是注意,这个结构所使用的内存在调用者完成后必须仍然是有效的,
因为一个线程就相当于是一个栈,当线程退出时,此时栈空间被其他线程使用时,那么读取到的数据就是脏数据。 这里可以使用全局变量、或者malloc来解决
例如
/*************************************************************************
> Fil Name: thread.c
> 作者:YJK
> Mail: 745506980@qq.com
> Created Time: 2021年05月08日 星期六 14时08分27秒
************************************************************************/
#include<stdio.h>
#include <pthread.h>
#include <unistd.h>
struct ret{
int a;
int b;
};
void * cthread(void *arg)
{
struct ret ret= {0x1, 0x2};
printf("thread id :%lu, pid :%u \n", pthread_self(), getpid());
pthread_exit((void *)&ret);
// 此时栈已弹出,栈空间已经释放,当主线程再次访问数据时,会是脏数据
}
int main(int argc,char *argv[])
{
pthread_t tid;
void * retval = NULL;
struct ret *ret1;
int err = pthread_create(&tid, NULL, cthread, NULL);
if (err != 0)
{
printf("errno :%d\n", err);
return -1;
}
printf("man thread id:%lu, pid :%u\n", pthread_self(), getpid());
// sleep(1); // 主线程休眠,因为子线程与主线程处于竞争状态,如果主线程先运行完毕,那么子线程将无法运行
pthread_join(tid, (void *)&ret1);
// printf("retval :0x%lx\n", (long)retval);
printf("%d %d\n", ret1->a, ret1->b);
return 0;
}
此时读出的就是脏数据,使用全局变量或者malloc即可解决
8.3pthread_cancel 线程取消
线程可以通过调用pthread_cancel函数来请求取消同一进程中的其他线程
#include <pthread.h>
int pthread_cancel(pthread_t thread);
//返回值 成功返回0, 失败返回 错误编号
在默认情况下,pthread_cancel函数会使得由tid标识的线程的行为表现为如同调用了参数为PTHREAD_CANCELED的pthread_exit函数,但是线程可以忽略取消或者控制如何被取消。
注意:pthread_cancel并不等待线程终止,它仅仅提出请求 (推迟取消)
在线程没有到达取消点时,并不会被取消
void * ptread1(void *arg)
{
while(1);
}
int main()
{
xxx
pthread_cancel(tid);
}
上述代码并不会将ptread1取消,因为并没有到达取消点
当线程执行到上述的任一一个取消点时,才会被取消
线程的可取消状态
PTHREAD_CANCEL_ENABLE 可取消 线程默认
PTHREAD_CANCEL_DISABLE 不可取消 调用pthread_cancel 即使有可取消点也不会被取消
通过pthread_setcancelstate 修改可取消状态
#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
//成功返回0 , 失败返回错误编号
如果没有上述的取消点,可以添加自己的取消点
#include <pthread.h>
void pthread_testcancel(void);
如果线程的可取消状态被设置为了PTHREAD_CANCEL_DISABLE那么这个也是无效的
异步取消
上面所描述的默认的取消类型也称为推迟取消, 线程到达取消点后才会被取消,并不会出现真正的取消
但是,现在的异步取消,可以直接取消线程,不必取消点
#include <pthread.h>
int pthread_setcanceltype(int type, int *oldtype);
//成功返回0 , 失败返回错误编号
type取消类型
PTHREADCANCEL_DEFERREND 默认的取消类型, 推迟取消
PTHREAD_CANCEL_ASYNCHRONOUS 异步取消,可以在任意时间取消,不必等到取消点
void * ptread1(void *arg)
{
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
while(1);
}
int main()
{
xxx
pthread_cancel(tid);
}
这样的话就可以直接取消,而无需等到取消点
8.4 线程清理处理程序
主要是用来释放资源,例如线程意外退出,但是还占用读写锁,那么就有可能造成死锁
与进程中的atexit类似,注册进栈,清理出栈
#include <pthread.h>
void pthread_cleanup_push(void (*routine)(void *),
void *arg);
void pthread_cleanup_pop(int execute);
参数 routine 是 清理函数的地址
arg为 清理函数的参数
清理函数routine只有在以下动作时才会被调用
1、线程调用pthread_exit 终止当前线程时。
2、响应取消请求时
3、用非零execute参数调用pthread_cleanup_pop时。
如果execute为0,不管上述哪种情况,都不会执行清理函数 ,pthread_cleanup_pop将删除上次pthread_cleanup_push调用建立的清理处理程序
上面的两个函数有一个限制, 在与线程相同的作用域中以匹配对的形式使用。
/*************************************************************************
> File Name: thread.c
> 作者:YJK
> Mail: 745506980@qq.com
> Created Time: 2021年05月08日 星期六 14时08分27秒
************************************************************************/
#include<stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
void cleanup(void * arg) //线程清理函数
{
printf("cleanup : %s\n", (char *)arg);
free(arg);
}
void * cthread(void *arg)
{
char * buf = malloc(sizeof(char) * 10);
memset(buf, 'x', 10);
printf("thread 1 start\n");
pthread_cleanup_push(cleanup, (void *)buf); //入栈
printf("thread 1 push complete\n");
pthread_cleanup_pop(0);
//设置为异步取消, 无需等到遇到取消点,发送取消请求后便直接取消
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
while(1);
}
void * cthread2(void *arg)
{
char * buf = malloc(sizeof(char) * 10);
memset(buf, 'a', 10);
printf("thread 2 start\n");
pthread_cleanup_push(cleanup, (void *)buf);
printf("thread 2 push complete\n");
pthread_cleanup_pop(1);
return ((void *)2 );
}
int main(int argc,char *argv[])
{
pthread_t tid, tid2;
void * retval = NULL;
void * retval1 = NULL;
int err = pthread_create(&tid, NULL, cthread, NULL);
if (err != 0)
{
printf("errno :%d\n", err);
return -1;
}
err = pthread_create(&tid2, NULL, cthread2, NULL);
if (err != 0)
{
printf("errno :%d\n", err);
return -1;
}
err = pthread_cancel(tid); //取消线程1 只是一个请求,当线程运行到取消点时才会被取消
if (err != 0)
{
printf("errno :%d\n", err);
return -1;
}
pthread_join(tid, &retval);
printf("thread 1 exit code : %lu\n", (long)retval);
pthread_join(tid2, &retval1);
printf("thread 2 exit code : %lu\n", (long)retval1);
return 0;
}
当pthread_cleanup_pop(0)时,无论是 线程pthread_exit, 还是线程被取消,都不会执行清理程序
9、分离线程
在默认情况下,线程的终止状态会保存直到对该线程调用pthread_join。如果线程已经被分离,线程的底层存储资源可以在线程终止时立即被收回。
在线程被分离后,我们不能用pthread_join函数等待它的终止状态,因为对分离状态的线程调用pthread_join会产生未定义行为。
pthread_detach分离线程
#include <pthread.h>
int pthread_detach(pthread_t thread);
//返回值, 若成功返回0, 失败返回错误编号