Linux应用开发7 线程、线程同步

        线程是参与系统调度的最小单位。它被包含在进程之中,是进程中的实际运行单位。一个线程指的是进程中一个单一顺序的控制流(或者说是执行路线、执行流),一个进程中可以创建多个线程,多个线程实现并发运行,每个线程执行不同的任务。譬如某应用程序设计了两个需要并发运行的任务 task1 和 task2 ,可将两个不同的任务分别放置在两个线程中。
线程——进程中的main函数
        线程是程序最基本的运行单位,而进程不能运行,真正运行的是进程中的线程。当启动应用程序后,系统就创建了一个进程,可以认为进程仅仅是一个容器,它包含了线程运行所需的数据结构、环境变量等信息。
        同一进程中的多个线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack ,我们称为线程栈),自己的寄存器环境( register context)、自己的线程本地存储( thread-local storage )。
        
        在多线程应用程序中,通常一个进程中包括了多个线程,每个线程都可以参与系统调度、被 CPU 执行, 线程具有以下一些特点        
线程不单独存在、而是包含在进程中;
线程是参与系统调度的基本单位;
可并发执行。同一进程的多个线程之间可并发执行,在宏观上实现同时运行的效果;
共享进程资源。同一进程中的各个线程,可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址; 此外,还可以访问进程所拥有的已打开文件、定时器、信号量等等。

分析多进程和多线程两种编程模型的优势和劣势

多进程编程的劣势:
进程间切换开销大。多个进程同时运行(指宏观上同时运行,无特别说明,均指宏观上),微观上依然是轮流切换运行,进程间切换开销远大于同一进程的多个线程间切换的开销,通常对于一些中小型应用程序来说不划算。
进程间通信较为麻烦。每个进程都在各自的地址空间中、相互独立、隔离,处在于不同的地址空间 中,因此相互通信较为麻烦,在上一章节给大家有所介绍。
解决方案便是使用多线程编程,多线程能够弥补上面的问题:
同一进程的多个线程间切换开销比较小。
同一进程的多个线程间通信容易。它们共享了进程的地址空间,所以它们都是在同一个地址空间
中,通信容易。
线程创建的速度远大于进程创建的速度。
多线程在多核处理器上更有优势!
多线程也有它的缺点、劣势,譬如多线程编程难度高,对程序员的编程功底要求比较高,因为在多线程环境下需要考虑很多的问题,例如线程安全问题、信号处理的问题等,编写与调试一个多线程程序比单线程程序困难得多。

并发和并行和串行

⚫ 串行:一件事、一件事接着做

⚫ 并发:交替做不同的事;

并行:同时做不同的事。

串行

        对于串行比较容易理解,它指的是一种顺序执行,譬如先完成 task1 ,接着做 task2 、直到完成 task2 ,然 后做 task3 、直到完成 task3…… 依次按照顺序完成每一件事情,必须要完成上一件事才能去做下一件事,只有一个执行单元,这就是串行运行。

并行
        并行指的是可以并排/并列执行多个任务,并行运行并不一定要同时开始运行、同时结束运行,只需满足在某一个时间段上存在多个任务被多个执行单元同时在运行着
并发

        相比于串行和并行,并发强调的是一种时分复用,与串行的区别在于,它不必等待上一个任务完成之后在做下一个任务,可以打断当前执行的任务切换执行下一个任何,这就是时分复用。在同一个执行单元上,将时间分解成不同的片段(时间片),每个任务执行一段时间,时间一到则切换执行下一个任务,依次这样轮训(交叉/交替执行),这就是并发运行。

线程 ID(重要)

        线程 ID 只有在它所属的进程上下文中才有意义
        进程 ID 使用 pid_t 数据类型来表示,它是一个非负整数。而线程 ID 使用 pthread_t 数据类型来表示, 一个线程可通过库函数 pthread_self() 获取自己的线程 ID ,其函数原型如下所示:
#include <pthread.h>
pthread_t pthread_self(void);
使用该函数需要包含头文件 <pthread.h>
可以使用 pthread_equal() 函数来检查两个线程 ID 是否相等,其函数原型如下所示:
#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);
创建线程
        启动程序时,创建的进程只是一个单线程的进程,称之为初始线程或主线程,本小节我们讨论如何创建一个新的线程。
        主线程可以使用库函数 pthread_create() 负责创建一个新的线程,创建出来的新线程被称为主线程的子线程,其函数原型如下所示:
        
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
使用该函数需要包含头文件 <pthread.h>
thread
pthread_t 类型指针,当 pthread_create() 成功返回时,新创建的线程的线程 ID 会保存在参数 thread 所指向的内存中,后续的线程相关函数会使用该标识来引用此线程。
attr pthread_attr_t 类型指针,指向 pthread_attr_t 类型的缓冲区, pthread_attr_t 数据类型定义了线程的各种属性,关于线程属性将会在 11.8 小节介绍。如果将参数 attr 设置为 NULL ,那么表示将线程的所有属性设置为默认值,以此创建新线程。
start_routine 参数 start_routine 是一个函数指针,指向一个函数,新创建的线程从 start_routine() 函数开始运行,该函数返回值类型为void * ,并且该函数的参数只有一个 void * ,其实这个参数就是 pthread_create() 函数的第四个参数 arg 。如果需要向 start_routine() 传递的参数有一个以上,那么需要把这些参数放到一个结构体中,然后把这个结构体对象的地址作为 arg 参数传入。
arg 传递给 start_routine() 函数的参数。一般情况下,需要将 arg 指向一个全局或堆变量,意思就是说在线程的生命周期中,该 arg 指向的对象必须存在,否则如果线程中访问了该对象将会出现错误。当然也可将参数 arg 设置为 NULL ,表示不需要传入参数给 start_routine() 函数。
返回值: 成功返回 0 ;失败时将返回一个错误号,并且参数 thread 指向的内容是不确定的。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
 printf("新线程: 进程 ID<%d> 线程 ID<%lu>\n", getpid(), pthread_self());
 return (void *)0;
}
int main(void)
{
 pthread_t tid;
 int ret;
 ret = pthread_create(&tid, NULL, new_thread_start, NULL);
 if (ret) {
 fprintf(stderr, "Error: %s\n", strerror(ret));
 exit(-1);
 }
 printf("主线程: 进程 ID<%d> 线程 ID<%lu>\n", getpid(), pthread_self());
 sleep(1);
 exit(0);
}
终止线程
可以通过如下方式终止线程的运行:
线程的 start 函数执行 return 语句并返回指定值,返回值就是线程的退出码;
线程调用 pthread_exit() 函数;
调用 pthread_cancel() 取消线程(将在 11.6 小节介绍);

pthread_exit()函数

#include <pthread.h>
void pthread_exit(void *retval);
使用该函数需要包含头文件 <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
 printf("新线程 start\n");
 sleep(1);
 printf("新线程 end\n");
 pthread_exit(NULL);
}
int main(void)
{
 pthread_t tid;
 int ret;
 ret = pthread_create(&tid, NULL, new_thread_start, NULL);
 if (ret) {
 fprintf(stderr, "Error: %s\n", strerror(ret));
 exit(-1);
 }
 printf("主线程 end\n");
 pthread_exit(NULL);
 exit(0);
}

回收线程

通过调用 pthread_join() 函数阻塞等待线程的终止, 并获取线程的退出码,回收线程资源;pthread_join() 函数原型如下所示:
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
使用该函数需要包含头文件 <pthread.h>
thread pthread_join() 等待指定线程的终止,通过参数 thread (线程 ID )指定需要等待的线程;
retval 如果参数 retval 不为 NULL ,则 pthread_join() 将目标线程的退出状态(即目标线程通过 pthread_exit()退出时指定的返回值或者在线程 start 函数中执行 return 语句对应的返回值)复制到 *retval 所指向的内存区域;如果目标线程被 pthread_cancel() 取消,则将 PTHREAD_CANCELED 放在 *retval 中。如果对目标线程的终止状态不感兴趣,则可将参数 retval 设置为 NULL
返回值: 成功返回 0 ;失败将返回错误码。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
 printf("新线程 start\n");
 sleep(2);
 printf("新线程 end\n");
 pthread_exit((void *)10);
}
int main(void)
{
 pthread_t tid;
 void *tret;
 int ret;
 ret = pthread_create(&tid, NULL, new_thread_start, NULL);
 if (ret) {
 fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
 exit(-1);
 }
 ret = pthread_join(tid, &tret);
 if (ret) {
 fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
 exit(-1);
 }
 printf("新线程终止, code=%ld\n", (long)tret);
 exit(0);
}

取消线程
        在通常情况下,进程中的多个线程会并发执行,每个线程各司其职,直到线程的任务完成之后,该线程中会调用 pthread_exit()退出,或在线程 start 函数执行 return 语句退出。
        有时候,在程序设计需求当中,需要向一个线程发送一个请求,要求它立刻退出,我们把这种操作称为取消线程,也就是向指定的线程发送一个请求,要求其立刻终止、退出。譬如,一组线程正在执行一个运算,一旦某个线程检测到错误发生,需要其它线程退出,取消线程这项功能就派上用场了。
