目录
一、前言
二、Thread Attributes
1、Setting the Detached State Attribute(thread5.c)
2、Thread Attributes Scheduling
(1)Scheduling(thread6.c)
三、Canceling a Thread
1、Canceling a Thread(thread7.c)
四、Threads in Abundance
1、Many Threads(thread8.c)
一、前言
在上一板块“进程与信号”中,我们了解了如何在 Linux ( 以及 UNIX ) 中处理进程。这些多处理特性一直是类 unix 操作系统的一个特性。有时,让一个程序同时做两件事可能非常有用,或者至少看起来是这样做的,或者我们可能希望两件或更多的事情以紧密耦合的方式同时发生,但考虑到使用 fork 创建新进程的开销太大。对于这些情况,我们可以使用线程,它允许单个进程执行多任务。
下面我们将学习到:
❑ Creating new threads within a process
❑ Synchronizing data access between threads in a single process
❑ Modifying the attributes of a thread
❑ Controlling one thread from another in the same process
二、Thread Attributes
当我们第一次讨论线程时,我们没有讨论线程属性这个更高级的主题。既然我们已经讨论了同步线程的关键主题,那么我们可以回过头来看看线程本身的这些更高级的特性。线程有相当多的属性可以控制,在这里,我们只看那些你最有可能需要的。你可以在手册页面中找到其他的详细信息。
在前面的所有示例中,在允许程序退出之前,必须使用 pthread_join 重新同步线程。如果希望允许一个线程将数据返回给创建数据的线程,则需要这样做。有时,你既不需要第二个线程向主线程返回信息,也不希望主线程等待它。
假设在主线程继续为用户服务的同时,你创建了第二个线程来脱机正在编辑的数据文件的备份副本。备份完成后,第二个线程就可以终止了。它不需要重新连接主线程。
你可以创建这样的线程。它们被称为分离线程,可以通过修改线程属性或调用 pthread_detach 来创建它们。因为我们想要演示属性,所以我们在这里使用前一种方法。
你需要的最重要的函数是 pthread_attr_init,它初始化线程属性对象。
同样,成功时返回 0,失败时返回错误代码。
还有一个 destroy 函数:pthread_attr_destroy。它的目的是允许清除属性对象。一旦对象被销毁,它就不能再被使用,直到它被重新初始化。
初始化线程属性对象后,可以调用许多附加函数来设置不同的属性行为。我们在这里列出了主要的函数 (也可以在手册页中找到完整的列表,通常在 pthread.h 下面的条目),但只仔细看两个,detachedstate 和 schedpolicy:
正如你所看到的,有相当多的属性可以使用,但幸运的是,你通常不需要使用其中的大多数属性。
❑ detachedstate:此属性允许你避免需要重新连接线程。与大多数 these_set 函数一样,它接受一个指向属性的指针和一个标志来确定所需的状态。pthread_attr_setdetachstate 的两个可能标志值是 PTHREAD_CREATE_JOINABLE 和 PTHREAD_CREATE_DETACHED。默认情况下,该属性的值为 PTHREAD_CREATE_JOINABLE,这样你就可以允许两个线程连接。如果状态设置为 PTHREAD_CREATE_DETACHED,则不能调用 pthread_join 来恢复另一个线程的退出状态。
❑ schedpolicy:它控制如何调度线程。选项包括 SCHED_OTHER、SCHED_RP 和 SCHED_FIFO。默认情况下,该属性为 SCHED_OTHER。另外两种调度类型仅对具有超级用户权限的进程可用,因为它们都具有实时调度,但行为略有不同。SCHED_RR 使用 roundrobin 调度方案,SCHED_FIFO 使用“先进先出”策略。
❑ schedparam:这是 schedpolicy 的一部分,允许控制使用调度策略 SCHED_OTHER 运行的线程的调度。在后面我们会看到一个这样的例子。
❑ inheritsched:该属性有两个可能的值:PTHREAD_EXPLICIT_SCHED 和 PTHREAD_INHERIT_SCHED。默认情况下,该值是 PTHREAD_EXPLICIT_SCHED,这意味着调度是由属性显式设置的。通过将其设置为 PTHREAD_INHERIT_SCHED,新线程将使用其创建线程正在使用的参数。
❑ scope:此属性控制如何计算线程的调度。因为 Linux 目前只支持值 PTHREAD_SCOPE_SYSTEM,我们在这里不再深入讨论。
❑ stacksize:该属性控制线程创建堆栈的大小,以字节为单位设置。这是规范“可选”部分的一部分,仅在定义了 POSIX THREAD_ATTR_STACKSIZE 的实现中得到支持。Linux 在默认情况下实现了具有大量堆栈的线程,因此该特性在 Linux 上通常是多余的。
1、Setting the Detached State Attribute(thread5.c)
对于分离线程示例 thread5.c,你创建一个线程属性,将其设置为分离,然后使用该属性创建一个线程。现在,当子线程完成时,它以正常的方式调用 pthread_exit 。然而,这一次,初始线程不再等待它创建的线程重新加入。在本例中,你使用一个简单的 thread_finished 标志来允许主线程检测子进程是否已经完成,并显示线程仍然在共享变量。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
void *thread_function(void *arg);
char message[] = "Hello World";
int thread_finished = 0;
int main() {
int res;
pthread_t a_thread;
void *thread_result;
pthread_attr_t thread_attr;
res = pthread_attr_init(&thread_attr);
if (res != 0) {
perror("Attribute creation failed");
exit(EXIT_FAILURE);
}
res = pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED);
if (res != 0) {
perror("Setting detached attribute failed");
exit(EXIT_FAILURE);
}
res = pthread_create(&a_thread, &thread_attr, thread_function, (void *)message);
if (res != 0) {
perror("Thread creation failed");
exit(EXIT_FAILURE);
}
(void)pthread_attr_destroy(&thread_attr);
while(!thread_finished) {
printf("Waiting for thread to say it's finished...\n");
sleep(1);
}
printf("Other thread finished, bye!\n");
exit(EXIT_SUCCESS);
}
void *thread_function(void *arg) {
printf("thread_function is running. Argument was %s\n", (char *)arg);
sleep(4);
printf("Second thread setting finished flag, and exiting now\n");
thread_finished = 1;
pthread_exit(NULL);
}
可以看到,设置分离状态允许次要线程独立完成,而不需要初始线程等待它。
How It Works:
代码的两个重要部分是:
声明一个线程属性并初始化它,同时:
它将属性值设置为具有分离状态。
其他细微的区别是创建线程,传递属性的地址,
并且,为了完整性,当你使用了属性时,销毁它们:
2、Thread Attributes Scheduling
让我们看看你可能希望更改的第二个线程属性:调度。更改调度属性非常类似于设置分离状态,但是还有两个函数可以用于查找可用的优先级级别,即 sched_get priority_max 和 sched_get priority_min。
(1)Scheduling(thread6.c)
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
void *thread_function(void *arg);
char message[] = "Hello World";
int thread_finished = 0;
int main() {
int res;
pthread_t a_thread;
void *thread_result;
pthread_attr_t thread_attr;
int max_priority;
int min_priority;
struct sched_param scheduling_value;
res = pthread_attr_init(&thread_attr);
if (res != 0) {
perror("Attribute creation failed");
exit(EXIT_FAILURE);
}
res = pthread_attr_setschedpolicy(&thread_attr, SCHED_OTHER);
if (res != 0) {
perror("Setting schedpolicy failed");
exit(EXIT_FAILURE);
}
res = pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED);
if (res != 0) {
perror("Setting detached attribute failed");
exit(EXIT_FAILURE);
}
res = pthread_create(&a_thread, &thread_attr, thread_function, (void *)message);
if (res != 0) {
perror("Thread creation failed");
exit(EXIT_FAILURE);
}
max_priority = sched_get_priority_max(SCHED_OTHER);
min_priority = sched_get_priority_min(SCHED_OTHER);
scheduling_value.sched_priority = min_priority;
res = pthread_attr_setschedparam(&thread_attr, &scheduling_value);
if (res != 0) {
perror("Setting schedpolicy failed");
exit(EXIT_FAILURE);
}
(void)pthread_attr_destroy(&thread_attr);
while(!thread_finished) {
printf("Waiting for thread to say it's finished...\n");
sleep(1);
}
printf("Other thread finished, bye!\n");
exit(EXIT_SUCCESS);
}
void *thread_function(void *arg) {
printf("thread_function is running. Argument was %s\n", (char *)arg);
sleep(4);
printf("Second thread setting finished flag, and exiting now\n");
thread_finished = 1;
pthread_exit(NULL);
}
How It Works:
这与设置分离状态属性非常相似,不同之处在于你设置的是调度策略。
三、Canceling a Thread
有时,你希望一个线程能够请求另一个线程终止,就像向它发送信号一样。对于线程,有一种方法可以做到这一点,并且与信号处理并行,线程可以在被要求终止时修改它们的行为。
让我们首先看一下请求线程终止的函数:
这非常简单:给定一个线程标识符,你可以请求取消它。在取消请求的接收端,情况稍微复杂一些,但也不是很多。线程可以使用 pthread_setcancelstate 设置其取消状态。
第一个参数是 PTHREAD_CANCEL_ENABLE,它允许它接收取消请求,或者 PTHREAD_CANCEL _DISABLE,它导致它们被忽略。oldstate 指针允许检索以前的状态。如果不感兴趣,可以简单地传递 NULL。如果接受取消请求,则线程可以获得第二级控制,即取消类型,该类型通过 pthread_setcanceltype 设置。
该类型可以接受两个值中的一个:PTHREAD_CANCEL_ASYNCHRONOUS,它使取消请求立即被处理;PTHREAD_CANCEL_DEFERRED,它使取消请求等待,直到线程执行以下函数之一:pthread_join、pthread_cond_wait、pthread_cond_timedwait、pthread_testcancel、sem_wait或sigwait。
我们没有涵盖所有这些调用,因为并非所有调用都是通常需要的。与以往一样,可以在手册页中找到更多的细节。
根据 POSIX 标准,其他可能阻塞的系统调用 (如读取、等待等) 也应该是取消点。在撰写本文时,Linux 中对此的支持似乎还不完全。然而,一些实验确实表明,一些被屏蔽的电话,如睡眠,确实允许取消发生。为了安全起见,你可能需要在您希望取消的代码中添加一些 pthread_testcancel 调用。
同样,oldtype 允许检索以前的状态,如果你对知道以前的状态不感兴趣,则可以传递 NULL。默认情况下,线程以取消状态PTHREAD_CANCEL_ENABLE和取消类型 PTHREAD_CANCEL_DEFERRED 开始。
1、Canceling a Thread(thread7.c)
程序 thread7.c 同样派生自 thread1.c。这一次,主线程向它所创建的线程发送一个取消请求。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
void *thread_function(void *arg);
int main() {
int res;
pthread_t a_thread;
void *thread_result;
res = pthread_create(&a_thread, NULL, thread_function, NULL);
if (res != 0) {
perror("Thread creation failed");
exit(EXIT_FAILURE);
}
sleep(3);
printf("Canceling thread...\n");
res = pthread_cancel(a_thread);
if (res != 0) {
perror("Thread cancelation failed");
exit(EXIT_FAILURE);
}
printf("Waiting for thread to finish...\n");
res = pthread_join(a_thread, &thread_result);
if (res != 0) {
perror("Thread join failed");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
void *thread_function(void *arg) {
int i, res, j;
res = pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
if (res != 0) {
perror("Thread pthread_setcancelstate failed");
exit(EXIT_FAILURE);
}
res = pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);
if (res != 0) {
perror("Thread pthread_setcanceltype failed");
exit(EXIT_FAILURE);
}
printf("thread_function is running\n");
for(i = 0; i < 10; i++) {
printf("Thread is still running (%d)...\n", i);
sleep(1);
}
pthread_exit(0);
}
How It Works:
在以通常的方式创建新线程之后,主线程休眠 (给新线程一些时间启动),然后发出一个取消请求:
在创建的线程中,首先设置 cancel 状态允许取消:
然后你设置取消类型为延迟:
最后,线程等待被取消:
四、Threads in Abundance
到目前为止,我们总是让程序执行的正常线程只创建另一个线程。但是,我们不希望你认为只能创建一个额外的线程。
1、Many Threads(thread8.c)
下面看最后一个例子thread8.c,我们将展示如何在同一个程序中创建几个线程,然后以不同于它们开始时的顺序再次收集它们。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#define NUM_THREADS 6
void *thread_function(void *arg);
int main() {
int res;
pthread_t a_thread[NUM_THREADS];
void *thread_result;
int lots_of_threads;
for(lots_of_threads = 0; lots_of_threads < NUM_THREADS; lots_of_threads++) {
res = pthread_create(&(a_thread[lots_of_threads]), NULL, thread_function, (void *)&lots_of_threads);
if (res != 0) {
perror("Thread creation failed");
exit(EXIT_FAILURE);
}
sleep(1);
}
printf("Waiting for threads to finish...\n");
for(lots_of_threads = NUM_THREADS - 1; lots_of_threads >= 0; lots_of_threads--) {
res = pthread_join(a_thread[lots_of_threads], &thread_result);
if (res == 0) {
printf("Picked up a thread\n");
}
else {
perror("pthread_join failed");
}
}
printf("All done\n");
exit(EXIT_SUCCESS);
}
void *thread_function(void *arg) {
int my_number = *(int *)arg;
int rand_num;
printf("thread_function is running. Argument was %d\n", my_number);
rand_num=1+(int)(9.0*rand()/(RAND_MAX+1.0));
sleep(rand_num);
printf("Bye from %d\n", my_number);
pthread_exit(NULL);
}
如你所见,你创建了许多线程,并允许它们不按顺序完成。这个程序中有一个微妙的错误,如果从启动线程的循环中删除对 sleep 的调用,这个错误就会很明显。我们将它包含进来是为了向你展示在编写使用线程的程序时需要多么小心。我们将在下面解释。
这次你创建了一个线程 id 数组:
循环创建几个线程:
线程本身在退出前等待一个随机的时间:
而在主线程 (原始) 中,你等待取它们,但不是按照你创建它们的顺序:
如果你试图在没有睡眠的情况下运行程序,可能会看到一些奇怪的效果,包括一些线程以相同的参数启动;例如,你可能会看到类似这样的输出:
你知道为什么会发生这种情况吗?线程使用局部变量作为线程函数的参数启动。这个变量在循环中更新。违规的行是:
如果主线程运行得足够快,它可能会改变某些线程的参数 (lots_of_threads)。当对共享变量和多个执行路径不够注意时,就会出现这样的行为。编程线程需要在设计时格外注意!要纠正这个问题,你需要像这样直接传递值:
当然还要改变thread_function:
这在程序 thread8a.c 中显示,并突出显示了更改:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#define NUM_THREADS 6
void *thread_function(void *arg);
int main() {
int res;
pthread_t a_thread[NUM_THREADS];
void *thread_result;
int lots_of_threads;
for(lots_of_threads = 0; lots_of_threads < NUM_THREADS; lots_of_threads++) {
/*modify*/
res = pthread_create(&(a_thread[lots_of_threads]), NULL, thread_function, (void *)lots_of_threads);
if (res != 0) {
perror("Thread creation failed");
exit(EXIT_FAILURE);
}
/*modify*/
/* sleep(1); */
}
printf("Waiting for threads to finish...\n");
for(lots_of_threads = NUM_THREADS - 1; lots_of_threads >= 0; lots_of_threads--) {
res = pthread_join(a_thread[lots_of_threads], &thread_result);
if (res == 0) {
printf("Picked up a thread\n");
} else {
perror("pthread_join failed");
}
}
printf("All done\n");
exit(EXIT_SUCCESS);
}
/*modify*/
void *thread_function(void *arg) {
int my_number = (int)arg;
/*modify*/
int rand_num;
printf("thread_function is running. Argument was %d\n", my_number);
rand_num=1+(int)(9.0*rand()/(RAND_MAX+1.0));
sleep(rand_num);
printf("Bye from %d\n", my_number);
pthread_exit(NULL);
}
POSIX线程到此我们学习了如何在一个进程中创建多个执行线程,其中每个线程共享文件作用域变量。了解了线程可以使用信号量和互斥锁控制对关键代码和数据的访问的两种方式。接下来,了解了如何控制线程的属性,特别是如何将它们与主线程分离,以便主线程不再需要等待它创建的线程完成。在快速查看了一个线程如何请求另一个线程完成任务以及接收线程如何管理这些请求之后,我们给出了一个同时执行多个线程的程序示例。我们没有足够的篇幅来介绍所有与线程相关的函数调用和细微差别,但是我现在应该已经充分了解如何开始使用线程编写自己的程序,并通过阅读手册来研究线程更深奥的方面。
以上,POSIX线程(二)
祝好