APUE读书笔记-第十一章 线程

匆匆了结了第十章,APUE的水确实挺深的,也许我应该几种书结合者看。开始第十一章的内容,刚刚大概翻了翻,线程开始的部分还不是很困难。

11.2 线程的概念

其实书中并没有明确的定义线程的概念,不过有关于线程的概念,我之前也分析过一段:http://blog.csdn.net/u012927281/article/details/51602898

由于Linux对于线程的实现也经历过几个版本的变化,所以我的这一篇文章可能也有疏漏的地方,欢迎大家对其中不对的地方予以指出

11.3 线程标识

与进程ID不同,线程ID是仅在它所属的进程上下文中才有意义。

pthread标准给出了一个函数用于比较两个线程的ID是否相等,函数原型如下:

#include <pthread.h>
extern int pthread_equal (pthread_t __thread1, pthread_t __thread2)
  __THROW __attribute__ ((__const__));

通过以下函数可以获得线程自身的ID

#include <pthread.h>
extern pthread_t pthread_self (void) __THROW __attribute__ ((__const__));

11.4 线程创建

在创建多个控制线程以前,程序的行为与传统的进程并没有什么区别。之前大概看了这样一篇文章,有点体会,有机会分享给大家。

新增的线程可以通过调用pthread_create函数。函数原型如下:

<pre name="code" class="cpp">#include <pthread.h>
extern int pthread_create (pthread_t *__restrict __newthread, const pthread_attr_t *__restrict __attr, void *(*__start_routine) (void *), void *__restrict __arg) __THROWNL __nonnull ((1, 3));


当pthread_create返回时,新创建线程的线程ID会被设置成“__newthread”指向的内存单元。但要注意的一点是子线程不能直接访问“__newthread”的值,因为主线程与子线程的调度顺序不可知,所以可能子线程先调度而主线程后调度,造成“__newthread”的值并不是想要得到的结果。新创建的线程从“__start_routine”函数的地址开始运行,如果需要向该函数传递一个以上的参数,则需要将这些参数放到一个结构中,然后将这个结构的地址作为“__arg”参数传入。

书中给出了一个实例,我也来实验一下:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.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 %ld (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()
{
	int err;
	err = pthread_create(&ntid,NULL,thr_fn,NULL);
	if(err!=0) perror("error exist");
	printids("main thread: ");
	sleep(1);
	exit(0);
}

一字不落的敲了一遍。

编译命令如下:

gcc -o test_11_2 test_11_2.c -lpthread

运行结果如下:

./test_11_2 
main thread:  pid 2142 tid -1218562304 (0xb75e3700)
new thread:  pid 2142 tid -1218565312 (0xb75e2b40)

书中还提到了一点:主线程还需要休眠,若主线程不休眠,它就可能退出,如此新线程可能还未被调度,整个进程可能就已经终止。

所以将sleep注释掉再实验一下,运行结果如下,真的出现了子线程没有执行的情况。

main thread:  pid 2229 tid -1218578688 (0xb75df700)
此处子线程退出的原因我推测是主线程的退出先于子进程被调度,主线程退出后可能向所有子线程发送信号,因此子线程就得不到执行了。

11.5 线程终止

如果进程的任一线程调用exit、_exit或_Exit函数,那么整个进程就会终止。注意此处是整个进程都终止了。

单个线程可以通过3种方式退出,此处是仅退出线程,而不会终止整个进程。

  1. 线程可以简单地从启动历程中返回,返回值是线程的退出码。
  2. 线程可以被同一进程中的其他线程取消。
  3. 线程调用pthread_exit。

pthread_exit函数原型如下:

#include <pthread>
extern void pthread_exit (void *__retval) __attribute__ ((__noreturn__));

"__retval"参数是一个无类型指针,与传送给启动例程中的单个参数相类似,可作为启动例程的返回值。进程中的其他线程可以调用pthread_join等待特定的线程终止(通过上面介绍的三种方法),pthread_join函数原型如下:

<pre name="code" class="cpp">#include <pthread>
extern int pthread_join (pthread_t __th, void **__thread_return);


如果线程简单地从它的启动例程返回,“__retval”就包含返回码;如果线程被取消,由“__retval”指定的内存单元就被设置为“PTHREAD_CANCELED”。

通过一个实例对上面的内容进行一下简单的验证,源码如下:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.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);
}