取消一个线程
通过调用 pthread_cancel() 库函数向一个指定的线程发送取消请求
发出取消请求之后,函数 pthread_cancel()立即返回,不会等待目标线程的退出,所以 pthread_cancel()并不会等待线程终止,仅仅只是提出请求。
#include <pthread.h>
int pthread_cancel(pthread_t thread);
使用该函数需要包含头文件 <pthread.h>
参数 thread 指定需要取消的目标线程ID;成功返回 0 ,失败将返回错误码。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
 printf("新线程--running\n");
 for ( ; ; )
 sleep(1);
 return (void *)0;
}
int main(void)
{
 pthread_t tid;
 void *tret;
 int ret;

 /* 创建新线程 */
 ret = pthread_create(&tid, NULL, new_thread_start, NULL);
 if (ret) {
 fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
 exit(-1);
 }
 sleep(1);

 /* 向新线程发送取消请求 */
 ret = pthread_cancel(tid);
 if (ret) {
 fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
 exit(-1);
 }

 /* 等待新线程终止 回收线程*/
 ret = pthread_join(tid, &tret);
 if (ret) {
 fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
 exit(-1);
 }
 printf("新线程终止, code=%ld\n", (long)tret);
 exit(0);
}

取消状态以及类型

        默认情况下,线程是被动响应其它线程发送过来的取消请求的,响应请求然后退出线程。当然,线程可以主动选择不被取消或者控制如何被取消,通过 pthread_setcancelstate()pthread_setcanceltype()来设置线程的取消性状态和类型。

#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);
使用这些函数需要包含头文件 <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg)

{
 /* 设置为不可被取消 */
 pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
 for ( ; ; ) {
 printf("新线程--running\n");
 sleep(2);
 }
 return (void *)0;
}
int main(void)
{
 pthread_t tid;
 void *tret;
 int ret;

 /* 创建新线程 */
 ret = pthread_create(&tid, NULL, new_thread_start, NULL);
 if (ret) {
 fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
 exit(-1);
 }
 sleep(1);

 /* 向新线程发送取消请求 */
 ret = pthread_cancel(tid);
 if (ret) {
 fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
 exit(-1);
 }

 /* 等待新线程终止 ,但实际上无法中止新线程*/
 ret = pthread_join(tid, &tret);
 if (ret) {
 fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
 exit(-1);
 }
 printf("新线程终止, code=%ld\n", (long)tret);
 exit(0);
}
分离线程(处于分离状态的线程,当其终止后,能够自动回收线程资源
默认情况下,当线程终止时,其它线程可以通过调用 pthread_join() 获取其返回状态、回收线程资源,有时,程序员并不关系线程的返回状态,只是希望系统在线程终止时能够自动回收线程资源并将其移除。在这种情况下,可以调用 pthread_detach() 将指定线程进行分离,也就是分离线程, pthread_detach() 函数原型如下所示:
​​​​​​​
#include <pthread.h>
int pthread_detach(pthread_t thread);
使用该函数需要包含头文件 <pthread.h>

一个线程既可以将另一个线程分离,同时也可以将自己分离

pthread_detach(pthread_self());

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
 int ret;

 /* 自行分离 */
 ret = pthread_detach(pthread_self());
 if (ret) {
 fprintf(stderr, "pthread_detach error: %s\n", strerror(ret));
 return NULL;
 }
 printf("新线程 start\n");
 sleep(2); //休眠 2 秒钟
 printf("新线程 end\n");
 pthread_exit(NULL);
}
int main(void)
{
 pthread_t tid;
 int ret;

 /* 创建新线程 */
 ret = pthread_create(&tid, NULL, new_thread_start, NULL);
 if (ret) {
 fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
 exit(-1);
 }
 sleep(1); //休眠 1 秒钟

 /* 等待新线程终止 */
 ret = pthread_join(tid, NULL);
 if (ret)
 fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
 pthread_exit(NULL);
}

 注册线程清理处理函数(重要)

        使用 atexit() 函数注册进程终止处理函数,当进程调用 exit() 退出时就会执行进程终止处理函数;其实,当线程退出时也可以这样做,当线程终止退出时,去执行这样的处理函数, 我们把这个称为线程清理函数(thread cleanup handler)
        与进程不同,一个线程可以注册多个清理函数,这些清理函数记录在栈中,每个线程都可以拥有一个清理函数栈,栈是一种先进后出的数据结构,也就是说它们的执行顺序与注册(添加)顺序相反,当执行完所有清理函数后,线程终止。
        
        线程通过函数 pthread_cleanup_push() pthread_cleanup_pop() 分别负责向调用线程的清理函数栈中添加和移除清理函数
#include <pthread.h>
void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);
使用这些函数需要包含头文件 <pthread.h>

