2-6-1-1 QNX编程入门之进程和线程(四)

阅读前言

本文以QNX系统官方的文档英文原版资料“Getting Started with QNX Neutrino: A Guide for Realtime Programmers”为参考,翻译和逐句校对后,对在QNX操作系统下进行应用程序开发及进行资源管理器编写开发等方面,进行了深度整理,旨在帮助想要了解QNX的读者及开发者可以快速阅读,而不必查看晦涩难懂的英文原文,这些文章将会作为一个或多个系列进行发布,从遵从原文的翻译,到针对某些重要概念的穿插引入,以及再到各个重要专题的梳理,大致分为这三个层次部分,分不同的文章进行发布,依据这样的原则进行组织,读者可以更好的查找和理解。


1. 进程和线程

1.3. 线程和进程

2-6-1-1 QNX编程入门之进程和线程(一)

2-6-1-1 QNX编程入门之进程和线程(二)

2-6-1-1 QNX编程入门之进程和线程(三)

接前面章节内容继续。

1.3.3. 启动一个线程

既然我们已经了解了如何启动另一个进程,那么让我们来看看如何启动另一个线程。

任何线程都可以在同一进程中创建另一个线程,没有任何限制(当然,除了内存空间!)。最常见的方法是通过POSIX pthread_create() 调用:

#include <pthread.h>

int
pthread_create (pthread_t *thread,
                const pthread_attr_t *attr,
                void *(*start_routine) (void *),
                void *arg);

pthread_create() 函数需要四个参数:

  • thread,指向存储线程 ID 的 pthread_t 的指针。
  • attr,属性结构。
  • start_routine,线程开始的例程。
  • arg,传递给线程的 start_routine 例程的参数。

请注意, 线程指针(thread)和属性结构体(attr )是可选的,你可以将它们传递为 NULL。
参数 thread 可用于存储新建线程的线程 ID。你会注意到,在下面的示例中,我们将传递一个 NULL,这意味着我们并不关心新创建线程的 ID 是什么。如果我们关心的话,可以这样做:

pthread_t tid;
pthread_create (&tid, …);
printf ("Newly created thread id is %d\n", tid);

这种用法其实很典型,因为你通常会想要知道哪个线程 ID 正在运行哪段代码。

这里有一个微妙的小问题。新创建的线程有可能在线程 ID(参数 tid 指针指向的内容)被填充之前就已开始运行,这意味着在将 tid 用作全局变量时一定要小心这一点。上面所显示的用法没有问题,因为 pthread_create() 调用已返回,这意味着 tid 值已正确填入。

新线程开始执行 start_routine(),参数为 arg。

1.3.3.1. 线程属性结构体

当你启动一个新线程时,它可以采用一些定义明确的默认值,或者你也可以显式地指定其特性。
在我们深入探讨线程属性函数之前,先来看看 pthread_attr_t 数据类型:

typedef struct {
    int                 __flags;
    size_t              __stacksize;
    void                *__stackaddr;
    void                (*__exitfunc)(void *status);
    int                 __policy;
    struct sched_param  __param;
    unsigned            __guardsize;
} pthread_attr_t;


/* 
这些字段的基本使用方法如下: 
__flags
非数字(布尔)特性(例如,线程应 "可分离 detached" 运行还是 "可连接 joinable" 运行)。

stacksize、 stackaddr 和 guardsize
堆栈的规格。

__exitfunc
线程退出时执行的函数。

__policy and __param
调度相关参数。
*/

以下是可供使用的函数:

属性管理相关:

标志(布尔特征)相关:

堆栈相关 :

调度相关:


这个列表看起来很长,但实际上我们只需要关心其中一半,因为它们是以 "get "和 "set" 的形式成对出现的(不过,pthread_attr_init()pthread_attr_destroy() 除外) 。

在研究属性相关函数之前,有一点需要注意。在使用属性结构体之前,必须调用 pthread_attr_init() 对其进行初始化, 然后使用相应的 pthread_attr_set*() 函数对其进行设置,最后再调用 pthread_create() 函数进行线程的创建。 在线程创建之后再更改属性结构体的内容是没有效果的。
 

1.3.3.1.1. 线程属性管理

在使用属性结构体之前,必须调用 pthread_attr_init() 函数对其进行初始化。

...
pthread_attr_t  attr;
...
pthread_attr_init (&attr);

你可以调用 pthread_attr_destroy() 函数来 “反初始化” 线程属性结构体,但几乎没人会这么做(除非你的代码要遵循 POSIX 标准)。

在接下来的描述中,我已用 “(default)” 标记出了默认值。

1.3.3.1.2. 线程属性“flags”

让我们从布尔型线程属性开始说起。

  • 要创建一个 “可连接(joinable)” 的线程(意思是另一个线程能够通过 pthread_join() 函数与该线程的“终止”进行同步),你可以像下面这样:
(default)
pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_JOINABLE);

要创建一个无法被连接的线程(称为 "detached" 线程),可以使用下面的方法:

pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED);

  • 如果希望线程继承创建者线程的调度属性(即具有相同的调度策略和优先级),你可以这样:
(default)
pthread_attr_setinheritsched (&attr, PTHREAD_INHERIT_SCHED);

要创建一个由属性结构体本身指定调度属性的线程(使用 pthread_attr_setschedparam()pthread_attr_setschedpolicy() 进行调度属性设置),可以使用下面的方法:

pthread_attr_setinheritsched (&attr, PTHREAD_EXPLICIT_SCHED);

  • 在 QNX Neutrino 7.0.1 或更高版本中,如果你不希望线程在创建时就进入挂起状态(suspended state),可以像下面这样:
(default)
pthread_attr_setsuspendstate_np (&attr, PTHREAD_NOT_SUSPENDED);

如果你希望线程在创建时就处于挂起状态(suspended),你可以使用下面这个方式:

pthread_attr_setsuspendstate_np(&attr, PTHREAD_SUSPENDED);

  • 最后,你永远不会调用 pthread_attr_setscope()。为什么?因为 QNX Neutrino 只支持 "system" 作用域,这也是属性结构体初始化时的默认值。(“system”作用域意味着所有线程都在系统中互相竞争 CPU;而另一个值“process”则意味着线程在进程内相互竞争 CPU,而内核则对进程进行调度)。 如果你坚持要调用,只能按如下方式进行调用:
(default)
pthread_attr_setscope (&attr, PTHREAD_SCOPE_SYSTEM);

1.3.3.1.3. 线程属性“stack”

设置线程属性堆栈参数的函数如下:

int
pthread_attr_setstack (pthread_attr_t *attr, void *addr, size_t ssize );

int
pthread_attr_setstackaddr (pthread_attr_t *attr, void *addr);

int
pthread_attr_setguardsize (pthread_attr_t *attr, size_t gsize);

int
pthread_attr_setstacklazy (pthread_attr_t *attr, int lazystack);

int
pthread_attr_setstackprealloc (pthread_attr_t * attr, size_t psize);

int
pthread_attr_setstacksize (pthread_attr_t *attr, size_t ssize);

这些函数都将属性结构作为它们的第一个参数;其他参数则从以下各项中选取:

addr栈的地址(如果由你提供栈地址的话)。

gsize“保护(guard)” 区域的大小。

lazystack指示栈是应该按需从物理内存中分配,还是预先分配。

psize要为线程的 MAP_LAZY 栈预分配的内存量。

ssize栈的大小。

保护区域(guard area)是紧挨着栈(stack)的一块内存区域,线程不能对其进行写入操作。如果线程对其进行了写入(意味着栈即将溢出),那么线程将会收到 SIGSEGV 信号。如果保护区域大小(guardsize)为 0,则表示不存在保护区域。这也意味着不会进行栈溢出检查。如果 guardsize 不为 0,那么它至少会被设置为系统范围内的默认保护区域大小(你可以通过调用 sysconf() 函数,并传入常量 _SC_PAGESIZE 来获取该默认大小)。需要注意的是,保护区域大小至少会和一个“页面”一样大(例如,在 x86 处理器上是 4KB)。另外,还要注意保护区域的“页面”并不会占用任何实际的物理内存,它只是通过虚拟地址(内存管理单元,即 MMU)的 “技巧” 来实现的。

addr 是栈的地址(如果你提供了的话)。你可以将其设置为 NULL,这意味着需要系统将为线程进行分配(并且会自动释放!)栈。指定栈的好处在于你可以事后对栈的深度进行分析。具体做法是分配一个栈区域,用一个“标识”(例如,反复重复的字符串“STACK”)填充它,然后让线程运行。当线程运行结束后,你查看栈区域,看看线程覆盖你的标识到了什么程度,这样就能得知在这次特定运行过程中所使用的栈的最大深度。

ssize 参数用于指定栈的大小。如果你通过 addr 提供了栈,那么 ssize 就应该是该数据区域的大小。如果你没有通过 addr 提供栈(也就是传递了 NULL),那么 ssize 参数会告知系统应该为你分配多大的栈。如果你将 ssize 指定为 0,系统会为你选择默认的栈大小。显然,将 ssize 指定为 0 同时又通过 addr 指定一个栈的地址,这种做法很不好。这样做,实际上相当于在说“这是一个指向某个对象的指针,而这个对象的大小是某个默认值”。 但是,问题在于你所指定的对象大小和传递的地址值之间没有关联。

如果通过 addr 提供了栈,那么对于该线程来说不存在自动的栈溢出保护机制(即不存在保护区域)。不过,你当然可以自己使用 mmap()mprotect() 函数来设置相关保护机制。

最后,lazystack 参数用于指示物理内存是应该按需分配(使用值 PTHREAD_STACK_LAZY)还是预先全部分配(使用值 PTHREAD_STACK_NOTLAZY)。按需分配栈(根据需要分配)的好处在于线程不会占用比实际所需更多的物理内存。预先全部分配方法的好处在于,在内存不足的环境中,线程在运行期间需要额外的栈空间但又没有剩余内存时,不会莫名其妙地终止运行。如果你使用后一种方法(即 PTHREAD_STACK_NOTLAZY),你很可能会想要设置栈的实际大小,而不是采用默认大小,因为默认大小通常是比较大的。