int main()
{
	int err;
	pthread_t ptd1,ptd2;
	void* tret;

	err = pthread_create(&ptd1,NULL,thr_fn1,NULL);
	if(err!=0) perror("create thread1 error");
	err = pthread_create(&ptd2,NULL,thr_fn2,NULL);
	if(err!=0) perror("create thread2 error");

	err = pthread_join(ptd1,&tret);
	if(err!=0) perror("join thread1 error");
	printf("thread1 exit code %ld\n",(long)tret);

	err = pthread_join(ptd2,&tret);
	if(err!=0) perror("join thread2 error");
	printf("thread2 exit code %ld\n",(long)tret);

	exit(0);

}

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

./test_11_3
thread 1 returning
thread1 exit code 1
thread 2 exiting
thread2 exit code 2

在敲代码的过程中发现了一点觉得比较怪异的地方

void* tret;

直接申请了一个void指针用于存储数据,虽然没有为void指针指向的内存申请指针,但void指针本身具有地址,pthread_join的第二个参数是“void** ”类型,因此将tret的地址赋给第二个参数,程序最后的运行结果可以发现返回码直接放置在tret所在的地址空间中。

这里有一点使用上的问题要留心,pthread_create、pthread_exit函数中使用的void型指针参数所使用的内存在调用者完成调用以后必须是仍然有效的,所以为了解决这个问题,可以使用全局结构,或用malloc函数分配结构。书中也给出了一个实例,实验一下:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>

struct foo{
	int a,b,c,d;
};