线程属性

        如前所述,调用 pthread_create()创建线程,可对新建线程的各种属性进行设置。在 Linux 下,使用pthread_attr_t 数据类型定义线程的所有属性

        调用 pthread_create() 创建线程时,参数 attr 设置为 NULL ,表示使用属性的默认值创建线程。如果不使用默认值,参数 attr 必须要指向一个 pthread_attr_t 对象,而不能使用 NULL 。当定义 pthread_attr_t 对象之后 ,需要 使用 pthread_attr_init() 函数 对 该对象进 行初始 化操作 ,当对象 不再使 用时, 需要使用 pthread_attr_destroy()函数将其销毁,函数原型如下所示:
​​​​​​​
#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
使用这些函数需要包含头文件 <pthread.h>

线程栈属性

        每个线程都有自己的栈空间,pthread_attr_t 数据结构中定义了栈的起始地址以及栈大小,调用函数 pthread_attr_getstack()可以获取这些信息,函数 pthread_attr_setstack()对栈起始地址和栈大小进行设置,其函数原型如下所示:

#include <pthread.h>
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr, size_t *stacksize);
使用这些函数需要包含头文件 <pthread.h>
函数 pthread_attr_getstack() ,参数和返回值含义如下
attr 参数 attr 指向线程属性对象。
stackaddr 调用 pthread_attr_getstack() 可获取栈起始地址,并将起始地址信息保存在 *stackaddr 中;
stacksize 调用 pthread_attr_getstack() 可获取栈大小,并将栈大小信息保存在参数 stacksize 所指向的内存中;
返回值: 成功返回 0 ,失败将返回一个非 0 值的错误码。
函数 pthread_attr_setstack() ,参数和返回值含义如下:
attr 参数 attr 指向线程属性对象。
stackaddr 设置栈起始地址为指定值。
stacksize 设置栈大小为指定值;
返回值: 成功返回 0 ,失败将返回一个非 0 值的错误码。

线程安全

        当我们编写的程序是一个多线程应用程序时,就不得不考虑到线程安全的问题,确保我们编写的程序是一个线程安全(thread-safe)的多线程应用程序
        一个函数被多个线程(其实也是多个执行流,但是不包括由信号处理函数所产生的执行流)同时调用时,它总会一直产生正确的结果,把这样的函数称为线程安全函数。线程安全函数包括可重入函数,可重入函数是线程安全函数的一个真子集,也就是说可重入函数一定是线程安全函数,但线程安全函数不一定是可重入函数
        如果对该函数进行修改,使用线程同步技术(譬如互斥锁)对共享变量 glob 的访问进行保护,在读写该变量之前先上锁、读写完成之后在解锁。这样,该函数就变成了一个线程安全函数,但是它依然不是可重入函数,因为该函数更改了外部全局变量的值。
        判断一个函数是否为线程安全函数的方法是,该函数被多个线程同时调用是否总能产生正确的结果,如果每次都能产生预期的结果则表示该函数是一个线程安全函数。判读一个函数是否为可重入函数的方法是, 从语言语法角度分析,该函数被多个执行流同时调用是否总能产生正确的结果,如果每次都能产生预期的结果则表示该函数是一个可重入函数。

==================================================================================================================================================

线程同步

        本章来聊一聊线程同步这个话题,对于一个单线程进程来说,它不需要处理线程同步的问题,所以线程同步是在多线程环境下可能需要注意的一个问题。线程的主要优势在于,资源的共享性,譬如通过全局变量来实现信息共享,不过这种便捷的共享是有代价的,那就是多个线程并发访问共享数据所导致的数据不一致的问题。
线程同步是为了对共享资源的访问进行保护
保护的目的是为了解决数据一致性的问题
出现数据一致性问题其本质在于进程中的多个线程对共享资源的并发访问(同时访问)
方法一,互斥锁,当不再需要互斥锁时,应该将其销毁,通过调用 pthread_mutex_destroy() 函数来销毁互斥锁
方法二,条件变量,条件变量是线程可用的另一种同步机制。条件变量用于自动阻塞线程,直到某个特定事件发生或某个条件满足为止,通常情况下,条件变量是和互斥锁一起搭配使用的。使用条件变量主要包括两个动作:
一个线程等待某个条件满足而被阻塞;
另一个线程中,条件满足时发出“信号”。
方法三,自旋锁,自旋锁与互斥锁很相似,从本质上说也是一把锁,在访问共享资源之前对自旋锁进行上锁,在访问完成后释放自旋锁(解锁);事实上,从实现方式上来说,互斥锁是基于自旋锁来实现的,所以自旋锁相较于互斥锁更加底层。
方法四,读写锁,互斥锁或自旋锁要么是加锁状态、要么是不加锁状态,而且一次只有一个线程可以对其加锁。读写锁有3 种状态:读模式下的加锁状态(以下简称读加锁状态)、写模式下的加锁状态(以下简称写加锁状态)和不加锁状态(见),一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁。因此可知, 读写锁比互斥锁具有更高的并行性
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值