线程:在一个程序中的多个执行路线叫做线程,线程是一个进程内部的一个控制序列。
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 ",(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... "); res = pthread_join(a_thread, &thread_result); if (res != 0){ perror("Thread join failed..."); exit(EXIT_FAILURE); } printf("Thread joined, it returned %s ", (char *)thread_result); printf("Message is now %s ", 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 ",(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... "); res = pthread_join(a_thread, &thread_result); if (res != 0){ perror("Thread join failed..."); exit(EXIT_FAILURE); } printf("Thread joined, it returned %s ", (char *)thread_result); printf("Message is now %s ", 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 ",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 "); while (strncmp("end", work_area, 3) != 0){ fgets(work_area, WORK_SIZE, stdin); sem_post(&bin_sem); } printf(" Waiting for thread to finish ... "); res = pthread_join(a_thread, &thread_result); if (res != 0){ perror("Thread join failed"); exit(EXIT_FAILURE); } printf("Thread joined "); 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 "); 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 ",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 "); 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 ", strlen(work_area) - 1); // 如果不想退出,就统计字符 work_area[0] = '