线程与进程
为什么有了进程的概念后,还要再引入线程呢?使用多线程到底有哪些好处?什么的系统应该选用多线程?我们首先必须回答这些问题。
- 使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。
- 使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。
除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:
- 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。
- 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
- 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。
一、线程标识
- 线程有ID, 但不是系统唯一, 而是进程环境中唯一有效;
- 线程的句柄是pthread_t类型, 该类型不能作为整数处理, 而是一个结构。
下面介绍两个函数:
- 头文件: <pthread.h>
- 原型:int pthread_equal(pthread_t tid1, pthread_t tid2);
- 返回值: 相等返回非0, 不相等返回0.
- 说明::比较两个线程ID是否相等.
- 头文件: <pthread.h>
- 原型: pthread_t pthread_self();
- 返回值: 返回调用线程的线程ID.
二、线程创建
在执行中创建一个线程, 可以为该线程分配它需要做的工作(线程执行函数), 该线程共享进程的资源.,创建线程的函数:pthread_create()
#include <pthread.h>
int pthread_create(pthread_t *restrict tidp,
const pthread_attr_t *restrict attr,
void *(*start_rtn)(void *),
void *restrict arg);
- 返回值: 成功则返回0, 否则返回错误编号.
- 参数:
- tidp: 指向新创建线程ID的变量,作为函数的输出;
- attr: 用于定制各种不同的线程属性, NULL为默认属性(见下);
- 新创建的线程从
start_rtn函数的地址开始运行,该函数只有一个void类型的指针参数即arg,如果start_rtn需要多个参数,可以将参数放入一个结构中,然后将结构的地址作为arg传入。该函数可以返回一个void *类型的返回值,而这个返回值也可以是其他类型,并由 pthread_join()获取; - arg:函数的唯一无类型(void)指针参数, 如要传多个参数, 可以用结构封装。
linux下多线程程序的编译方法:
因为pthread的库不是linux系统的库,所以在进行编译的时候要加上 -lpthread
# gcc filename -lpthread //默认情况下gcc使用c库,要使用额外的库要这样选择使用的库.
例:我们看下面一个例子,该示例中,程序创建了一个线程,打印了进程ID、新线程的线程ID以及初始线程的线程ID。
#include "apue.h"
#include <pthread.h>
pthread_t ntid;
void printids(const char *s)
{
pid_t pid;
pthread_t tid;
pid = getpid();
tid = pthread_self();
printf("%s pid %lu tid %lu (0x%lx)\n", s, (unsigned long)pid,(unsigned long)tid, (unsigned long)tid);
}
void *thr_fn(void *arg)
{
printids("new thread: ");
return((void *)0);
}
int main(void)
{
int err;
err = pthread_create(&ntid, NULL, thr_fn, NULL);
if (err != 0)
err_exit(err, "can't create thread");
printids("main thread:");
sleep(1);
exit(0);
}
编译运行:
正如我们的期望,进程ID相同10310,线程ID不同。主线程如果不休眠,有可能在新线程执行之前就退出了。如下是去掉后的再次执行结果,很明显,第一次执行时,新线程没有机会运行:
三、线程终止
如果进程的任一线程调用exit、_exit或_Exit函数,那么整个进程就会终止。注意此处是整个进程都终止了。单个线程可以通过3种方式退出,此处是仅退出线程,而不会终止整个进程。
- 线程可以简单地从启动历程中返回,返回值是线程的退出码。
- 线程可以被同一进程中的其他线程取消。
- 线程调用pthread_exit。
- pthread_exit函数:
- 原型: void pthread_exit(void *rval_ptr);
- 头文件: <pthread.h>
- 参数: rval_ptr是一个无类型指针, 指向线程的返回值存储变量.
- pthread_join函数:
- 原型: int pthread_join(pthread_t thread, void **rval_ptr);
- 头文件: <pthread.h>
- 返回值: 成功则返回0, 否则返回错误编号.
- 参数:
- thread: 线程ID.
- rval_ptr: 指向返回值的指针(返回值也是个指针).
- 说明:
- 调用线程将一直阻塞, 直到指定的线程调用pthread_exit, 从启动例程返回或被取消.
- 如果线程从它的启动例程返回, rval_ptr包含返回码.
- 如果线程被取消, 由rval_ptr指定的内存单元置为: PTHREAD_CANCELED.
- 如果对返回值不关心, 可把rval_ptr设为NULL.
通过一个实例对上面的内容进行一下简单的验证,源码如下:
#include "apue.h"
#include <pthread.h>
void *thr_fn1(void *arg)
{
printf("thread 1 returning\n");
return((void *)1);
}
void *thr_fn2(void *arg)
{
printf("thread 2 exiting\n");
pthread_exit((void *)2); // 调用pthread_exit()
}
int main(void)
{
int err;
pthread_t tid1, tid2;
void *tret;
err = pthread_create(&tid1, NULL, thr_fn1, NULL);
if (err != 0)
err_exit(err, "can't create thread 1");
err = pthread_create(&tid2, NULL, thr_fn2, NULL);
if (err != 0)
err_exit(err, "can't create thread 2");
err = pthread_join(tid1, &tret); // 获取线程1的退出状态
if (err != 0)
err_exit(err, "can't join with thread 1");
printf("thread 1 exit code %ld\n", (long)tret);
err = pthread_join(tid2, &tret); // 获取线程2的退出状态
if (err != 0)
err_exit(err, "can't join with thread 2");
printf("thread 2 exit code %ld\n", (long)tret);
exit(0);
}
运行结果:
运行结果如下(与书中给出的结果稍有不同),从以下的运行结果可以得到结论。
在敲代码的过程中发现了一点觉得比较怪异的地方void *ret;直接申请了一个void指针用于存储数据,虽然没有为void指针指向的内存申请指针,但void指针本身具有地址,pthread_join的第二个参数是“void** ”类型,因此将tret的地址赋给第二个参数,程序最后的运行结果可以发现返回码直接放置在tret所在的地址空间中。这里有一点使用上的问题要留心,pthread_create、pthread_exit函数中使用的void型指针参数所使用的内存在调用者完成调用以后必须是仍然有效的,所以为了解决这个问题,可以使用全局结构,或用malloc函数分配结构。书中也给出了一个实例,实验一下:
#include "apue.h"
#include <pthread.h>
struct foo
{
int a, b, c, d;
};
void printfoo(const char *s, const struct foo *fp)
{
printf("%s", s);
printf(" structure at 0x%lx\n", (unsigned long)fp);
printf(" foo.a = %d\n", fp->a);
printf(" foo.b = %d\n", fp->b);
printf(" foo.c = %d\n", fp->c);
printf(" foo.d = %d\n", fp->d);
}
void *thr_fn1(void *arg)
{
struct foo foo = {1, 2, 3, 4};
printfoo("thread 1:\n", &foo);
pthread_exit((void *)&foo);
}
void *thr_fn2(void *arg)
{
printf("thread 2: ID is %lu\n", (unsigned long)pthread_self());
pthread_exit((void *)0);
}
int main(void)
{
int err;
pthread_t tid1, tid2;
struct foo *fp;
// 线程1
err = pthread_create(&tid1, NULL, thr_fn1, NULL);
if (err != 0)
err_exit(err, "can't create thread 1");
err = pthread_join(tid1, (void *)&fp);
if (err != 0)
err_exit(err, "can't join with thread 1");
sleep(1);
// 线程2
printf("parent starting second thread\n");
err = pthread_create(&tid2, NULL, thr_fn2, NULL);
if (err != 0)
err_exit(err, "can't create thread 2");
sleep(1);
printfoo("parent:\n", fp);
exit(0);
}
运行结果:
pthread_create()、pthread_exit*()函数中使用的void型指针参数所使用的内存在调用者完成调用以后必须是仍然有效的。
线程可以通过调用pthread_cancel函数来请求取消同一进程中的其他线程。函数原型如下:
#include <pthread.h>
int pthread_cancel(pthread_t thread);
- 注意pthread_cancel并不等待线程终止,它仅仅提出请求,被取消线程可以选择如何相应这一请求。
- 线程可以安排它退出时需要调用的函数,这与进程可以用atexit函数安排进程退出时需要调用的函数是类似的。线程可以建立多个清理处理程序。处理程序记录在栈中,也就是说它们的执行顺序与它们注册时的顺序相反。
#include <pthread.h>
void pthread_cleanup_push(void(*rtn)(void*),void *arg);
void pthread_cleanup_pop(int execute);
当线程执行以下动作时调用清理函数,调用参数为arg,清理函数的调用顺序用pthread_cleanup_push来安排。
- 调用pthread_exit时
- 响应取消请求时
- 用非0的execute参数调用pthread_cleanup_pop时。
书中给出了一个例子,让我们也实验一下:
#include "apue.h"
#include <pthread.h>
void cleanup(void *arg)
{
printf("cleanup: %s\n", (char *)arg);
}
void *thr_fn1(void *arg)
{
printf("thread 1 start\n");
pthread_cleanup_push(cleanup, "thread 1 first handler");
pthread_cleanup_push(cleanup, "thread 1 second handler");
printf("thread 1 push complete\n");
if (arg)
return((void *)1); // 线程是通过从它的启动例程中返回而终止的话,它的清理处理程序就不会被调用。
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
return((void *)1);
}
void *thr_fn2(void *arg)
{
printf("thread 2 start\n");
pthread_cleanup_push(cleanup, "thread 2 first handler");
pthread_cleanup_push(cleanup, "thread 2 second handler");
printf("thread 2 push complete\n");
if (arg)
pthread_exit((void *)2);
pthread_cleanup_pop(0);
pthread_cleanup_pop(0); // pthread_cleanup_push、pthread_cleanup_pop函数必须成对出现;
pthread_exit((void *)2);
}
int main(void)
{
int err;
pthread_t tid1, tid2;
void *tret;
err = pthread_create(&tid1, NULL, thr_fn1, (void *)1);
if (err != 0)
err_exit(err, "can't create thread 1");
err = pthread_create(&tid2, NULL, thr_fn2, (void *)1);
//err = pthread_create(&tid2, NULL, thr_fn2, NULL); // 由于使用NULL,直接删除了已经注册的清理函数,调用pthread_exit也就没有任何清理函数可以调用。
if (err != 0)
err_exit(err, "can't create thread 2");
err = pthread_join(tid1, &tret);
if (err != 0)
err_exit(err, "can't join with thread 1");
printf("thread 1 exit code %ld\n", (long)tret);
err = pthread_join(tid2, &tret);
if (err != 0)
err_exit(err, "can't join with thread 2");
printf("thread 2 exit code %ld\n", (long)tret);
exit(0);
}
运行结果:
结论:
- 由于使用0参数调用以上函数,所以直接删除了已经注册的清理函数,调用pthread_exit也就没有任何清理函数可以调用。
- pthread_cleanup_push、pthread_cleanup_pop函数必须成对出现。
- 如果线程是通过从它的启动例程中返回而终止的话,它的清理处理程序就不会被调用。
线程可以通过可以调用pthread_detach函数分离线程:
#include <pthread.h>
int pthread_detach(pthread_t thread);
- 如果线程已经被分离,线程的底层存储资源可以在线程终止时被立即收回。
- 在线程被分离后,不能使用pthread_join等待它的终止状态。