UNIX高级编程:第11章 线程

请移步到这里:

http://note.youdao.com/noteshare?id=1181ceb083c1dcf4a22415ad2433a3be&sub=FBAFCED22E034E15BE56133783B03C16

 

对于线程, Linux提供了gettid系统调用来返回其线程ID, 可惜的是glibc并没有将该系统调用封装起来, 再开放出接口来供程序员使用。 如果确实需要获取线程ID, 可以采用如下方法:
#define _GNU_SOURCE         /* See feature_test_macros(7) */
#include <unistd.h>
#include <sys/syscall.h> 
int TID = syscall(SYS_gettid);

#ps -Lef

UID         PID   PPID    LWP  C NLWP STIME TTY          TIME CMD

root      58617   3187  58617  0    4 19:10 pts/4    00:00:00 ./a.out
root      58617   3187  58618  0    4 19:10 pts/4    00:00:00 ./a.out
root      58617   3187  58619 97    4 19:10 pts/4    00:00:20 ./a.out
root      58617   3187  58620  0    4 19:10 pts/4    00:00:00 ./a.out

ps命令中的-L选项, 会显示出线程的如下信息。
·LWP: 线程ID, 即gettid() 系统调用的返回值。
·NLWP: 线程组内线程的个数。
所以从上面可以看出a.out进程是多线程的, 进程ID为58617, 进程内有4个线程, 线程ID分别为58617、 58618、 58619和58620

线程组内的第一个线程, 在用户态被称为主线程(main thread) , 在内核中被称为Group Leader。 内核在创建第一个线程时, 会将线程组ID的值设置成第一个线程的线程ID。
 

线程和进程不一样, 进程有父进程的概念, 但在线程组里面, 所有的线程都是对等的关系(如图7-6所示) 。

  • 并不是只有主线程才能创建线程, 被创建出来的线程同样可以创建线程。
  • 不存在类似于fork函数那样的父子关系, 大家都归属于同一个线程组, 进程ID都相等, group_leader都指向主线程, 而且各有各的线程ID。
  • 并非只有主线程才能调用pthread_join连接其他线程, 同一线程组内的任意线程都可以对某线程执行pthread_join函数。
  • 并非只有主线程才能调用pthread_detach函数, 其实任意线程都可以对同一线程组内的线程执行分离操作。
     

线程常用库函数:

 

pthread_create函数
 

#include <pthread.h>
int pthread_create(pthread_t *restrict thread,
                   const pthread_attr_t *restrict attr,
                   void *(*start_routine)(void*),
                   void *restrict arg)

第一个参数是pthread_t类型的指针, 线程创建成功的话, 会将分配的线程ID填入该指针指向的地址。 线程的后续操作将
使用该值作为线程的唯一标识。
第二个参数是pthread_attr_t类型, 通过该参数可以定制线程的属性, 比如可以指定新建线程栈的大小、 调度策略等。 如果创建线程无特殊的要求, 该值也可以是NULL, 表示采用默认属性。
第三个参数是线程需要执行的函数。 创建线程, 是为了让线程执行一定的任务。 线程创建成功之后, 该线程就会执行start_routine函数, 该函数之于线程, 就如同main函数之于主线程。
第四个参数是新建线程执行的start_routine函数的入参,一般用结构体做参数。
 

 

线程ID及进程地址空间布局


pthread_create函数, 会产生一个线程ID, 存放在第一个参数指向的地址中。 该线程ID和上面用命令输出的线程ID是一回事吗?
答案是否定的。


上面用命令输出的线程ID, 属于进程调度的范畴。 因为线程是轻量级进程, 是操作系统调度器的最小单位, 所以需要一个数值来唯一标识该线程。
pthread_create函数产生并记录在第一个参数指向地址的线程ID中, 属于NPTL线程库的范畴, 线程库的后续操作, 就是根据该线程ID来操作线程的。
线程库NPTL提供了pthread_self函数, 可以获取到线程自身的ID:

#include <pthread.h>
pthread_t pthread_self(void);

 

给线程起一个有意义的名字

命名以后, 既可以从procfs中获取到线程的名字, 也可以从ps命令中得到线程的名字, 这样就可以更好地辨识不同的线程。
Linux提供了prctl系统调用:
 

#include <sys/prctl.h>
int prctl(int option, unsigned long arg2,
            unsigned long arg3 , unsigned long arg4,
            unsigned long arg5)

