线程与进程

为什么有了进程的概念后,还要再引入线程呢?使用多线程到底有哪些好处?什么的系统应该选用多线程?我们首先必须回答这些问题。

  • 使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。
  • 使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。

除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:

  1. 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。
  2. 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
  3. 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。

一、线程标识

  • 线程有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);
}

编译运行:

Linux线程详解

正如我们的期望,进程ID相同10310,线程ID不同。主线程如果不休眠,有可能在新线程执行之前就退出了。如下是去掉后的再次执行结果,很明显,第一次执行时,新线程没有机会运行:

Linux线程详解

三、线程终止

如果进程的任一线程调用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);
}

运行结果:

Linux线程详解

 

运行结果如下(与书中给出的结果稍有不同),从以下的运行结果可以得到结论。

在敲代码的过程中发现了一点觉得比较怪异的地方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);
}

运行结果:

Linux线程详解

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);
}

运行结果:

Linux线程详解

结论:

  1. 由于使用0参数调用以上函数,所以直接删除了已经注册的清理函数,调用pthread_exit也就没有任何清理函数可以调用。
  2. pthread_cleanup_push、pthread_cleanup_pop函数必须成对出现。
  3. 如果线程是通过从它的启动例程中返回而终止的话,它的清理处理程序就不会被调用。

线程可以通过可以调用pthread_detach函数分离线程:

#include <pthread.h>
int pthread_detach(pthread_t thread);

  • 如果线程已经被分离,线程的底层存储资源可以在线程终止时被立即收回。
  • 在线程被分离后,不能使用pthread_join等待它的终止状态。

相关文章: