POSIX线程_学习笔记
线程:在一个程序中的多个执行路线叫做线程,线程是一个进程内部的一个控制序列。
fork()系统调用与创建新线程的区别:
当进程执行fork调用时,将创建出该进程的一份副本。这个新进程拥有自己的变量和自己的PID,它的时间调度也是独立的,它的执行(通常)几乎完全独立于父进程。
当在进程中创建一个新线程时,新的执行线程将拥有自己的栈(因此也有自己的局部变量),但与它的创建者共享全局变量、文件描述符、信号处理函数和当前目录
状态。
一、第一个线程程序
线程创建函数:pthread_create
#include <pthread.h> int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
第一个参数是指向 pthread_t 类型数据的指针。 线程被创建时,这个指针指向的变量中将被写入一个标识符,我们用该标识符来引用新线程。
第二个参数用于设置线程的属性。我们一般不需要特殊的属性,所以只需要设置该参数为 NULL。
最后两个参数分别告诉线程将要启动执行的函数和传递给该函数的参数。
void *(*start_routine)(void *)
上面一行告诉我们必须要传递一个函数地址,该函数以一个指向void的指针为参数,返回的也是一个指向void的指针。因此,可以传递一个
任一类型的参数并返回一个任一类型的指针。
用fork调用后,父子进程将在同一位置继续执行下去,只是fork掉用的返回值是不同的;但对于新线程来说,我们必须明确地提供给它一个函数
指针,新线程将在这个位置开始执行。
该函数调用成功时返回 0 ,失败则返回错误代码。
线程终止函数:pthread_exit
#include <pthread.h> void pthread_exit(void *retval);
线程通过调用pthread_exit函数终止执行,就如同进程在结束时调用exit函数一样。
这个函数的作用是,终止调用它的线程并返回一个指向某个对象的指针。注意不能用它来返回一个指向局部变量的指针,因为线程调用该函数后,这个局部
变量就不存在了,这个将引起严重的程序漏洞。
pthread_join 函数
pthread_join函数在线程中的作用等价于进程中用来收集子进程信息的wait函数。
#include <pthread.h> int pthread_join(pthread_t th, void **thread_return);
第一个参数指定了将要等待的线程,线程通过 pthread_create返回的标识符来指定。
第二个参数时一个指针,它指向另一个指针,而后者指向线程的返回值。与 pthread_create类似,这个函数在成功时返回0,失败时返回错误代码。
示例: 一个简单的线程程序 thread1.c
下面的程序创建一个新的线程,新线程与原先的线程共享变量,并在结束时向原先的线程返回一个结果。
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <pthread.h> char message[] = "Hello World!"; void *thread_function(void *arg){ printf("thread_funcion is running...Argument was %s\n",(char *)arg); sleep(3); strcpy(message, "Bye...."); pthread_exit("thank you for the CPU time"); } int main(){ int res; pthread_t a_thread; void *thread_result; res = pthread_create(&a_thread, NULL, thread_function, (void *)message); if (res != 0){ perror("Thread creation 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); } printf("Thread joined, it returned %s\n", (char *)thread_result); printf("Message is now %s\n", message); exit(EXIT_SUCCESS); }
分析:
我们向pthread_create函数传递了一个pthread_t类型对象的地址,今后可以用它来引用这个新线程。我们不想改变默认的线程属性,所以设置第二个参数为 NULL。最后两个参数
分别为将要调用的函数和一个传递给该函数的参数。
如果这个调用成功了,就会有两个线程在运行:原先的线程(main)继续执行pthread_create 后面的代码,而新的线程开始执行 thread_function 函数。
原先的线程在查明新线程已经启动后,将调用 pthread_join 函数,如下所示:
res = pthread_join(a_thread, &thread_result);
我们给这个函数传递两个参数,一个是正在等待其结束的线程的标识符,另一个是指向线程返回值的指针。
这个函数将等到它所指定的线程终止后才返回,然后主线程将打印新线程的返回值和全局变量message 的值,最后退出。
新线程在 thread_function 函数中开始执行,它先打印出自己的参数,休眠一会儿,然后更新全局变量,最后退出并向主线程返回一个字符串。
新线程修改了数组 message, 而原先的线程也可以访问该数组。如果我们调用的是 fork 而不是 pthread_create,就不会有这样的效果。
二、同时执行
现在我们将编写一个程序来验证两个线程的执行是同时进行的(如果是在一个单处理器系统上,线程的同时执行就需要靠CPU在线程之间的快速切换来实现)。因为还
未介绍到任何可以帮助我们有效地完成这一工作的线程同步函数,在这个程序中我们是在两个线程之间使用 轮询 技术,所以它的效率很低。同时,我们的程序仍然
利用这一事实:即除了局部变量外,所有其他变量都将在一个进程中的所有线程之间共享。
示例: 两个线程同时执行 thread2.c
现在我们对 thread1.c 稍加修改。我们增加了另一个文件范围变量来测试哪个线程正在运行,如下所示:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <pthread.h> char message[] = "Hello World!"; int run_now = 1; void *thread_function(void *arg){ printf("thread_funcion is running...Argument was %s\n",(char *)arg); sleep(3); int print_count2 = 0; while (print_count2++ < 20){ if (2 == run_now){ printf("2"); run_now = 1; } else{ sleep(1); } } strcpy(message, "Bye...."); pthread_exit("thank you for the CPU time"); } int main(){ int res; pthread_t a_thread; void *thread_result; res = pthread_create(&a_thread, NULL, thread_function, (void *)message); int print_count1 = 0; while (print_count1++ < 20){ if (1 == run_now){ printf("1"); run_now = 2; } else { sleep(1); } } if (res != 0){ perror("Thread creation 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); } printf("Thread joined, it returned %s\n", (char *)thread_result); printf("Message is now %s\n", message); exit(EXIT_SUCCESS); }
运行结果:
分析:
每个线程通过设置 run_now 变量的方法来通知另一个线程开始运行,然后它会等待另一个线程改变了这个变量的值后再次运行。
这个例子显示了两个线程之间自动交替执行,同时也再次阐明了一个观点,即两个线程共享 run_now 变量。
三、同步
上面的例子中,两个线程之间进行切换的方法是非常笨拙而且没有效率的。幸运的是,专门有一组设计好的函数为我们提供了更好的
控制线程执行和访问代码临界区的方法。包括 信号量 和 互斥量 等。
有两组接口函数用于信号量。一组取自POSIX 的实时扩展,用于线程。另一组被称为系统 V信号量,常用于进程的同步。
这两组接口函数虽然很相近,但不保证它们之间可以互换,而且它们使用的函数调用也各不相同。
信号量是一个特殊类型的变量,它可以被增加或减少,但对其的关键访问被保证是原子操作,即使在一个多线程程序中也是如此,这意味着
如果一个程序中有两个(或多个)线程试图改变一个信号量的值,系统将保证所有的操作都将一次进行。但如果是普通变量,来自同一程序中的
不同线程的冲突操作所导致的结果将是不确定的。
信号量函数的名字都以 sem_ 开头,而不像大多数线程函数那样以pthread_ 开头。线程中使用的基本信号量函数有 4 个,它们都非常的简单。
信号量通过 sem_init 函数创建,它的定义如下:
#include <semaphore.h> int sem_init(sem_t *sem, int pshared, unsigned int value);
这个函数初始化由 sem 指向的信号量对象,设置它的共享选项,并给它一个初始的整数值。
pshared 参数控制信号量的类型,如果其值为 0,就表示这个信号量是当前进程的局部信号量,否则,
这个信号量就可以在多个进程之间共享。
接下来的两个函数控制信号量的值,它们的定义如下所示:
#include <semaphore.h> int sem_wait(sem_t * sem); int sem_post(sem_t * sem);
这两个函数都以一个指针为参数,该指针指向的对象是由 sem_init 调用初始化的信号量。
sem_post 函数的作用是以原子操作的方式给信号量的值加 1。所谓原子操作是指,如果两个线程企图同时给一个信号量加 1,它们之间不会互相干扰,
而不像如果两个程序同时对一个文件进行读取、增加、写入操作时可能会引起冲突。信号量的值总是会被正确地加 2,因为有两个线程试图改变它。
sem_wait 函数以原子操作的方式将信号量的值减 1,但它会等待直到信号量有个非零值才会开始减法操作。因此,如果对值为 2 的信号量调用sem_wait,
线程将继续执行,但信号量的值会减到 1。如果对值为 0 的信号量调用 sem_wait,这个函数就会等待,直到有其它线程增加了该信号量的值使其不再是 0 为止。
如果两个线程同时在 sem_wait 调用上等待同一个信号量变为非零值,那么当该信号量被第三个线程增加1 时,只有其中一个等待线程将开始对信号量减 1,然后
继续执行,另外一个线程还将继续等待。
最后一个信号量函数是 sem_destroy。这个函数的作用是:用完信号量后对它进行清理。它的定义如下:
#include <semaphore.h>
int sem_destroy(sem_t * sem);
与前几个函数一样,这个函数也以一个信号量指针作为参数,并清理该信号量拥有的所有资源。
如果企图清理的信号量正在被一些线程等待,就会收到一个错误。
与大多数的Linux函数一样,这些函数在成功时都会返回 0。
示例:一个线程信号量 thread3.c
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <pthread.h> #include <semaphore.h> #define WORK_SIZE 1024 char work_area[WORK_SIZE]; sem_t bin_sem; void *thread_function(void *arg){ sem_wait(&bin_sem); while (strncmp("end", work_area, 3) != 0){ printf("You input %d characters\n",strlen(work_area) - 1); sem_wait(&bin_sem); } pthread_exit(NULL); } int main(){ int res; pthread_t a_thread; void *thread_result; res = sem_init(&bin_sem, 0, 0); if (res != 0){ perror("Semaphore initalization failed..."); exit(EXIT_FAILURE); } res = pthread_create(&a_thread, NULL, thread_function, NULL); if (res != 0){ perror("Thread creation failed"); exit(EXIT_FAILURE); } printf("Input some text. Enter 'end' to finish\n"); while (strncmp("end", work_area, 3) != 0){ fgets(work_area, WORK_SIZE, stdin); sem_post(&bin_sem); } printf("\nWaiting for thread to finish ... \n"); res = pthread_join(a_thread, &thread_result); if (res != 0){ perror("Thread join failed"); exit(EXIT_FAILURE); } printf("Thread joined\n"); sem_destroy(&bin_sem); exit(EXIT_SUCCESS); }
说明:
第一个重要的改动是包含了头文件 semaphore.h, 它使我们可以访问信号量函数。
然后,定义一个信号量和几个变量,并在创建新线程之前对信号量进行初始化。
如下所示:
#define WORK_SIZE 1024 char work_area[WORK_SIZE]; sem_t bin_sem; int main(){ int res; pthread_t a_thread; void *thread_result; res = sem_init(&bin_sem, 0, 0); if (res != 0){ perror("Semaphore initalization failed..."); exit(EXIT_FAILURE); }
注意,我们将这个信号量的初始值设置为 0。
在 main 函数中,启动新线程后,我们从键盘读取一些文本并把它们放到工作区 work_area 数组中,然后调用 sem_post 增加信号量的值。
如下所示:
printf("Input some text. Enter 'end' to finish\n"); while (strncmp("end", work_area, 3) != 0){ fgets(work_area, WORK_SIZE, stdin); sem_post(&bin_sem); }
在新的线程中,我们等待信号量,然后统计来自输入的字符个数。如下所示:
sem_wait(&bin_sem); while (strncmp("end", work_area, 3) != 0){ printf("You input %d characters\n",strlen(work_area) - 1); sem_wait(&bin_sem); } pthread_exit(NULL);
设置信号量的同时,我们等待这键盘的输入。当输入到达时,我们释放信号量,允许第二个线程在第一个线程再次读取键盘输入之前统计出输入字符的个数。
这两个线程共享一个 work_area 数组。为了方便理解,省略了一些错误检查。例如,没有检查 sem_wait 函数的返回值。 在产品代码中,除非有特别充足的理由才
省略错误检查,否则总是应该检查函数的返回值。
运行结果:
初始化信号量时,我们把它的值设置为 0。这样,在线程函数启动时,sem_wait 函数调用就会阻塞并等待信号量变为非零值。
我们容易忽略程序设计上的细微错误,而该错误会导致程序运行结果中的一些细微错误。我们将上面的程序修改为thread3.c。
它偶尔会将来自键盘的输入用事先准备好的文本自动替换掉。把main 函数中读数据循环修改为:
printf("Input some text. Enter 'end' to finish\n"); while (strncmp("end", work_area, 3) != 0){ if (strncmp(work_area, "fast", 4) == 0){ sem_post(&bin_sem); strcpy(work_area, "wheeee..."); } else{ fgets(work_area, WORK_SIZE, stdin); } sem_post(&bin_sem); }
运行结果:
问题在于,单词统计线程在我们尝试连续快速地给它两组不同的单词去统计时,没有时间去执行。
我们可以再加一个信号量来解决这个问题,让主线程等到统计线程完成字符个数的统计之后再继续执行,更简单的方式是使用 互斥量。
用 互斥量 进行同步
另一种用在多线程程序中的同步访问方法是使用 互斥量。它允许程序员锁住某个对象,使得每次只能有一个线程访问它。为了控制对关键
代码的访问,必须在进入这段代码之前锁住一个互斥量,然后在完成操作后解锁它。
用于互斥量的基本函数和用于信号量的函数非常相似,它们的定义如下:
#include <pthread.h> int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr); int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); int pthread_mutex_destroy(pthread_mutex_t *mutex);
与其它函数一样,成功时返回 0,失败时返回错误代码,但这些函数都不设置 errno,你必须对函数的返回代码进行检查。
与信号量相似,这些函数的参数都是一个先前声明过的对象的指针。 对于互斥量来说,这个对象的类型为 pthread_mutex_t。
pthread_mutex_init 函数中的属性参数允许我们设置互斥量的属性,而属性控制着互斥量的行为。
我们可以传递 NULL给属性指针,从而使用其默认行为。
示例:线程互斥量
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <pthread.h> #include <semaphore.h> #define WORK_SIZE 1024 char work_area[WORK_SIZE]; // 声明工作区 int time_to_exit = 0; pthread_mutex_t work_mutex; // 声明互斥量 /* protects both work_area and time_to_exit */ void *thread_function(void *arg){ sleep(1); pthread_mutex_lock(&work_mutex); // 新线程首先试图对互斥量加锁。如果它已经被锁住,这个调用将被阻塞到它被释放为止。 while (strncmp("end", work_area, 3) != 0){ printf("You input %d characters\n", strlen(work_area) - 1); // 如果不想退出,就统计字符 work_area[0] = '\0'; // 然后把工作区第一个字符设置为null,并用这个方法通知读取输入的线程:我们完成了字符统计。 pthread_mutex_unlock(&work_mutex); // 然后解锁等待主线程继续运行。 sleep(1); pthread_mutex_lock(&work_mutex); while (work_area[0] == '\0'){ pthread_mutex_unlock(&work_mutex); // 如果还没有,就解锁互斥量继续等待 sleep(1); pthread_mutex_lock(&work_mutex); // 周期性的尝试给互斥量加锁,如果加锁成功,就检查是否主线程又有字符送来要处理。 } } time_to_exit = 1; // 获得访问权之后检查是否有申请退出程序的请求。 work_area[0] = '\0'; // 如果有就把工作区的第一个字符设置为 \0,然后退出 pthread_mutex_unlock(&work_mutex); pthread_exit(0); } int main(){ int res; pthread_t a_thread; void *thread_result; res = pthread_mutex_init(&work_mutex, NULL); // 初始化互斥量 if (res != 0){ perror("Mutex initialization failed..."); exit(EXIT_FAILURE); } res = pthread_create(&a_thread, NULL, thread_function, NULL); // 启动新线程 if (res != 0){ perror("Thread creation failed"); exit(EXIT_FAILURE); } pthread_mutex_lock(&work_mutex); printf("Input some text. Enter 'end' to finish\n"); while (!time_to_exit){ fgets(work_area, WORK_SIZE, stdin); pthread_mutex_unlock(&work_mutex); while (1){ pthread_mutex_lock(&work_mutex); if (work_area[0] != '\0'){ pthread_mutex_unlock(&work_mutex); sleep(1); } else{ break; } } } pthread_mutex_unlock(&work_mutex); printf("\nWaiting for thread to finish...\n"); res = pthread_join(a_thread, &thread_result); if (res != 0){ perror("Thread join failed..."); exit(EXIT_FAILURE); } printf("Thread joined...\n"); pthread_mutex_destroy(&work_mutex); exit(EXIT_FAILURE); }
这种通过轮询来获得结果的方法通常并不是好的编程方式。在实际的编程中,我们应该尽可能用信号量来避免这种情况。这里只做示例。
四、线程的属性
在前面的示例中,我们都在程序退出之前用 pthread_join 对线程再次进行同步,如果我们想让线程向创建它的线程返回数据就需要这样做。
但有时候也会出现这种情况,我们既不需要第二个线程向主线程返回信息,也不想让主线程等待它的结束。
我们可以创建这一类型的线程,它们被称为 脱离线程(detached thread)。
可以通过修改线程的属性 或 调用 pthread_detach 的方法来创建它们。这里介绍线程的属性,在这里我们就使用前一种方法。
需要用到的最重要的函数是 pthread_attr_init, 它的作用是初始化一个线程属性对象。
#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
与前面的函数一样,它在成功时返回 0, 失败时返回错误代码。
还有一个回收函数 pthread_attr_destroy,它的作用是对属性对象进行清理和回收。一旦对象被回收了,除非它被重新初始化,否则就不能被再次使用。
初始化一个线程属性对象后,我们可以调用许多其它的函数来设置不同的属性行为。
下面列出了一些主要的函数。
#include <pthread.h> int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate); int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy); int pthread_attr_getschedpolicy(const pthread_attr_t *attr, int *policy); int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param); int pthread_attr_getschedparam(const pthread_attr_t *attr, struct sched_param *param);
int pthread_attr_setinheritsched(pthread_attr_t *attr, int inherit);
int pthread_attr_getinheritsched(const pthread_attr_t *attr, int *inherit);
int pthread_attr_setscope(pthread_attr_t *attr_t, int scope);
int pthread_attr_getscope(const pthread_attr_t *attr, int *scope);
int pthread_setstacksize(pthread_attr_t *attr, int scope);
int pthread_getstacksize(const pthread_attr_t *attr, int *scope);
示例:设置脱离状态属性 thread5.c
我们创建一个线程属性,将其设置为脱离状态,然后用这个属性创建一个线程。子线程结束时,它照常调用 phtread_exit, 但这次,原先的线程不再等待与它创建的子线程重新合并。
主线程通过一个简单的thread_finished 标志类检测子线程是否已经结束,并显示线程之间仍然共享着变量。
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <pthread.h> int thread_finished = 0; char message[] = "hello world"; 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); } int main(){ int res; pthread_t a_thread; 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 (0 == thread_finished){ printf("Waiting for thread to say it's finished...\n"); sleep(1); } printf("Other thread finished, bye...\n"); exit(EXIT_SUCCESS); }
线程属性——调度
我们再看一下另外一个可能希望修改的线程属性: 调度。
改变调度属性和设置脱离状态非常相似,可以用 sched_get_priority_max 和 sched_get_priority_min 这两个函数来查找可用的优先级别。
示例:调度
这里的程序和上面的程序类似。
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <pthread.h> #include <errno.h> int thread_finished = 0; char message[] = "hello world"; 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); } int main(){ int max_priority; // 定义一些额外的变量 int min_priority; struct sched_param scheduling_value; int res; pthread_t a_thread; 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_attr_setschedpolicy(&thread_attr, SCHED_OTHER); // 设置好脱离属性后,设置调度策略: if (res != 0){ perror("Setting scheduling policy 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 scheduling priority 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 (0 == thread_finished){ printf("Waiting for thread to say it's finished...\n"); sleep(1); } printf("Other thread finished, bye...\n"); exit(EXIT_SUCCESS); }
运行结果:
分析:
这与设置脱离状态属性很相似,区别只是我们设置了调度策略。
五、取消一个线程
有时,我们想让一个线程可以要求另一个线程终止,就像给它发送一个信号一样。
线程有方法可以做到这一点,与信号处理一样,线程可以在被要求终止时改变其行为。
线程终止函数 pthread_cancel
#include <pthread.h>
int pthread_cancel(pthread_t thread);
给这个函数提供一个线程标识符,我们就可以发送请求来取消它。但在接收到取消请求的一端,事情会稍微复杂一点,不过也不是非常复杂。
线程可以用 pthread_setcancelstate 设置自己的取消状态。
#include <pthread.h> int pthread_setcancelstate(int state, int *oldstate);
第一个参数的取值可以是 PTHREAD_CANCEL_ENBALE, 这个值允许线程接收取消请求;或者是 PTHREAD_CANCEL_DISABLE, 它的作用是忽略取消请求。
oldstate 指针用于获取先前的取消状态。如果对它没有兴趣,只需传递 NULL 给它。 如果取消请求被接受了,线程就可以进入第二个控制层次,用 pthread_setcanceltype
设置取消类型。
#include <pthread.h> int pthread_setcanceltype(int type, int *oldtype);
type 参数可以有两种取值:一个是 PTHREAD_CANCEL_ASYNCHRONOUS, 它将使得在接收到取消请求后立即采取行动;
另一个是 PTHREAD_CANCEL_DEFERRED,它将使得在接收到取消请求后,一直等待直到线程执行了下述函数之一后才采取行动。
具体是函数 pthread_join、pthread_cond_wait、pthread_cond_timedwait、pthread_testcancel、sem_wait 或 sigwait。
oldtype参数可以保存先前的状态,如果不想知道先前的状态,可以传递 NULL给它。默认情况下,线程在启动时的取消状态为
PTHREAD_CANCEL_ENABLE,取消类型是 PTHREAD_CANCEL_DEFERRED。
示例:取消一个线程 thread7.c
thread7.c 还是基于thread1.c。这次,主线程向它创建的线程发送一个取消请求。
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <pthread.h> void *pthread_function(void *arg){ int i, res; 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); } int main(){ int res; pthread_t a_thread; void *thread_result; res = pthread_create(&a_thread, NULL, pthread_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_FAILURE); }
运行结果:
六、多线程
至此,我们总是让程序的主执行线程仅仅创建一个线程。但是我们可以创建更多的线程的其实。
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <pthread.h> #define NUM_THREADS 6 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 = rand() % 5; //sleep(rand_num); // 等待一段时间后退出运行 printf("Bye from %d\n", my_number); pthread_exit(NULL); } 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); }
运行结果:
从这个输出结果我们可以看出和预想的输出有点不同。
问题分析:
启动线程时,线程函数的参数是一个局部变量,这个变量在循环中被更新,引起问题的代码行是:
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);
}
如果主线程运行的足够快,就有可能改变某些线程的参数(即lots_of_threads)。
当对共享变量和多个执行路径没有做到足够的重视时,程序就有可能出现这样的错误行为。
要改正这个问题,我们可以直接传递这个参数的值:
res = pthread_create(&(a_thread[lots_of_threads]), NULL, thread_function, (void *)lots_of_threads);
当然还要修改 thread_function 函数:
void *thread_function(void *arg){ int my_number = (int)arg; }
七、小结
在上面的介绍中,我们介绍了如何在一个进程中创建多个执行线程,每个线程共享着文件范围的变量。
接着,我们介绍了线程对关键代码和数据的两种访问控制方法——使用信号量和互斥量。此后,我们介绍
了如何控制线程的属性,特别介绍了如何才能将子线程和主线程分离开来,使主线程无需等待它创建的子
线程终止运行。在简单介绍完一个线程如何请求另一个线程结束运行以及接收端的线程如何处理这类请求
之后,我们展示了一个有多个并发执行线程的程序示例。