系列文章:
- 文件操作
- 数据管理
- 进程和信号
- POSIX 线程
- 进程间通信:管道
- 信号量共享内存和消息队列
- 套接字
用 fork 调用来创建新进程的代价太高。如果能让一个进程同时做两件事,或者至少是看起来这样,线程的作用就来了。
- 在进程中创建新线程
- 在一个进程中同步线程之间的数据访问
- 修改线程的属性
- 在同一个进程中,从一个线程中控制另一个线程
什么线程?
在一个程序中的多个执行路线叫做线程。更准确地定义是:线程是一个进程内部的一个控制序列。事实上,所有的进程都至少有一个执行线程。
弄清 fork 系统调用和创建新线程之间的区别非常重要。当进程执行 fork 调用时,将创建出该进程的一份新副本,这个新进程拥有自己的变量和自己的 PID,它的调度时间也是独立的,它的执行(通常)几乎完全独立于父进程。当在进程中创建一个新线程时,新的执行线程将拥有自己的栈、但与创建者共享全局变量、文件描述符、信号处理函数和当前目录状态。
Linux 系统在 1996 年第一次获得线程的支持,我们常把当时使用的函数库称为 LinuxThread,LinuxThread 已经和 POSIX 的标准非常接近了,但是在 Linux 的线程实现版本和 POSIX 标准之间还是存在着细微的差别,最明显的是关于信号处理部分。这些差别中的大部分都受底层 Linux 内核的限制,而不是函数库实现所加强的。
许多项目都在研究如何才能改善 Linux 对线程的支持,这种改善不仅仅是清除 POSIX 标准和 Linux 具体实现之间的细微的差别,而且要增强 Linux 线程的性能和删除一些不需要的限制,其中大部分工作都集中在如何将用户级的线程映射到内核级的线程。这些项目中有两个主要的项目分别是下一代 POSIX 线程(New Generation POSIX Thread,NGPT)和本地 POSIX 线程库(Native POSIX Thread Library,NPTL)。这两个项目都必须修改 Linux 的内核来支持新的函数库,与旧的 Linux 线程相比,两者都极大地提升了性能。
2002 年,NGPT 项目组宣布,停止 NGPT 添加新功能。
线程的优点和缺点
在某些环境下,创建新线程要比创建新进程更有优势。新线程的创建代价要比新进程小得多:
优点:
- 让程序看起来好像是在同时做两件事情是很有用的。一个例子是数据库服务器,这个明显的多任务工作如果多进程的方式来完成将很难做到高效,因为各个不同的进程必须紧密合作才能满足加锁和数据一致性方面的要求,而用多线程来完成就比多进程要容易得多。
- 一个混合着输入、计算和输出的应用程序,可以将这几个部分分离为 3 个线程来执行,从而改善程序执行的性能。当输入或输出线程等待连接时,另外一个线程可以继续执行。因此,如果一个进程在任一时刻最多只能做一件事情的话,线程可以让它在等待连接之类的事情的同时做一些其他有用的事情。一个需要同时处理多个网络连接的服务器程序也是一个天生适用于应用多线程的例子。
- 一般而言,线程之间的切换需要操作系统做的工作要比进程之间的切换少得多,因此多个线程对资源的需求要远小于多个进程。如果一个程序在逻辑上需要有多个执行线程,那么在单处理器系统上把它运行为一个多线程程序才更符合实际情况。虽然如此,编写一个多线程程序的设计困难较大,不应等闲视之。
缺点:
- 编写多线程程序需要非常仔细的设计。在多线程程序中,因时序上的细微偏差或无意造成的变量共享而引发错误的可能性是很大的。
- 对多线程程序的调试要比对单线程程序的调试困难得多,因为线程之间的交互非常难于控制。
- 将大量计算分成两个部分,并把这两个部分作为两个不同的线程来运行的程序在一台单处理器上并不一定运行得更快,除非计算确实允许它的不同部分可以被同时计算,而且运行它的机器拥有多个处理器核来支持真正的多处理。
第一个线程程序
线程有一套完整的与其有关的函数库调用,它们中的绝大多数函数名都以 pthread_ 开头。为了使用这些函数库调用,必须定义宏 _REENTRANT,在程序中包含头文件 pthrea.h,并且在编译程序时需要用选项 -lpthread 来链接线程库。
在设计最初的 UNIX 和 POSIX 库例程时,人们假设每个进程中只有一个执行线程,一个明显的例子就是 errno,该变量用于获取某个函数调用失败后的错误信息。在一个多线程程序里,默认情况下,只有一个 errno 变量供所有的线程共享。在一个线程准备获取刚才的错误代码时,该变量很容易被另一个线程中的函数调用所改变。类似的问题还存在于 fputs 之类的函数中,这些函数通常用一个全局性区域来缓存输出数据。
为了解决这个问题,我们需要使用称为可重入的例程。可重入代码可以被多次调用而仍然正常工作,这些调用可以来自不同的线程,也可以是某种形式的嵌套调用。因此,代码中的可重入部分通常只使用局部变量,这使得每次对改代码的调用都将获得它自己的唯一的一份数据副本。
编写多线程程序时,我们通过定义宏 _PEENTRANT 来告诉编译器我们需要可重入功能,这个宏的定义必须位于程序中的任何 #include 语句之前。它将为我们做 3 件事情,而且做得非常优雅,以至于我们一般不需要知道它到底做了哪些事情。
- 它会对部分函数重新定义它们的可安全重入的版本,这些函数的名字一般不会发生改变,只是会在函数名后面添加_r字符串。
- stdio.h 中原来以宏的形式实现的一些函数将变成可安全重入的函数。
- 在errno.h中定义的变量 errno 现在将成为一个函数调用,它能够以一种多线程安全的方式来获取真正的errno值。
在程序中包含头文件 pthread.h还将向我们提供一些其他得到将在代码中使用到的定义和函数原型,就如同头文件 stdio.h 为标准输入和标准输出例程所提供的的定义一样。最后,需要确保在程序中包含了正确的线程头文件,并且在编译程序时链接了实现 pthread 函数的正确的线程库。
来看一个用于管理线程的新函数 pthread_create,作用是创建一个新线程,类似于创建新进程的 fork 函数。
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 的指针。因此,可以传递一个任一类型的参数并返回一个任一类型的指针。用 for 调用后,父子进程将在同一个位置继续执行下去,只是 fork 调用的返回值是不同的;但对新线程来说,我们必须明确地提供给它一个函数指针,新线程将在这个新位置开始执行。
该函数调用成功时返回值是 0,如果失败则返回错误代码。
线程通过调用 pthread_exit 函数终止执行,就如同进程在结束时调用 exit 函数一样。这个函数的作用是,终止调用它的线程并返回一个指向某个对象的指针。注意,决不能用来返回一个指向局部变量的指针,因为线程调用该函数后,这个局部变量就不再存在了,这将引起严重的程序漏洞。
void pthread_exit(void *retval);
pthread_join 函数在线程中的作用等价于进程中用来收集子进程信息的 wait 函数。
int pthread_join(pthread_t th, void **thread_return);
第一个参数指定了将要等待的线程,线程通过 pthread_create 返回的标识符来指定。第二个参数是一个指针,它指向另一个指针,而后者指向线程的返回值。与 pthread_create 类似,这个函数在成功时返回 0,失败时返回错误代码。
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); // 调用成功后,就会有两个线程运行。原先的线程(main)继续执行 pthread_create 后面的代码,而新线程开始执行 thread_function 函数。
if(res != 0)
{
perror("Thread creation failed");
exit(EXIT_FAILURE);
}
printf("Waiting for thread to finish...\n");
res = pthread_join(a_thread, &thread_result); // 原先的线程在查明新线程已经启动后,将调用 pthread_join 函数,pthread_exit 中的字符串传递给 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) // 新线程在 pthread_function 函数中执行
{
printf("thread_function is running. Argument was %s\n", (char *)arg);
sleep(3);
strcpy(message, "Bye!"); // 更新全局变量
pthread_exit("Thank you for the CPU time");
}
编译这个程序时,首先需要定义宏 _REENTRANT,在少数系统上,可能还需要定义宏 _POSIX_C_SOURCE,但一般不需要定义它。
接下来必须链接正确的线程库。如果使用的是一个老的 Linux发行版,默认的线程库不是 NPTL,查看头文件 /usrinclude/pthread.h,如果这个文件显示的版本日期是 200 年或更晚,那几乎可以肯定是 NPTL 实现。
jiaming@jiaming-pc:~/Documents/test$ cc -D_REENTRANT thread.c -o thread1 -lpthreadjiaming@jiaming-pc:~/Documents/test$ ./thread1
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!
同时执行
编写一个程序验证两个线程的执行是同时进行的,使用轮询技术。除了局部变量之外,所有其它变量都将在一个进程中的所有线程之间共享。
void *thread_function(void *arg);
char message[] = "Hello World";
int run_now = 1;
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);
}
int print_count1 = 0;
while(print_count1++ < 20) // 不断检查值,忙等待
{
if(run_now == 1)
{
printf("1");
run_now = 2;
}
else
{
sleep(1);
}
}
// 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!");
int print_count2 = 0;
while(print_count2++ < 20)
{
if(run_now == 2)
{
printf("2");
run_now = 1;
}
else
{
sleep(1);
}
}
pthread_exit("Thank you for the CPU time");
}jiaming@jiaming-pc:~/Documents/test$ cc -D_REENTRANT thread.c -o thread -lpthread
jiaming@jiaming-pc:~/Documents/test$ ./thread
1212121212121212jiaming@jiaming-pc:~/Documents/test$
你可能会发现程序要过几秒钟才会产生输出,特别是在一个单核CPU的机器上。每个线程通过设置run_now变量的方法来通知另一个线程开始运行,然后,它会等待另一个线程改变了这个变量的值后再次运行。
同步
上一节实现的两个线程同时执行的情况是十分低效且笨拙的。专门有一组设计好的函数为我们提供了更好的控制线程执行和访问代码临界区域的方法 —— 信号量、互斥量,两种方法很类似,可以通过对方来实现。
用信号量进行同步
有两组接口函数用于信号量。一组取自 POSIX 的实时扩展,用于线程。另一组被称为系统 V 信号量,常用于进程的同步。这两组接口函数虽然相近,但并不保证它们之间可以互换,而且它们使用的函数调用也各不相同。
荷兰计算机科学技术 Dijkstra 首先提出了信号量的概念,信号量是一个特殊类型的变量,它可以被增加或减少,但对其的关键访问被保证是原子操作,即使在一个多线程程序中也是如此,这意味着如果一个程序中有两个(或更多)的线程试图改变一个信号量的值,系统将保证所有的操作都依次进行。但如果是普通变量,来自同一程序中的不同线程的冲突操作所导致的结果将是不确定的。
常见的信号量 —— 二进制信号量,只有 0 和 1 两种取值、计数信号量,它有更大的取值范围。
信号量一般常用来保护一段代码,使其每次只能被一个执行线程运行,要完成这个工作,就要使用二进制信号量,有时,希望可以允许有限数目的线程执行一段指定的代码,需要使用计数信号量。
信号量函数名字都以 sem_ 开头,而不像大多数线程函数那样以 pthread_ 开头。线程中使用的基本信号量函数有 4 个,它们都非常的简单。
信号量通过 sem_init 函数创建。
int sem_init(sem_t *sem, int pshared, unsigned int value);
这个函数初始化由sem指向的信号量对象,设置它的共享选项,并给它一个初始的整数值,pshared 参数控制信号量的类型,如果其值为 0,就表示这个信号量是当前进程的局部信号量,否则,这个信号量就可以在多个进程之间共享。
两个控制信号量的函数:
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,然后继续执行,另一个线程还将继续等待。信号量的这种“在单个函数中就能原子化地进行测试和设置”的能力使其变得非常有价值。
用完信号量后对其进行清理,清理该信号量拥有的所有资源,如果企图清理的信号量正被一些线程等待,就会收到一个错误。
int sem_destroy(sem_t *sem);
void *thread_function(void *arg);
sem_t bin_sem; // 定义一个信号量
char work_area[WORK_SIZE];
int main()
{
int res;
pthread_t a_thread;
void *thread_result;
res = sem_init(&bin_sem, 0, 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 test, Enter 'end' to finish\n");
while(strncmp("end", work_area, 3) != 0) // 只要输入不符合要求,main 线程就一直循环执行
{
fgets(work_area, WORK_SIZE, stdin); // 向 work_area 缓存输入数据
sem_post(&bin_sem); // 信号量 +1,将立刻另一个线程从 sem_wait 的等待中返回并开始执行
}
// 之上的代码是 main 线程,thread_function 是另一个线程。
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); // 保证线程先执行时挂起等待(因为信号初始化为0),等到 sem_post 执行后,再执行
while(strncmp("end", work_area, 3) != 0)
{
printf("You input %ld chacters\n", strlen(work_area) - 1);
sem_wait(&bin_sem); // 将 work_area 中内容取出,然后信号量 -1
}
pthread_exit(NULL);
}jiaming@jiaming-pc:~/Documents/test$ cc -D_REENTRANT thread.c -o thread -lpthread
jiaming@jiaming-pc:~/Documents/test$ ./thread
Input some test, Enter 'end' to finish
asd
You input 3 chacters
djaksldjas
You input 10 chacters
aslkdjkasjdkjf
You input 14 chacters
endjksjdlk
Waiting for thread to finish...
Thread joined
用互斥量进行同步
另一种用在多线程程序中的同步访问访问方法是使用互斥量。它允许程序员锁住某个对象,使得每次只能有一个线程访问它。为了控制对关键代码的访问,必须在进入这段代码之前锁住一个互斥量,然后在完成操作之后解锁它。
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);
与信号量类似,这些函数的参数都是一个先前声明过的对象的指针。对互斥量来说,这个对象的类型 pthread_mutex_t。pthread_mutex_init 函数中的属性允许我们设置互斥量的属性,而属性控制着互斥量的行为。属性类型默认为 fast,但它有一个小缺点:如果程序试图对一个已经加了锁的互斥量调用 pthread_mutex_lock,程序就会被阻塞,而又因为拥有互斥量的这个线程正是现在被阻塞的线程,所以互斥量就永远也不会被解锁了,程序也就进入死锁状态。这个问题可以通过改变互斥量的属性来解决,我们可以让它检查这种情况并返回一个错误,或者让它递归的操作,给同一个线程加上多个锁,但必须注意在后面执行同等数量的解锁操作。
假设需要保护对一些关键变量的访问,用一个互斥量来保证任一时刻只能有一个线程访问它们。
void *thread_function(void *arg);
pthread_mutex_t work_mutex; // 声明一个互斥量,protect both work_area、time_to_exit
char work_area[WORK_SIZE]; // 声明一个工作区
int time_to_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_SUCCESS);
}
void *thread_function(void *arg)
{
sleep(1);
pthread_mutex_lock(&work_mutex);
while (strncmp("end", work_area, 3) != 0) // 是否有申请退出程序的请求
{
printf("You input %ld characters\n", strlen(work_area) - 1);
work_area[0] = '\0';
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);
}jiaming@jiaming-pc:~/Documents/test$ cc -D_REENTRANT thread.c -o thread -lpthread
jiaming@jiaming-pc:~/Documents/test$ ./thread
Input some text. Enter 'end' to finish
whit
You input 4 characters
The Crow Road
You input 13 characters
end
Waiting for thread to finish...
Thread joined
线程的属性
在前面的示例中,我们在程序退出之前用 pthread_join 对线程再次进行同步,如果想让线程向创建它的线程返回数据就需要这样做。但有时也会有这种情况,我们既不需要第二个线程向主线程返回信息,也不想让主线程等待它的结束。
假设我们在主线程继续为用户提供服务的同时创建了第二个线程,新线程的作用是将用户正在编辑的数据文件进行备份存储,备份工作结束后,第二个线程就可以直接终止了,它没有必要再回到主线程中。
我们可以创建这一类型的线程,它们被称为脱离线程,可以通过修改线程属性或调用 pthread_detach 的方法来创建它们。线程属性最重要的函数是 pthread_attr_init,它的作用是初始化一个线程属性对象。
int pthread_attr_init(pthread_attr_t *attr);
与前面的函数一样,它在成功时返回 0,失败时返回错误代码。还有一个回收函数 pthread_attr_destroy,它的目的是对属性对象进行清理和回收,一旦对象被回收了,除非它重新初始化,否则就不能再次使用。
初始化一个线程属性对象后,我们可以调用许多其他的函数来设置不同的属性行为。其中主要的一些函数列在下面:
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 *inhert);
int pthread_attr_setscope(pthread_attr_t *attr, int scope);
int pthread_attr_getscope(const pthread_attr_t *attr, int *scope);
int pthread_attr_setstacksize(pthread_attr_t *attr, int scope);
int pthread_attr_getstacksize(const pthread_attr_t *attr, int *scope);
- detachedstate: 这个属性允许我们无需对线程进行重新合并,与大多数_set类函数一样,它以一个属性指针和一个标志为参数来确定需要的状态。pthread_attr_setdetachstate 函数可能用到的两个标志分别是PTHREAD_CREATE_JOINABLE 和PTHREAD_CREATE_DETACHED,默认是前者,所以可以允许两个线程重新合并。如果标志设置为后者,就不能调用 pthread_join 来获得另一个线程的退出状态。
- schedpolicy: 这个属性控制线程的调度方式,它的取值可以是 SCHED_OTHER、SCHED_RP、SCHED_FIFO,默认为 SCHED_OTHRE。另外两种调度方式只能用于超级用户权限运行的进程,因为它们都具备实时调度的功能,但在行为上略有区别,SCHED_RP 使用循环调度机制,而SCHED_FIFO使用先进先出策略。
- schedparam: 这个属性是和schedpolicy属性结合使用的,它可以对 SCHED_OTHER 策略运行的线程的调度进行控制。
- inheritsched: 这个属性可以取两个值:PTHREAD_EXPLICIT_SCHED、PTHREAD_INHERIT_SCHED,它的默认取值是前者,表示调度由属性明确地设置。如果设置为后者,新线程将沿用其创建者所使用的参数。
- scope: 这个属性控制一个线程调度的计算方式。目前Linux只支持一种取值 PTHREAD_SCOPE_SYSTEM。
- stacksize: 这个属性控制线程创建的栈大小,单位为字节。Linux 在实现线程时,默认使用的栈很大,这个参数不常用。
创建一个线程属性,将其设置为脱离状态,然后用这个属性创建一个线程,子线程结束时,照常调用 pthread_exit,但这次,原先的线程不再等待与它创建的子线程重新合并。主线程通过一个简单的 thread_finished 标志来检测子线程i是否已经结束,并显示线程之间仍然共享着变量。
void *thread_function(void *arg);
char message[] = "Hello, world";
int thread_finished = 0;
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(!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);
}jiaming@jiaming-pc:~/Documents/test$ ./thread
Waiting for thread to say it's finished...
thread_function is running. Argument was Hello, world
Waiting for thread to say it's finished...
Waiting for thread to say it's finished...
Waiting for thread to say it's finished...
Second thread setting finished flag, and exiting now
Other thread finished, bye!
设置脱离状态可以允许第二个线程独立地完成工作,而无需原先的线程等待它。
线程调度,查看允许的优先级范围:
max_priority = sched_get_priority_max(SCHED_OTHER);min_priority = sched_get_priority_min(SCHED_OTHER);
取消一个线程
有时候,我们i想让一个线程可以要求另一个线程终止,就像给它发送一个信号一样。线程有方法可以做到这一点,与信号处理一样,线程可以在被要求终止时改变其行为。
int pthread_cancel(pthread_t thread);
这个函数的定义简单易懂,提供一个线程标识符,我们就可以发送请求来取消它。线程可以用 pthread_setcancelstate设置自己的取消状态。
int pthread_setcancelstate(int state, int *oldstate);
第一个参数的取值可以是 PTHREAD_CANCEL_ENABLE,这个值允许线程接收取消请求,或者是 PTHREAD_CANCEL_DISABLE,它的作用是忽略取消请求。oldstate 指针用于获取先前的取消状态,可以设置为 NULL,如果取消请求被接受了,线程就可以进入第二个控制层次,用 pthread_setcanceltype 设置取消类型。
int pthread_setcanceltype(int type, int *oldtype);
type 参数可以有两种取值:一个是 PTHREAD_CANCEL_ASYNCHRONOUS,它将使得在接收到取消请求后立即采取行动;另一个是 PTHREAD_CANCEL_DEFERRED,它将使得在接收到取消请求后,一直等待直到线程执行了下述函数之一后才采取行动,pthread_join、pthread_cond_timedwait、pthread_testcancel、sem_wait、sigwait。
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;
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); // 延迟取消,直到碰到 pthread_join 结束
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);
}jiaming@jiaming-pc:~/Documents/test$ cc -D_REENTRANT thread.c -o thread -lpthread
jiaming@jiaming-pc:~/Documents/test$ ./thread
thread_function is running
Thread is still running (0)....
Thread is still running (1)....
Thread is still running (2)....
Canceling thread...
Thread is still running (3)....
Waiting for thread to finish...
多线程
我们创建了许多线程并让它们以随意的顺序结束执行。
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);
// res = pthread_create(&(a_thread[lots_of_threads]), NULL, thread_function, (void *)lots_of_threads); // 更安全,以免主线程运行得过块,改变参数,直接传递这个参数的值即可
if(res != 0)
{
perror("Thread create 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 thred\n");
}
else
{
perror("pthread_join failed");
}
}
printf("All done\n");
exit(EXIT_SUCCESS);
}
void *thread_function(void *arg)
// 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);
}
/*
jiaming@jiaming-pc:~/Documents/test$ cc -D_REENTRANT thread.c -o thread -lpthread
jiaming@jiaming-pc:~/Documents/test$ ./thread
thread_function is running. Argument was 0
thread_function is running. Argument was 1
thread_function is running. Argument was 2
thread_function is running. Argument was 3
thread_function is running. Argument was 4
Bye from 1
thread_function is running. Argument was 5
Waiting for threads to finish...
Bye from 5
Picked up a thred
Bye from 0
Bye from 2
Bye from 3
Bye from 4
Picked up a thred
Picked up a thred
Picked up a thred
Picked up a thred
Picked up a thred
All done
*/