1.3.3.1.4. 线程属性“sheduling”

最后,如果你确实为 pthread_attr_setinheritsched() 函数指定了 PTHREAD_EXPLICIT_SCHED 参数,那么你就需要一种方法来指定即将创建的线程的调度策略以及优先级。

可以通过以下两个函数来实现:

int
pthread_attr_setschedparam (pthread_attr_t *attr,
                            const struct sched_param *param);
int
pthread_attr_setschedpolicy (pthread_attr_t *attr,
                             int policy);

调度策略很简单,它取值为 SCHED_FIFOSCHED_RRSCHED_OTHER 其中之一。

目前,SCHED_OTHER 也是被映射为 SCHED_RR

param 是一个结构体,这里与之相关的成员只有一个:sched_priority。通过直接赋值的方式将该值设置为期望的优先级即可。

需要留意的一个常见错误是指定了 PTHREAD_EXPLICIT_SCHED 参数后,却只设置了调度策略。问题在于,在一个已初始化的属性结构中,param.sched_priority 的值为 0。这个优先级与空闲进程的优先级相同,这意味着你新创建的线程将会与空闲进程竞争 CPU 资源。这种情况我亲身经历过,印象深刻呢。(偷笑)

已经有很多人被这个问题困扰过了,所以优先级 0 是预留给空闲线程的。你根本不能让一个线程以优先级 0 来运行。

1.3.3.2. 几个例子

让我们来看一些示例。我们假定已经包含了合适的头文件(<pthread.h><sched.h>),并且即将要创建的线程名为 new_thread(),它的函数原型声明和定义都是正确的。

创建线程最常见的方式就是简单地使用默认值:

pthread_create (NULL, NULL, new_thread, NULL);

在上述示例中,我们使用默认值创建了新线程,并给它传递了一个 NULL 作为其唯一的参数(也就是上面 pthread_create() 调用中的第三个 NULL)。

通常来说,你可以通过参数(arg 字段)给新线程传递任何你想要传递的内容。在这里我们传递数字 123

pthread_create (NULL, NULL, new_thread, (void *) 123);

下面是一个更复杂些的示例,创建一个不可结合的(non-joinable)线程,该线程采用轮转调度算法且优先级为 15

pthread_attr_t attr;

// 初始化属性结构
pthread_attr_init (&attr);

// 将分离状态设置为“分离”
pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED);

// 覆盖默认的继承调度属性设置
pthread_attr_setinheritsched (&attr, PTHREAD_EXPLICIT_SCHED);
pthread_attr_setschedpolicy (&attr, SCHED_RR);
attr.param.sched_priority = 15;

// 最后,创建线程
pthread_create (NULL, &attr, new_thread, NULL);

要看看一个多线程程序 “是什么样子的”,你可以在 shell 中运行 pidin 命令。假设我们的程序名为 spud。如果我们在 spud 创建线程之前运行一次 pidin 命令,然后在 spud 又创建了两个线程之后(总共三个线程)再运行一次 pidin 命令,输出结果看起来会是这样的(我对 pidin 的输出进行了简化,只展示 spud 的相关内容):

# pidin
pid    tid name               prio STATE       Blocked
12301   1 spud                10r READY
# pidin
pid    tid name               prio STATE       Blocked
12301   1 spud                10r READY
12301   2 spud                10r READY
12301   3 spud                10r READY

如你所见,进程 spud(进程 ID 为 12301)有三个线程(在 “tid” 列下可以看到)。这三个线程以优先级 10 运行,采用轮转调度算法(由 10 后面的 “r” 表示)。这三个线程都处于 “READY” 状态,这意味着它们能够使用 CPU,但当前并没有在 CPU 上运行(当前有另一个优先级更高的线程正在运行)。

既然我们已经了解了创建线程的所有相关内容,现在让我们来看看如何以及在哪些地方会用到线程吧。

1.3.3.3. 在这里使用线程是个好主意

有两类问题比较适合运用线程来解决。

线程就好比是 C++ 中的运算符重载;在当时看来,用一些有意思的用法去重载每一个运算符似乎是个好主意,但这会让代码变得难以理解。线程方面也是类似的情况,你可以创建大量的线程,然而额外增加的复杂性会使你的代码难以理解,进而难以维护。另一方面,明智地使用线程,则会让代码在功能上显得非常简洁明了。

在能够并行化操作的情况下,线程的作用很大,能让人马上想到很多数学问题(比如图形处理、数字信号处理等等)。另外,当你希望一个程序在共享数据的同时执行多个独立功能时,线程也非常有用,例如一个同时为多个客户端提供服务的网络服务器就是如此。我们接下来会对这两类情况进行探讨。


未完待续,请继续关注本专栏内容……

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星原飞火

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值