void printfoo(const char* s,const struct foo* fp)
{
	printf("%s",s);
	printf("struct 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("thread1 :\n",&foo);
	pthread_exit((void*)&foo);
}

void* thr_fn2(void* arg)
{
	printf("thread2 ID is %lu\n",(unsigned long)pthread_self());
	pthread_exit((void*)0);
}

int main()
{
	int err;
	pthread_t ptd1,ptd2;
	struct foo* fp;

	err = pthread_create(&ptd1,NULL,thr_fn1,NULL);
	if(err!=0) perror("create thread1 error");

	err = pthread_join(ptd1,(void**)&fp);
	if(err!=0) perror("join thread1 error");

	sleep(1);
	printf("parent starting second thread\n");

	err = pthread_create(&ptd2,NULL,thr_fn2,NULL);
	if(err!=0) perror("create thread2 error");

	sleep(1);
	printfoo("main :\n",fp);

	exit(0);

}

又是原封不动的敲上去,实验结果如下:

thread1 :
struct at 0xb757834c
  foo.a = 1
  foo.b = 2
  foo.c = 3
  foo.d = 4
parent starting second thread
thread2 ID is 3075967808
main :
struct at 0xb757834c
  foo.a = -1217649422
  foo.b = 0
  foo.c = -1218999488
  foo.d = -1219001304

线程可以通过调用pthread_cancel函数来请求取消同一进程中的其他线程。函数原型如下:

extern int pthread_cancel (pthread_t __th);

注意pthread_cancel并不等待线程终止,它仅仅提出请求,被取消线程可以选择如何相应这一请求。

线程同样可以安装“线程清理处理程序”,处理程序的调用顺序与安装顺序正好相反。函数原型如下:

#include <pthread.h>
pthread_cleanup_push(routine, arg)
pthread_cleanup_pop(execute)

在我的机器上,这两个函数确实是通过宏实现的,但在我的机器上这两个函数的实现有好几个,所以我也仅仅是将函数的原型贴出来,具体怎么实现的,有机会再详细研究。

以下几种情况可以调用pthread_cleanup_push注册的清理函数:

  1. 调用pthread_exit函数。
  2. 响应取消请求时
  3. 用非零execute参数调用pthread_cleanup_pop函数。如果execute参数被设置为0,清理函数将不被调用。

处理程序记录在栈上。不管发生上述哪种情况,pthread_cleanup_pop都将删除上次pthread_cleanup_push调用建立的清理处理函数。

书中给出了一个例子,让我们也实验一下:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>

void cleanup(void* arg)
{
	printf("cleanup:%s \n",(char*)arg);
}

void* thr_fn1(void* arg)
{
	printf("thread1 start\n");
	pthread_cleanup_push(cleanup,"thread1 first handler");
	pthread_cleanup_push(cleanup,"thread1 second handler");
	printf("thread1 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("thread2 start\n");
	pthread_cleanup_push(cleanup,"thread2 first handler");
	pthread_cleanup_push(cleanup,"thread2 second handler");
	printf("thread2 push complete\n");

	if(arg) pthread_exit((void*)2);

	pthread_cleanup_pop(0);
	pthread_cleanup_pop(0);
	pthread_exit((void*)2);
}

int main()
{
	int err;
	pthread_t ptd1,ptd2;
	void* iret;

	err = pthread_create(&ptd1,NULL,thr_fn1,(void*)1);
	if(err!=0) perror("create thread1 error");

	err = pthread_create(&ptd2,NULL,thr_fn2,(void*)1);
	if(err!=0) perror("create thread2 error");

	err = pthread_join(ptd1,&iret);
	if(err!=0) perror("join thread1 error");

	printf("thread1 exit code %ld\n",(long)iret);

	err = pthread_join(ptd2,&iret);
	if(err!=0) perror("join thread1 error");

	printf("thread1 exit code %ld\n",(long)iret);


	exit(0);

}

程序运行结果如下:

./test_11_5
thread1 start
thread1 push complete
thread1 exit code 1
thread2 start
thread2 push complete
cleanup:thread2 second handler 
cleanup:thread2 first handler 
thread1 exit code 2

一开始程序的编写出现了一些小问题:

err = pthread_create(&ptd1,NULL,thr_fn1,NULL);

直接使用NULL参数调用thr_fn1函数,造成如下两句的调用:

pthread_cleanup_pop(0);
pthread_cleanup_pop(0);

由于使用0参数调用以上函数,所以直接删除了已经注册的清理函数,调用pthread_exit也就没有任何清理函数可以调用。

再来实验一下调用多个pthread_cleanup_pop会有什么结果?

此处直接编译错误,看来pthread_cleanup_push、pthread_cleanup_pop函数必须成对出现。

通过thread1的运行结果还可以发现:如果线程是通过从它的启动例程中返回而终止的话,它的清理处理程序就不会被调用。

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

extern int pthread_detach (pthread_t __th) __THROW;

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

11.6 线程同步

在开始学习这一章的内容之前,我们需要了解线程之间哪些数据是共享的,哪些数据是独享的,可以参考这篇blog:http://www.cnblogs.com/tracylee/archive/2012/10/29/2744228.html

11.6.1 互斥量

互斥量使用pthread_mutex_t数据类型表示,在使用互斥量以前,必须首先对其进行初始化,可以把他设置为常量“PTHREAD_MUTEX_INITIALIZER”(只适用于静态分配的互斥量),在我的机器上有两个定义:

#ifdef __PTHREAD_MUTEX_HAVE_PREV
# define PTHREAD_MUTEX_INITIALIZER \
  { { 0, 0, 0, 0, 0, __PTHREAD_SPINS, { 0, 0 } } }
# endif
#else
# define PTHREAD_MUTEX_INITIALIZER \
  { { 0, 0, 0, 0, 0, { __PTHREAD_SPINS } } }

看来pthread_mutex_t的定义并不简单。

也可以调用函数初始化互斥量。函数如下:

extern int pthread_mutex_init (pthread_mutex_t *__mutex,
			       const pthread_mutexattr_t *__mutexattr)
     __THROW __nonnull ((1));
extern int pthread_mutex_destroy (pthread_mutex_t *__mutex)
     __THROW __nonnull ((1));

如果动态分配互斥量(例如,通过malloc函数),再释放内存前需要调用pthread_mutex_destory。

对互斥量进行加锁,需要调用pthread_mutex_init。如果互斥量已经上锁,调用线程将阻塞直到互斥量被解锁。对互斥量解锁,需要解锁pthread_mutex_unlock。使用pthread_mutex_trylock尝试对互斥量进行加锁,如果调用pthread_mutex_trylock时互斥量处于未锁住状态,那么pthread_mutex_trylock将锁住互斥量,不会出现阻塞并返回0,否则pthread_mutex_trylock就会失败,不能锁住互斥量,返回EBUSY。

extern int pthread_mutex_lock (pthread_mutex_t *__mutex)
     __THROWNL __nonnull ((1));
extern int pthread_mutex_unlock (pthread_mutex_t *__mutex)
     __THROWNL __nonnull ((1));
extern int pthread_mutex_trylock (pthread_mutex_t *__mutex)
     __THROWNL __nonnull ((1));

11.6.2 避免死锁

书中通过具体的方法给出了避免死锁的方法。首先给出了一个具体的解除死锁的方法:使用相同的顺序加锁。

11.6.3 pthread_mutex_timedlock

当线程试图获取一个已加锁的互斥量是,pthread_mutex_timedlock原语允许绑定线程阻塞时间。函数原型如下:

extern int pthread_mutex_timedlock (pthread_mutex_t *__restrict __mutex,
				    const struct timespec *__restrict
				    __abstime) __THROWNL __nonnull ((1, 2));

当到达超时时间值时,pthread_mutex_timedlock返回错误码ETIMEDOUT。

11.6.4 读写锁

读写锁可以有3种状态:读模式下加锁状态、写模式下加锁状态、不加锁状态。

可通过以下函数初始化、销毁读写锁。

extern int pthread_rwlock_init (pthread_rwlock_t *__restrict __rwlock,
				const pthread_rwlockattr_t *__restrict
				__attr) __THROW __nonnull ((1));
extern int pthread_rwlock_destroy (pthread_rwlock_t *__rwlock)
     __THROW __nonnull ((1));

当读写锁处于写状态加锁时,在该锁被解除之前,所有试图对这个锁加锁的线程都会被阻塞。当以读模式加锁时,以读模式进行加锁的线程都可以获得访问权,而以写模式加锁的线程都会被阻塞。直到所有的线程释放它们的锁为止。虽然各操作系统对读写锁的实现不同,但当读写锁处于读模式加锁状态,而这时有一个线程试图以写模式获取锁时,读写锁通常会阻塞随后的读模式锁请求。这样可以避免读模式锁长期占用,而等待的写模式锁请求一直得不到满足。

通过以下函数对读写锁进行加锁、解锁。

extern int pthread_rwlock_rdlock (pthread_rwlock_t *__rwlock) //获取读模式锁
     __THROWNL __nonnull ((1));
extern int pthread_rwlock_wrlock (pthread_rwlock_t *__rwlock) //获取写模式锁
     __THROWNL __nonnull ((1));
extern int pthread_rwlock_unlock (pthread_rwlock_t *__rwlock) //解锁
     __THROWNL __nonnull ((1));

11.6.5 带有超时的读写锁

函数原型如下:

extern int pthread_rwlock_timedrdlock (pthread_rwlock_t *__restrict __rwlock,
				       const struct timespec *__restrict
				       __abstime) __THROWNL __nonnull ((1, 2));
extern int pthread_rwlock_timedwrlock (pthread_rwlock_t *__restrict __rwlock,
                       const struct timespec *__restrict
                       __abstime) __THROWNL __nonnull ((1, 2));

11.6.6 条件变量

条件变量是线程可用的另一种同步机制,这是本章中提到的第三种同步方式,前两种分别是:互斥量、读写锁。

使用条件变量前还是需要初始化,初始化的方法还是分为静态与动态两种,静态是通过PTHREAD_COND_INITIALIZER进行初始化,相关定义如下:

PTHREAD_COND_INITIALIZER

动态是通过函数进行初始化:

extern int pthread_cond_init (pthread_cond_t *__restrict __cond,
			      const pthread_condattr_t *__restrict __cond_attr)
     __THROW __nonnull ((1));

既然有初始化,就有销毁操作,函数原型如下:

extern int pthread_cond_destroy (pthread_cond_t *__cond)
     __THROW __nonnull ((1));

可以使用pthread_cond_wait函数等待条件变量变为真,如果在给定的时间内条件不能满足,那么会生成一个返回错误码的变量。调用者将锁住的互斥量传给函数,函数然后自动把调用线程放到等待条件的线程列表上,对互斥量解锁。pthread_wait_cond返回时,互斥量再次被锁住。

extern int pthread_cond_wait (pthread_cond_t *__restrict __cond,
			      pthread_mutex_t *__restrict __mutex)
     __nonnull ((1, 2));
extern int pthread_cond_timedwait (pthread_cond_t *__restrict __cond,
                   pthread_mutex_t *__restrict __mutex,
                   const struct timespec *__restrict __abstime)
     __nonnull ((1, 2, 3));

有两个函数用于通知线程条件已经满足。

pthread_cond_signal函数至少能唤醒一个等待该条件的线程。

extern int pthread_cond_signal (pthread_cond_t *__cond)
     __THROWNL __nonnull ((1));

pthread_cond_broadcast函数能唤醒等待该条件的所有线程。

extern int pthread_cond_broadcast (pthread_cond_t *__cond)
     __THROWNL __nonnull ((1));

11.6.7 自旋锁

自旋锁与互斥量类似,但它不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等待状态。自旋锁可用于以下情况:锁被持有的时间短。而且线程并不希望在重新调度上花费太多成本。

还是一样的做法,在使用自旋锁之前同样需要初始化,使用后需要销毁。

extern int pthread_spin_init (pthread_spinlock_t *__lock, int __pshared)
     __THROW __nonnull ((1));
extern int pthread_spin_destroy (pthread_spinlock_t *__lock)
     __THROW __nonnull ((1));

加锁及解锁函数如下:

extern int pthread_spin_lock (pthread_spinlock_t *__lock)
     __THROWNL __nonnull ((1));
extern int pthread_spin_trylock (pthread_spinlock_t *__lock)
     __THROWNL __nonnull ((1));
extern int pthread_spin_unlock (pthread_spinlock_t *__lock)
     __THROWNL __nonnull ((1));

需要注意,不要调用在持有自旋锁情况下可能进入休眠状态的函数,如果调用了这些函数,会浪费CPU资源,因为其他线程需要获取自旋锁需要等待的时间就延长了。

11.6.8 屏障

屏障允许每个线程等待,直到所有的合作线程都到达某一点,然后从该点继续执行。其实在MPI标准中也有一个类似的函数:MPI_Barrier。

屏障的使用也需要初始化与销毁。

extern int pthread_barrier_init (pthread_barrier_t *__restrict __barrier,
				 const pthread_barrierattr_t *__restrict
				 __attr, unsigned int __count)
     __THROW __nonnull ((1));
extern int pthread_barrier_destroy (pthread_barrier_t *__barrier)
     __THROW __nonnull ((1));

可以使用pthread_barrier_wait函数来表明,线程已完成工作,准备等其他所有线程赶上来。函数原型如下:

extern int pthread_barrier_wait (pthread_barrier_t *__barrier)
     __THROWNL __nonnull ((1));

对于一个任意线程,pthread_barrier_wait函数返回PTHREAD_BARRIER_SERIAL_THREAD(貌似无法指定某个线程返回这个值),这个值在我的机器上是-1。剩下的线程看到的返回值是0。这使得一个线程可以作为主线程,它可以工作在其他所有线程已完成的工作结果上。




发布了100 篇原创文章 · 获赞 24 · 访问量 18万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览