这个系统调用和ioctl非常类似, 通过option来控制系统调用的行为。 当需要给线程设定名字的时候, 只需要将option设为PR_SET_NAME, 同时将线程的名字作为arg2传递给prctl系统调用即可, 这样就能给线程命名了。
下面是示例代码:
 

void thread_setnamev(const char* namefmt, va_list args)
{
    char name[17];
    vsnprintf(name, sizeof(name), namefmt, args);
    prctl(PR_SET_NAME, name, NULL, NULL, NULL);
}
void thread_setname(const char* namefmt, ...)
{
    va_list args;
    va_start(args, namefmt);
    thread_setnamev(namefmt, args);
    va_end(args);
}
thread_setname("BEAN-%d",num);

这里共创建了四个线程, 按照调用pthread_create的顺序, 将0、 1、 2、 3作为参数传递给线程, 然后调用prctl给每个线程起名字: 分别为BEAN-0、 BEAN-1、 BEAN-2和BEAN-3。 命名以后可以通过ps命令来查看线程的名字:
#ps -L -p 3454
PID LWP TTY TIME CMD
3454 3454 pts/0 00:00:00 pthread_tid
3454 3455 pts/0 00:00:00 BEAN-0
3454 3456 pts/0 00:00:00 BEAN-1
3454 3457 pts/0 00:00:00 BEAN-2
3454 3458 pts/0 00:00:00 BEAN-3
manu@manu-hacks:~$ cat /proc/3454/task/3457/status
Name: BEAN-2
State: S (sleeping)
Tgid: 3454
 

# ulimit -s
8192    //线程的栈大小

 

线程的连接与分离
1 线程的连接
 

线程退出时是可以有返回值的, 那么如何取到线程退出时的返回值呢?

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

该函数第一个参数为要等待的线程的线程ID, 第二个参数用来接收返回值。
根据等待的线程是否退出, 可得到如下两种情况:
等待的线程尚未退出, 那么pthread_join的调用线程就会陷入阻塞。
等待的线程已经退出, 那么pthread_join函数会将线程的退出值(void*类型) 存放到retval指针指向的位置。


线程的连接(join) 操作有点类似于进程等待子进程退出的等待(wait) 操作, 但细细想来, 还是有不同之处:
第一点不同之处是进程之间的等待只能是父进程等待子进程, 而线程则不然。 线程组内的成员是对等的关系, 只要是在一个线程组内, 就可以对另外一个线程执行连接(join) 操作。

第二点不同之处是进程可以等待任一子进程的退出(用下面的代码不难做到) , 但是线程的连接操作没有类似的接口, 即不能连接线程组内的任一线程, 必须明确指明要连接的线程的线程ID。
 

2 为什么要连接退出的线程


不连接已经退出的线程会怎么样?
如果不连接已经退出的线程, 会导致资源无法释放。 所谓资源指的又是什么呢?

资源就是栈空间

带来了两个问题:

  1. 已经退出的线程, 其空间没有被释放, 仍然在进程的地址空间之内。
  2. 新创建的线程, 没有复用刚才退出的线程的地址空间。
     

3 线程的分离


默认情况下, 新创建的线程处于可连接(Joinable) 的状态, 可连接状态的线程退出后, 需要对其执行连接操作, 否则线程资源无法释放,从而造成资源泄漏。
如果其他线程并不关心线程的返回值, 那么连接操作就会变成一种负担: 你不需要它, 但是你不去执行连接操作又会造成资源泄漏。 这时候你需要的东西只是: 线程退出时, 系统自动将线程相关的资源释放掉, 无须等待连接。
NPTL提供了pthread_detach函数来将线程设置成已分离(detached) 的状态, 如果线程处于已分离的状态, 那么线程退出时, 系统将负责回收线程的资源, 如下:
 

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

可以是线程组内其他线程对目标线程进行分离, 也可以是线程自己执行pthread_detach函数, 将自身设置成已分离的状态, 如下:
 

pthread_detach(pthread_self())

需要强调的是, 不要误解已分离状态的内涵。 所谓已分离, 并不是指线程失去控制, 不归线程组管理, 而是指线程退出后, 系统会自动释
放线程资源。 若线程组内的任意线程执行了exit函数, 即使是已分离的线程, 也仍然会受到影响, 一并退出。
将线程设置成已分离状态, 并非只有pthread_detach一种方法。 另一种方法是在创建线程时, 将线程的属性设定为已分离:
 

 

