线程的启动
任何线程在同一个进程中都可以创建另一个线程,这没有任何的限制。创建线程最常用的就是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 一个指向pthread_t的指针,在那里保存着线程的ID;
attr 一个属性结构;
start_routine 线程要开始执行的程序;
arg 要传递给线程的执行函数的参量。
在这个函数里面,线程指针以及属性结构都是可选参数,你可以给它们传递NULL。
thread参数可以用来保存新创建的线程的ID。在下面的例子你可能会注意到我们传递了NULL到这个参数,表示我们不关心这个新建的线程的ID。如果关心的话,可以像下面这样做:
pthread_t tid;
pthread_create (&tid, …
printf ("Newly created thread id is %d\n", tid);
这是常用的做法,因为有些时候你可能想知道哪个线程运行了哪段代码。
新的线程是通过使用参量arg的start_routine()的开始执行那个而开始的。
线程属性结构(thread attributes structure)
当启动新线程时,你可以设定它的一些特性或让其按照默认特性运行。在开始讨论线程属性函数之前,先看看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;
基本上,数据结构中的各个元素解释如下:
可以对线程操作的函数如下:
- 属性管理
- pthread_attr_destroy()
pthread_attr_init()- 标志(布尔量特性)
- pthread_attr_destroy()
- pthread_attr_getdetachstate()
pthread_attr_setdetachstate()
pthread_attr_getinheritsched()
pthread_attr_setinheritsched()
pthread_attr_getscope()
pthread_attr_setscope() - 堆栈相关
- pthread_attr_getguardsize()
pthread_attr_setguardsize()
pthread_attr_getstackaddr()
pthread_attr_setstackaddr()
pthread_attr_getstacksize()
pthread_attr_setstacksize()
pthread_attr_getstacklazy()
pthread_attr_setstacklazy() - 线程调度相关
pthread_attr_getschedparam()
pthread_attr_setschedparam()
pthread_attr_getschedpolicy()
pthread_attr_setschedpolicy()
一共有大约20个函数,不过在实际使用中我们只要关心一半就行了,因为这些函数都是成对出现的:get与set(只有pthread_attr_init()和pthread_attr_destory()两个函数例外)。
在研究这些属性函数之前,需要注意一点。在使用属性结构之前,需要先调用pthread_attr_init()函数来先初始化这个结构,为其设置合适的属性参数之后再调用pthread_create()函数来创建线程。在线程已经创建之后再修改属性结构是没有任何效果的。
线程属性管理
在线程属性结构使用之前需要先调用pthread_attr_init()函数对其初始话,如下面的代码所示:
… pthread_attr_t attr; … pthread_attr_init (&attr);
你也可以调用pthread_attr-destory()函数来清除这个属性结构,不过很少人会这么使用(如果你需要编写POSIX兼容代码,可能需要使用这个函数)。
标志线程属性(flag thread attribute)
pthread_attr_setdetachstate()、pthread_attr_setinheritsched()和pthread_attr_setscope()这三个函数确定了线程是“分离的”还是“可联接的”、线程是否继承了创建线程的调度属性或使用通过pthread_attr_setschedparam()和pthread_attr_setschedpolicy()函数所设定的调度属性、以及线程是否有“全局”或“进程”的作用域。
要创建一个“可联接”(也就是说其他线程可以通过pthread_join()函数与该线程的终端同步)的线程,要使用的函数如下:
(default) pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_JOINABLE);
要创建一个不能联接的线程(或称分离的),使用的函数如下:
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);
可能你会注意到一直没有调用pthread_attr_setscope()函数。原因就是,本系统只支持“系统”作用域并且在初始化属性的时候是默认的。(“系统”作用域就是说在系统中的全部线程都在互相竞争CPU;“进程”作用域就是指线程只与同进程中的其他线程竞争CPU,系统内核来调度进程。)
(default) pthread_attr_setscope (&attr, PTHREAD_SCOPE_SYSTEM);
线程堆栈属性(stack thread attributes)
线程属性的堆栈参数原型如下:
int pthread_attr_setguardsize (pthread_attr_t *attr, size_t gsize); int pthread_attr_setstackaddr (pthread_attr_t *attr, void *addr); int pthread_attr_setstacksize (pthread_attr_t *attr, size_t ssize); int pthread_attr_setstacklazy (pthread_attr_t *attr, int lazystack);
这些函数都使用属性结构作为函数的第一个参数,第二个参数为下面的参数之一:
- gsize 守护区(guard area)的大小;
- addr 堆栈的地址;
- ssize 堆栈的大小;
- lazystack 表示堆栈是应该预先还是有请求才从物理内存中分配。
- 守护区是紧跟着堆栈后面的一块内存区,线程不会在这个区域写入。如果发生了写入(也就是说堆栈可能开始溢出了),线程就会收到一个SIGSEGV信号。如果守护区的大小为0,就意味着没有守护区。也就是没有堆栈溢出检查。如果守护区大小不是零,那么它最小是系统默认的守护区大小(系统默认大小可以通过使用常量_SC_PAGESIZE调用sysconf()函数得到)。需要注意的是守护区的最小也与一“页”的大小相同。另外也要注意,守护页实际上没有占用物理内存,它只是使用了虚拟地址。
- addr是堆栈的地址。你也可以将其设置为NULL,这样的话,系统就会为你的线程分配与释放堆栈了。指定堆栈的好处就是你可以对堆栈进行深度分析。分析的过程是这样的:分配堆栈区、在堆栈中灌入“签名”(例如把字符串“STACK”在其中反复重复),之后开始线程的运行,当线程运行结束后,你就可以查看堆栈区并可以查看线程抹掉了多少个你在堆栈区中的签名,之后你就可以知道在这次运行中堆栈使用的最大深度了。
- ssize参数确定了堆栈的大小。如果你在addr参数中指定了堆栈地址,ssize就是那个数据区域的大小。如果你没有在addr参数中指定地址而是使用NULL,ssize参数就告诉系统这个堆栈要分配多大的内存。如果你把0设定为参数ssize的值,系统就会为你分配默认堆栈大小的内存。最好不要在指定了addr之后再使用0为ssize的值。你可能要说的是“这里有一个指向对象的指针,这个对象是默认大小的”,问题是在目标大小和传递的值之间是没有任何绑定的。
- 另外,当一个堆栈是通过addr参数而产生的,系统不会自动为其创建堆栈溢出保护,也就是说没有保护区。不过你可以通过mmap()与mprotect()函数对其设定。
- lazystack参数用来设定物理内存是按需分配(PTHREAD_STACK_LAZY)还是提前分配(PTHREAD_STACK_NOTLAZY)。按需分配的优点就是线程不在必要的时候不会使用更多的物理内存。缺点就是在小内存的环境中,有时当线程需要额外的堆栈而这时没有可用内存时,线程会莫名其妙的死掉。如果你使用PTHREAD_STACK_NOTLAZY,你最好自己设定实际的堆栈大小而不要使用系统分配的默认大小,因为系统默认大小有时是很大的。
线程调度属性(scheduling thread attributes)
如果你在调用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_FIFO, SCHED_RR或SCHED_OTHER之一。(现在SCHED_OTHER已经映射到SCHED_RR上了)
- param是包含了一个相关的参数sched_priority的数据结构。可以直接将需要的优先级赋值给这个参数。
- 有一个需要注意的bug就是。当指定线程为PTHREAD_EXPLICIT_SCHED之后,只对调度策略进行了设置的话。当对属性结构初始化之后,param.sched_priority的值为0。这样就是与空闲进程的优先级一样了,也就是说你新建的线程将与空闲进程竞争CPU。这一点要特别注意。
一些例子
- 在下面的例子里面,已经假设需要的引用文件(比如<pthread.h>与<sched.h>)已经被引用,并且要创建的线程通过new_thread()创建的并且已经正确地定义。
- 最常用的创建线程方法就是使用默认值:
pthread_create (NULL, NULL, new_thread, NULL);
- 在这个例子里面,我们使用默认值创建新的线程,并使用NULL作为其唯一的参数。
- 通常你可以传递任何东西到你的新线程。下面的例子里面,我们传递的是数字123:
pthread_create (NULL, NULL, new_thread, (void *) 123);
下面是一个更复杂的例子,用来创建一个非可联接线程,并使用环形调度策略、优先级为15:
pthread_attr_t attr; // initialize the attribute structure pthread_attr_init (&attr); // set the detach state to "detached" pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED); // override the default of INHERIT_SCHED pthread_attr_setinheritsched (&attr, PTHREAD_EXPLICIT_SCHED); pthread_attr_setschedpolicy (&attr, SCHED_RR); attr.param.sched_priority = 15; // finally, create the thread pthread_create (NULL, &attr, new_thread, NULL);
- addr 堆栈的地址;
如果想要看看多线程程序是怎样运行的,可以在命令行输入pidin命令。假设我们的程序叫做spud。如果我们在spud创建一个线程之前运行一次pidin,在spud创建多个线程之后运行一次pidin,下面就是我们可能看到的输出:
# 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表示)。这三个线程都处于就绪状态,也就是说它们都可以使用CPU但是还没有在CPU上运行(另外一个较高优先级的线程可能正在运行)。
何处使用线程最好
在两种情况下使用线程是合适的。
线程就像C++中的操作符重载,在有些时候对每个操作符重载来完成有趣的事可能看可能是个好主意,不过这可能会让代码难于理解。对于多线程也是类似的,你可以创建一堆线程,不过复杂性的增加会让你的代码难于理解并且也会难于维护。另一方面,明智的使用多线程会让你的程序非常简洁的实现预定功能。
在需要并行操作的时候使用线程是不错的,比如同时进行数学处理(图像、数字信号处理等等)。另外当你想要你的程序完成多个独立功能的时候也是合适的,比如分享数据、像网络服务器那样同时服务多个用户。我们在下面详细说明这两种情况。
用于数学计算的多线程
假设我们有一个完成光线跟踪的图形程序。屏幕上的每一个扫描线依赖于主数据库(主数据库则描述着正在生成的实际图像)。这里的关键是:扫描线之间都是互相独立的。这立刻就让这个问题成为一个可以使用多线程来解决的问题。
下面是使用单线程的程序版本:
int
main (int argc, char **argv)
{
int x1;
… // perform initializations
for (x1 = 0; x1 < num_x_lines; x1++) {
do_one_line (x1);
}
… // display results
}
我们可以看到通过x1的不停循环来逐个计算全部的扫描线。
int
main (int argc, char **argv)
{
int x1;
… // perform initializations
for (x1 = 0; x1 < num_x_lines; x1++) {
pthread_create (NULL, NULL, do_one_line, (void *) x1);
}
… // display results
}
这个简单方法有很多问题。首先也是最重要的就是do_one_line()函数只能被修改为使用void *而不是int来作为其参数。这可以通过原型化来简单的修补。
int num_lines_per_cpu;
int num_cpus;
int
main (int argc, char **argv)
{
int cpu;
… // perform initializations
// get the number of CPUs
num_cpus = _syspage_ptr -> num_cpu;
num_lines_per_cpu = num_x_lines / num_cpus;
for (cpu = 0; cpu < num_cpus; cpu++) {
pthread_create (NULL, NULL,
do_one_batch, (void *) cpu);
}
… // display results
}
void *
do_one_batch (void *c)
{
int cpu = (int) c;
int x1;
for (x1 = 0; x1 < num_lines_per_cpu; x1++) {
do_line_line (x1 + cpu * num_lines_per_cpu);
}
}
在这里我们只用了num_cpus个线程。每个线程会在一个CPU上面运行。并且由于我们只使用了少量的线程,就不需要多余的堆栈来浪费内存。你可以留意我们是如何通过系统页面的全局变量_syspage_ptr来获取CPU个数的。
为对称多处理器或单处理器编程
这段代码最好的部分就是即使在单处理器系统中也能正常运行——因为你只会创建一个线程来做所有的工作。让程序在多处理器系统上能够运行快些的灵活性已经足以弥补附加的一个堆栈的开销了。
线程终止的同步
前面我们曾经提到过最初展示的简单代码有诸多问题。它的另外一个问题就是main()函数启动了一堆线程并接着显示结果。那么函数是如何才能知道安全显示结果的时间呢?
让main()函数来处理这个竞争会让我们失去实时操作系统的意义:
int
main (int argc, char **argv)
{
…
// start threads as before
while (num_lines_completed < num_x_lines) {
sleep (1);
}
}
永远也不要写这样的代码!对于这个问题有两个简洁的解决方案:pthread_join()与pthread_barrier_wait()。
联接(Joining)
最简单的同步方法就是在这些线程结束的时候联接它们。联接实际上就意味着等待结束。
联接的实现是一个线程等待另外一个线程的结束。等待线程调用pthread_join()函数:
#include <pthread.h> int pthread_join (pthread_t thread, void **value_ptr);
使用pthread_join()函数时,你把要联接的线程的ID传递给它,另外的一个可选参数就是value_ptr,这个参数可以用来存储被联接线程的结束返回值。(如果对该值不感兴趣可以使用NULL。)
int num_lines_per_cpu, num_cpus;
int main (int argc, char **argv)
{
int cpu;
pthread_t *thread_ids;
… // perform initializations
thread_ids = malloc (sizeof (pthread_t) * num_cpus);
num_lines_per_cpu = num_x_lines / num_cpus;
for (cpu = 0; cpu < num_cpus; cpu++) {
pthread_create (&thread_ids [cpu], NULL,
do_one_batch, (void *) cpu);
}
// synchronize to termination of all threads
for (cpu = 0; cpu < num_cpus; cpu++) {
pthread_join (thread_ids [cpu], NULL);
}
… // display results
}
这次我们传递给pthread_create()的第一个参数就是指向pthread_t的指针。这里就会保存新建线程的线程ID。在第一个for循环结束之后,就会有num_cpus个新线程运行,再加上原有的运行main()函数的线程。我们不太关心消耗全部CPU的main()线程,现在它要花时间等待了。
Related posts:
近期评论