POSIX线程学习
一、什么是线程
在一个程序中的多个执行路线就叫做线程。更准确的定义是:线程是一个进程内部的一个控制序列。所有的进程都至少有一个线程。当进程执行fork调用时,将创建出该进程的一份新副本,这个新进程拥有自己的变量和自己的PID,它的时间调度也是独立的,它的执行几乎完全独立于父进程。当在进程中创建一个新线程时,新的执行线程将拥有自己的栈(因此也有自己的局部变量),但与它的创建者共享全局变量、文件描述符、信号处理函数和当前目录状态。
一个混杂着输入、计算和输出的应用程序,可以将这几个部分分离为3个线程来执行,从而改善程序执行的性能。当输入或输出线程等待连接时,另外一个线程可以继续执行。因此,如果一个进程在任一时刻只能做一件事情的话,线程可以让它在等待连接之类的事情的同时做一些其他有用的事情。一般而言,线程之间的切换需要操作系统做的工作比进程之间的切换少得多,因此多个线程对资源的需求要远小于多个进程。
线程也有下面一些缺点:①在多线程程序中,因时序上的细微偏差或无意造成的变量共享而引发错误的可能性是很大。②对多线程程序的调试要比对单线程程序的调试困难得多,因为线程之间的交互非常难于控制。③将大量计算分成两个部分,并把这两个部分作为两个不同的线程来运行的程序在一台单处理器机器上并不一定运行得更快,除非计算确实允许它的不同部分可以被同时计算,而且运行它的机器拥有多个处理器核来支持真正的多处理。
二、第一个线程程序
我们首先来看一个用于管理线程的新函数pthread_create,它的作用是创建一个新进程,类似于创建新进程的fork函数,它的定义如下:
#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调用的返回值是不同的,但对新线程来说,我们必须明确地提供给它一个函数指针,新进程将在这个新位置开始执行。
线程通过调用pthread_exit函数终止执行,就如同进程在结束时调用exit函数一样。这个函数的作用是,终止调用它的线程并返回一个指向某个对象的指针。注意,绝不能用它来返回一个指向局部变量的指针,因为线程调用该函数后,这个局部变量就不再存在了,这将引起严重的程序漏洞。该函数的定义如下:
#include<pthread.h> void pthread_exit(void *retval);
pthread_join函数在线程中的作用等价于进程中用来收集子进程信息的wait函数,定义如下:
#include<pthread.h> int pthread_join(pthread_t th,void **thread_return);
第一个参数指定了将要等待的线程,线程通过pthread_create返回的标识符来指定。第二个参数是一个指针,它指向另一个指针,而后者指向线程的返回值。
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <pthread.h> void *thread_function(void *arg); char message[] = "Hello World"; 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); } void *thread_function(void *arg) { printf("thread_function is running. Argument was %s\n", (char *)arg); sleep(3); strcpy(message, "Bye!"); pthread_exit("Thank you for the CPU time"); }
Linux下用gcc编译为gcc -o pthread1 pthread1.c -lpthread 运行得到的结果为:
wanh@wanh-VirtualBox:~/linux_c_driver/Demo$ ./pthread1 Waiting for thread to finish... thread_function is running. Argument was Hello World Thread joined, it returned Thank you for the CPU time Message is now Bye!
三、同步
1、信号量进行同步
有两组接口函数用于信号量。一组取自POSIX的实时扩展,用于线程;另一组被称为系统V信号量,常用于进程的同步。信号量是一个特殊类型的变量,它可以被增加或减少,但对其的关键访问被保证是原子操作。这意味着如果一个程序中有两个(或更多)的线程试图改变一个信号量的值,系统将保证所有的操作都将依次进行。
信号量函数的名字都以sem_开头,线程中使用的基本信号量有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,它们之间不会互相干扰,而不像如果两个程序同时对同一个文件进行读取、增加、写入操作时可能会引起冲突。
sem_wait函数以原子操作的方式将信号量的值减1,但它会等待直到信号量有个非零值才会开始减法操作。因此,如果对值为2的信号量调用sem_wait,线程将继续执行,但信号量的值会减到1。如果对值为0的信号量调用sem_wait,这个函数就会等待,直到有其他线程增加了该信号量的值使其不再是0为止。
最后一个信号量函数是sem_destroy,这个函数的作用是,用完信号量后对它进行清理,它的定义如下:
#include<semaphore.h>
int sem_destroy(sem_t *sem);
例子程序pthread3.c如下:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <pthread.h> #include <semaphore.h> void *thread_function(void *arg); sem_t bin_sem; #define WORK_SIZE 1024 char work_area[WORK_SIZE]; int main() { int res; pthread_t a_thread; void *thread_result; res = sem_init(&bin_sem, 0, 0); if (res != 0) { perror("Semaphore initialization 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); } 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); }
gcc编译执行后如下所示:
wanh@wanh-VirtualBox:~/linux_c_driver/Demo$ ./pthread3 Input some text. Enter 'end' to finish addfgg You input 6 characters diengg You input 6 characters sddee You input 5 characters end Waiting for thread to finish... Thread joined
在主线程中,我们等待直到有文本输入,然后调用sem_post增加信号量的值,这将立刻令另一个线程从sem_wait的等待中返回并开始执行。在统计完字符个数之后,它再次调用sem_wait并再次被阻塞,直到主线程再次调用sem_post增加信号量的值为止。而这很容易导致一些错误,我们修改上面程序,把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); }
现在,如果输入FAST,程序就会调用sem_post使字符统计线程开始运行,同时立刻用其它数据更新work_area数组。程序运行情况如下所示:
wanh@wanh-VirtualBox:~/linux_c_driver/Demo$ ./pthread3a Input some text. Enter 'end' to finish ecdedff You input 7 characters ddaaa You input 5 characters FAST You input 8 characters You input 8 characters You input 8 characters end Waiting for thread to finish... Thread joined
问题在于,我们的程序依赖其接收文本输入的时间要足够长,这样另一个线程才有时间在主线程还未准备好给它更多的单词去统计之前统计出工作区中字符的个数。当我们试图连续快速地给它两组不同的单词去统计时(键盘输入的FAST和程序自动提供地Wheeee...),第二个线程就没有时间去执行。但信号量已被增加了不止一次,所以字符统计线程就会反复统计字符数目并减少信号量的值,直到它再次变为0为止。
2、用互斥量进行同步
另一种用在多线程程序中的同步访问方法是使用互斥量。它允许程序员锁住某个对象,使得每次只能有一个线程访问它。为了控制对关键代码的访问,必须在进入这段代码之前锁住一个互斥量,然后在完成操作之后解锁它。用于互斥量的基本函数和信号量的函数非常相似,它们的定义如下所以:
#include<pthread.h> int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *murexattr); int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); int pthread_mutex_destroy(pthread_mutex_t *mutex);
与信号量类似,这些函数的参数都是一个先前声明过的对象的指针。对互斥量来说,这个对象的类型为pthread_mutex_t。pthread_mutex_init函数中的属性参数允许我们设置互斥量的属性,而属性控制着互斥量的行为。属性类型默认为fast,但它有一个小缺点:如果程序试图对一个已经加了锁的互斥量调用pthread_mutex_lock,程序救会被阻塞,而又因为拥有互斥量的这个线程正是现在被阻塞的线程,所以互斥量就永远也不会被解锁了,程序也就进入死锁状态。这个问题可以通过改变互斥量的属性来解决,我们可以让它检查这种情况并返回一个错误,或者让它递归的操作,给同一个线程加上多个锁,但必须注意在后面执行同等数量的解锁操作。
四、取消一个线程
有时,我们想让一个线程可以要求另一线程终止,就像给它发送一个信号一样,线程有方法可以做到这一点,与信号处理一样,线程可以在被要求终止时改变其行为。
#include<pthread.h>
int pthread_cancel(pthread_t thread);
这个函数提供一个线程标识符,我们就可以发送请求来取消它,但在接收到取消请求的一端,事情会稍微复杂一点。线程可以用pthread_setcancelstate设置自己的取消状态。
#include<pthread.h> int pthread_setcancelstate(int state,int *oldstate);
第一个参数的取值可以是PTHREAD_CANCEL_ENABLE,这个值允许线程接收取消请求;或者是PTHREAD_CANCEL_DISABLE,它的作用是忽略取消请求。oldstate指针用于获取先前的取消状态。如果取消请求被接受了,线程就可以进入第二个控制层次,用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_timewait、pthread_testcancel、sem_wait或sigwait。
oldtype参数可以保存先前的状态,如果不想知道先前的状态,可以传递NULL给它。默认情况下,线程在启动时的取消状态为PTHREAD_CANCEL_ENABLE,取消类型是PTHREAD_CANCEL_DEFERRED。