pthread_attr_t attr_1;
pthread_attr_init(&attr_1);	
pthread_attr_setdetachstate(&attr_1, PTHREAD_CREATE_DETACHED); //设置默认分离属性
pthread_create(&tid1, &attr_1, th_reader1, NULL);  //创建 读 线程1	

其中detachstate的可能值

有了这个, 如果确实不关心线程的返回值, 可以在创建线程之初, 就指定其分离属性为PTHREAD_CREATE_DETACHED。
 

 

临界区的大小


现在, 我们已经意识到需要用锁来保护共享变量。 不过还有另一个需要注意的事项, 即合理地设定临界区的范围。
第一临界区的范围不能太小, 如果太小, 可能起不到保护的目的。 考虑如下场景, 如果哈希表中不存在某元素, 那么向哈希表中插入某元素, 代码如下:
 

if(!htable_contain(hashtable,elem.key))
{
    pthread_mutex_lock(&mutex);
    htable_insert(hashtable,&elem);
    pthread_mutex_lock(&mutex);
}

表面上看, 共享变量hashtable得到了保护, 在插入时有锁保护, 但是结果却不是我们想要的。 上面的程序不希望哈希表中有重复的元素, 但是其临界区太小, 多线程条件下可能达不到预设的效果。
如果时序如图7-15所示, 那么就会有重复的元素被插入哈希表中, 没有达到最初的目的。 究其原因, 就是临界区小了, 没有将判断部分加入临界区以内。
临界区也不能太大, 临界区的代码不能并发, 如果临界区太大, 就无法充分利用多处理器发挥多线程的优势。 对于被互斥量保护的临界区内的代码, 一定要好好审视, 不要将不相干的(特别是可能陷入阻塞的) 代码放入临界区内执行。

 

死锁和活锁
死锁

活锁
当我们使用trylock()函数的时候,trylock不行就回退的思想有可能会引发活锁(live lock) 。 生活中也经常遇到两个人迎面走来, 双
方都想给对方让路, 但是让的方向却不协调, 反而互相堵住的情况(如图7-19所示) 。 活锁现象与这种场景有点类似。

 

考虑下面两个线程, 线程1首先申请锁mutex_a后, 之后尝试申请mutex_b, 失败以后, 释放mutex_a进入下一轮循环, 同时线程2会因为尝试申请mutex_a失败, 而释放mutex_b, 如果两个线程恰好一直保持这种节奏, 就可能在很长的时间内两者都一次次地擦肩而过。 当然这毕竟不是死锁, 终究会有一个线程同时持有两把锁而结束这种情况。 尽管如此, 活锁的确会降低性能。
 

读写锁总结


从宏观意义上看, 读写锁要比互斥量并发性好, 因为读写锁在更多的时间区域内允许并发。
如果认为读写锁是完美的, 以至于认为互斥锁没有存在的必要, 那就是too young, toosimple, sometimes naive了。 Bryan Cantrill和Jeff Bonwick在《Real-world Concurrency》 中提出的并发编程的建议里提到了要警惕读写锁(Be wary of readers-writer locks) 。 读写锁存在如下的短处。

  • 性能: 如果临界区比较大, 读写锁高并发的优势就会显现出来, 但是如果临界区非常小, 读写锁的性能短板就会暴露出来。 由于读写锁无论是加锁还是解锁, 首先都会执行互斥操作, 加上读写锁还需要维护当前读者线程的个数、 写锁等待线程的个数、 读锁等待线程的个数, 因此这就决定了读写锁的开销不会小于互斥量。
  • 饿死: 互斥量虽然不是绝对意义上的公正, 但是线程不会饿死。 但是如上一小节的讨论, 读者优先的策略下, 写线程可能会饿死。 写者优先的情况下, 读线程可能会饿死。
  • 死锁: 读锁是可重入的, 这就可能会引发死锁。 考虑如下场景, 读写锁采用写者优先的策略, A线程已经持有读锁, B线程申请了写锁, 正处于等待状态, 而持有读锁的A线程再次申请读锁, 就会发生死锁。

比较适合读写锁的场景是: 临界区的大小比较可观, 绝大多数情况下是读, 只有非常少的写。
 

 

 

 

 

 

 

 

 

 

 

 

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值