Linux Pthread 深入解析

 1.线程特点

- 2.pthread创建

- 3.pthread终止

        - 4.mutex互斥量使用框架

        - 5.cond条件变量

        - 6.综合实例

================================================================================================
1. 线程特点
线程拥有自己独立的栈、调度优先级和策略、信号屏蔽字(创建时继承)、errno变量以及线程私有数据。进程的其他地址空间均被所有线程所共享,因此线程可以访问程序的全局变量和堆中分配的数据,并通过同步机制保证对数据访问的一致性。

2. pthread创建
pthread有一个线程ID,类型为pthread_t,在使用printf打印时,应转换为u类型。
pthread_equal可用于比较两个id是否相等;pthread_self用于获取当前线程的ID。
pthread_create用于创建新的线程,可以给线程传入一个void *类型的参数,例如一个结构体指针或者一个数值。
系统并不能保证哪个线程会现运行:新创建的线程还是调用线程。

3. pthread终止
a) 从线程函数中返回
b) 被同一进程中的其他线程取消
c) 线程调用pthread_exit
注意,线程的返回值需要转换为void *类型。

pthread_exit(void *ret)
pthread_join(pthread_t id, void **ret)

ret均可设置为NULL

4.  mutex 互斥量使用框架
pthread_mutex_t lock;
pthread_mutex_init 或者 PTHREAD_MUTEX_INITIALIZER(仅可用在静态变量)
pthread_mutex_lock / pthread_mutex_unlock / pthread_mutex_trylock
pthread_mutex_destroy

5. cond 条件变量
pthread_cond_t qready;
pthread_mutex_t qlock;
pthread_mutex_init 或者 PTHREAD_MUTEX_INITIALIZER
pthread_cond_init 或者 PTHREAD_COND_INITIALIZER
pthread_mutex_lock(&qlock...)
pthread_cond_wait(&qready, &qlock...) / pthread_cond_timewait
pthread_mutex_unlock(&qlock)
pthread_cond_destroy

//唤醒条件变量
pthread_cond_signal
pthread_cond_broadcast

条件变量是pthread中比较难以理解的一点,主要会产生以下疑惑:
Q1. 假如在调用pthread_{cond_wait | cond_timedwait}之前就调用pthread_cond_{signal | broadcast}会发生什么?

Q2. pthread_cond_{cond_wait | cond_timewait}为什么需要一个已经锁住的mutex作为变量?

Q3. pthread_cond_{signal | broadcast}使用之前必须获取wait中对应的mutex吗?

Q4. 假如pthread_cond_{signal | broadcast}必须获取mutex,那么下列两种形式,哪种正确?为什么?

 1)
 lock(lock_for_X);
 change(X);
 unlock(lock_for_X);
 pthread_cond_{signal | broadcast};

 2)
 lock(lock_for_X);
 change(X);
 pthread_cond_{signal | broadcast};
 unlock(lock_for_X);

----思考-------思考-------思考-------思考------思考-------思考------思考------思考-------思考-------

A1: 什么都不会发生,也不会出错,仅仅造成这次发送的signal丢失。

A2: 一般场景如下,我们需要检查某个条件是否满足(如队列X是否为空、布尔Y是否为真),假如没有条件变量,我们唯一的选择是

  1. while (1) {
  2.   lock(lock_for_X);

  3.   if (is not empty) {
  4.     unlock(lock_for_X);
  5.     break;
  6.   } else { //is empty, loop continues
  7.     unlock(lock_for_X);
  8.     sleep(10);
  9.   }
  10. }
  11. //is not empty, loop ends
  12. process(X);

明显这种轮询的方式非常耗费CPU时间,这时候我们很容易的想到,如果有一种机制,可以异步通知我们队列的状态发生了变化,那么我们便无须再轮询,只要等到通知到来时再检查条件是否满足即可,其他时间则将程序休眠,因此现在代码变成这样:

  1. while (1) {
  2.   lock(lock_for_X);
  3.   if (is not empty) { 
  4.     unlock(lock_for_X);
  5.     break;
  6.   } else {
  7.     unlock(lock_for_X); //must called before my_wait(), otherwise no one can acquire the lock and make change to X
  8.     -------------------------------------->窗口,由于已经解锁,其他程序可能改变X,并且试图唤醒mywait,但在一个繁忙的系统中,可能此时my_还没被调用!
  9.     my_wait(); //go to sleep and wait for the notification
  10.   }
  11. }

my_wait是一个假想的函数,作用如注释所示。
不难发现,这样做以后,我们无须再轮询了,只需要等待my_wait()被唤醒以后检查条件是否满足。
但是请注意,正如图中所示,存在1个时间窗口。若其他程序在这个窗口中试图唤醒my_wait,由于此时my_wait还没有被调用,那么这个信号将丢失,造成my_wait一直阻塞。解决的办法就是,要将unlock和my_wait合并成一个原子操作,这样就不会被其他程序插入执行。我想到这里,你应该已经明白了,这个原子操作的函数就是pthread_cond_{signal | broadcast}.

A3: 是的。

A4: 对于1),在不同的操作系统中,可能会造成不确定的调度结果(可能会造成调度优先级反转);对于2)可以保证无论在何种操作系统中都将获得预期的调度顺序。

设想一个场景:有两个消费者线程A和B,我们设定A的优先级比B高,A正在等待条件变量被出发,即已经调用了pthread_wait,并且处于阻塞状态:

  1. lock(lock_for_X);
  2. while (is empty) {
  3.   pthread_cond_wait(&qready, &lock_for_X);
  4. }
  5. unlock(lock_for_X);

B中没有调用pthread_wait,而是做类似如下的处理:

  1. while(1) { 
  2.   lock(lock_for_X);
  3.   dequeue(X);
  4.   unlock(lock_for_X);
  5. }
另一个线程C,为生产者,采用1)方案,则代码如下,先unlock,再发出signal:

 lock(lock_for_X);
 change(X);
 unlock(lock_for_X);
 pthread_cond_{signal | broadcast};

当发出unlock以后,发送signal之前,此时消费者B已经满足了运行条件,而消费者A虽然优先级比B高,但是由于其运行条件还需要signal,所以不具备立刻运行的条件,此时就看操作系统如何实现调度算法了。有些操作系统,可能会因为A不具备立刻运行条件,即使它的优先级比B高,此时还是让B线程先运行,那么,后续将分成两种情况:

(a) B获得了lock,但是还没有将X队列中的刚刚加入的条目移除,此时C调用了signal,A接收到了signal,由于A的优先级高,那么A抢占B,A从函数pthread_cond_wait返回之前需要再次将lock上锁,但是A抢占后发现,lock被人锁住了(还没有被B释放),只好再次休眠,等待锁被释放,结果B又被唤醒,也可能因此造成A和B的死锁,这个具体要看操作系统的调度算法。

(b) B获得了lock,并且执行了dequeue,然后释放了锁。此时C调用了signal,A接收到了signal,由于A的优先级高,那么A抢占B,A这次顺利的获取了锁得以从pthread_cond_wait中返回,但是在检查条件时,却发现队列是空的,于是乎再次进入pthread_cond_wait休眠。结果A又无法被执行,A可能由此进入饥饿状态。

但是如果C采用2)方案:

 lock(lock_for_X);
 change(X);
 pthread_cond_{signal | broadcast};
 unlock(lock_for_X);

在unlock以后,A、B都具备了立即运行的条件,由于A比B的优先级高,因此操作系统必定会先调度A执行,就避免了前面一种不确定的调度结果。


6. 综合实例
  1. /*
  2.  * =====================================================================================
  3.  *
  4.  * Filename: pthread.c
  5.  *
  6.  * Description: 
  7.  *
  8.  * Version: 1.0
  9.  * Created: 08/17/11 11:06:35
  10.  * Revision: none
  11.  * Compiler: gcc
  12.  *
  13.  * Author: YOUR NAME (), 
  14.  * Company: 
  15.  *
  16.  * =====================================================================================
  17.  */
  18. #include <stdio.h>
  19. #include <pthread.h>
  20. #include <error.h>
  21. #include <stdlib.h>
  22. #include <unistd.h>
  23. #include <string.h>

  24. pthread_cond_t qready;
  25. pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;

  26. struct foo {
  27.     int cnt;
  28.     pthread_mutex_t f_lock;
  29. };

  30. void cleanup(void *arg)
  31. {
  32.     printf("clean up: %s\n", (char *)arg);
  33. }

  34. void printids(char *str)
  35. {
  36.     printf("%s pid = %u tid = %u / 0x%x\n", 
  37.             str, (unsigned int)getpid(), (unsigned int)pthread_self(), (unsigned int)pthread_self());
  38. }

  39. void *thread1(void *arg)
  40. {
  41.     pthread_mutex_lock(&qlock);
  42.     pthread_cond_wait(&qready, &qlock);
  43.     pthread_mutex_unlock(&qlock);

  44.     printids("thread1:");
  45.     
  46.     pthread_cleanup_push(cleanup, "thread 1 first cleanup handler");
  47.     pthread_cleanup_push(cleanup, "thread 1 second cleanup handler");
  48.     printf("thread 1 push complete!\n");
  49.     
  50.     pthread_mutex_lock(&((struct foo *)arg)->f_lock);
  51.     ((struct foo *)arg)->cnt ;
  52.     printf("thread1: cnt = %d\n", ((struct foo *)arg)->cnt);
  53.     pthread_mutex_unlock(&((struct foo *)arg)->f_lock);

  54.     if (arg) 
  55.         return ((void *)0);
  56.     
  57.     pthread_cleanup_pop(0);
  58.     pthread_cleanup_pop(0);

  59.     pthread_exit((void *)1);
  60. }

  61. void *thread2(void *arg)
  62. {
  63.     int exit_code = -1;
  64.     printids("thread2:");
  65.     
  66.     printf("Now unlock thread1\n");

  67.     pthread_mutex_lock(&qlock);
  68.     pthread_mutex_unlock(&qlock);
  69.     pthread_cond_signal(&qready);

  70.     printf("Thread1 unlocked\n");

  71.     pthread_cleanup_push(cleanup, "thread 2 first cleanup handler");
  72.     pthread_cleanup_push(cleanup, "thread 2 second cleanup handler");
  73.     printf("thread 2 push complete!\n");
  74.     
  75.     if (arg) 
  76.         pthread_exit((void *)exit_code);

  77.     pthread_cleanup_pop(0);
  78.     pthread_cleanup_pop(0);
  79.     
  80.     pthread_exit((void *)exit_code);
  81. }

  82. int main(int argc, char *argv[])
  83. {
  84.     int ret;
  85.     pthread_t tid1, tid2;
  86.     void *retval;
  87.     struct foo *fp;

  88.     ret = pthread_cond_init(&qready, NULL);
  89.     if (ret != 0) {
  90.         printf("pthread_cond_init error: %s\n", strerror(ret));
  91.         return -1;
  92.     }



  93.     if ((fp = malloc(sizeof(struct foo))) == NULL) {
  94.         printf("malloc failed!\n");
  95.         return -1;
  96.     }

  97.     if (pthread_mutex_init(&fp->f_lock, NULL) != 0) {
  98.         free(fp);
  99.         printf("init mutex failed!\n");
  100.     }

  101.     pthread_mutex_lock(&fp->f_lock);

  102.     ret = pthread_create(&tid1, NULL, thread1, (void *)fp);
  103.     if (ret != 0) {
  104.         printf("main thread error: %s\n", strerror(ret));
  105.         return -1;
  106.     }
  107.     ret = pthread_create(&tid2, NULL, thread2, (void *)1);
  108.     if (ret != 0) {
  109.         printf("main thread error: %s\n", strerror(ret));
  110.         return -1;
  111.     }

  112.     
  113.     ret = pthread_join(tid2, &retval);
  114.     if (ret != 0) {
  115.         printf("pthread join falied!\n");
  116.         return -1;
  117.     }
  118.     else
  119.         printf("thread2 exit code %d\n", (int)retval);

  120.     fp->cnt = 1;
  121.     printf("main thread: cnt = %d\n",fp->cnt);

  122.     pthread_mutex_unlock(&fp->f_lock);

  123.     sleep(1);    //there is no guarantee the main thread will run before the newly created thread, so we wait for a while 
  124.     printids("main thread:");

  125.     printf("Press to exit\n");

  126.     ret = pthread_cond_destroy(&qready);
  127.     if (ret != 0) {
  128.         printf("pthread_cond_destroy error: %s\n", strerror(ret));
  129.         return -1;
  130.     }

  131.     getchar();
  132.     return 0;
  133. }

pthread_cond_wait总和一个互斥锁结合使用。在调用pthread_cond_wait前要先获取锁。pthread_cond_wait函数执行时先自动释放指定的锁,然后等待条件变量的变化。在函数调用返回之前,自动将指定的互斥量重新锁住。

int pthread_cond_signal(pthread_cond_t * cond);

pthread_cond_signal通过条件变量cond发送消息,若多个消息在等待,它只唤醒一个。pthread_cond_broadcast可以唤醒所有。调用pthread_cond_signal后要立刻释放互斥锁,因为pthread_cond_wait的最后一步是要将指定的互斥量重新锁住,如果pthread_cond_signal之后没有释放互斥锁,pthread_cond_wait仍然要阻塞。


无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()(或pthread_cond_timedwait(),下同)的竞争条件(Race   Condition)。mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁 (PTHREAD_MUTEX_ADAPTIVE_NP),且在调用pthread_cond_wait()前必须由本线程加锁 (pthread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开 pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。  
   
  激发条件有两种形式,pthread_cond_signal()激活一个等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个;而pthread_cond_broadcast()则激活所有等待线程。

下面是另一处说明:给出了函数运行全过程。 为什么在唤醒线程后要重新mutex加锁?

了解 pthread_cond_wait() 的作用非常重要 -- 它是 POSIX 线程信号发送系统的核心,也是最难以理解的部分。

首先,让我们考虑以下情况:线程为查看已链接列表而锁定了互斥对象,然而该列表恰巧是空的。这一特定线程什么也干不了 -- 其设计意图是从列表中除去节点,但是现在却没有节点。因此,它只能:

锁定互斥对象时,线程将调用 pthread_cond_wait(&mycond,&mymutex)。pthread_cond_wait() 调用相当复杂,因此我们每次只执行它的一个操作。

pthread_cond_wait() 所做的 第一件事 就是同时对互斥对象解锁(于是其它线程可以修改已链接列表),并等待条件 mycond 发生(这样当 pthread_cond_wait() 接收到另一个线程的“信号”时,它将苏醒)。现在互斥对象已被解锁,其它线程可以访问和修改已链接列表,可能还会添加项。 【 要求解锁并阻塞是一个原子操作

此时,pthread_cond_wait() 调用还未返回。对互斥对象解锁会立即发生,但等待条件 mycond 通常是一个阻塞操作,这意味着线程将睡眠,在它苏醒之前不会消耗 CPU 周期。这正是我们期待发生的情况。线程将 一直睡眠,直到特定条件发生 ,在这期间不会发生任何浪费 CPU 时间的繁忙查询。从线程的角度来看,它只是在等待 pthread_cond_wait() 调用返回。

现在继续说明,假设另一个线程(称作“2 号线程”)锁定了 mymutex 并对已链接列表添加了一项。在对互斥对象解锁之后,2 号线程会立即调用函数 pthread_cond_broadcast(&mycond)。此操作之后,2 号线程将使所有等待 mycond 条件变量的线程立即苏醒。这意味着第一个线程(仍处于 pthread_cond_wait() 调用中)现在将 苏醒

现在,看一下第一个线程发生了什么。您可能会认为在 2 号线程调用 pthread_cond_broadcast(&mymutex) 之后,1 号线程的 pthread_cond_wait() 会立即返回。不是那样!实际上,pthread_cond_wait() 将执行最后一个操作: 重新锁定 mymutex 。一旦 pthread_cond_wait() 锁定了互斥对象,那么它将返回并允许 1 号线程继续执行。那时,它可以马上检查列表,查看它所感兴趣的更改。


来看一个例子(你是否能理解呢?):

 

In Thread1:

pthread_mutex_lock(&m_mutex);   
pthread_cond_wait(&m_cond,&m_mutex);   
pthread_mutex_unlock(&m_mutex);  

 

In Thread2:

pthread_mutex_lock(&m_mutex);   
pthread_cond_signal(&m_cond);   
pthread_mutex_unlock(&m_mutex);  

 

为什么要与pthread_mutex 一起使用呢? 这是为了应对 线程1在调用pthread_cond_wait()但线程1还没有进入wait cond的状态的时候,此时线程2调用了 cond_singal 的情况。 如果不用mutex锁的话,这个cond_singal就丢失了。加了锁的情况是,线程2必须等到 mutex 被释放(也就是 pthread_cod_wait() 释放锁并进入wait_cond状态 ,此时线程2上锁) 的时候才能调用cond_singal.


pthread_cond_signal即可以放在pthread_mutex_lock和pthread_mutex_unlock之间,也可以放在pthread_mutex_lock和pthread_mutex_unlock之后,但是各有有缺点。

之间:
pthread_mutex_lock
    xxxxxxx
pthread_cond_signal
pthread_mutex_unlock
缺点:在某下线程的实现中,会造成等待线程从内核中唤醒(由于cond_signal)然后又回到内核空间(因为cond_wait返回后会有原子加锁的 行为),所以一来一回会有性能的问题。但是在LinuxThreads或者NPTL里面,就不会有这个问题,因为在Linux 线程中,有两个队列,分别是cond_wait队列和mutex_lock队列, cond_signal只是让线程从cond_wait队列移到mutex_lock队列,而不用返回到用户空间,不会有性能的损耗。
所以在Linux中推荐使用这种模式。

之后:
pthread_mutex_lock
    xxxxxxx
pthread_mutex_unlock
pthread_cond_signal
优点:不会出现之前说的那个潜在的性能损耗,因为在signal之前就已经释放锁了
缺点:如果unlock和signal之前,有个低优先级的线程正在mutex上等待的话,那么这个低优先级的线程就会抢占高优先级的线程(cond_wait的线程),而这在上面的放中间的模式下是不会出现的。


微软是个大宝包。什么东西都封装了。
Thread 就是线程。一个小小的对象而已。线程上可以执行一个函数。主要用法可以用来并发执行一些动作,也能在不阻塞UI的情况下完成一些持续计算。


但是,很多人觉得每次调用一个函数都要new一个线程是很麻烦的。所以干脆提前New好了很多线程。装在一个list中。你要调用函数的时候就从list中提取出一个空闲的线程。函数执行完毕后,就把这个空闲的线程又放到这个list中。减少了new thread的时间。


所以线程池,说白了就是List<Thread>。提供一个方法,让你能方便的把自己的函数不管三七二十一都放这个List中去,然后依次执行。


所以,如果你常常使用系统的线程池,你甚至不需要知道Thread是什么东西。你只要知道,这是一个魔术口袋,你把你的函数塞进去,过一阵子就执行完了。根本不要你来操心。


微软真是培养懒人啊。。


后来大家发现,线程池也不方便,因为进了这个魔术口袋的函数,你不能突然中断它的执行。在多核时代,它的效率也不尽如人意。所以微软又把原来的线程池改造了一下,现在都不叫threadPool了。直接叫Task。你不必管我是怎么实现的,你只要把函数塞我肚肚里,我一定会执行。而且你能用我提供的API。来控制整个过程。


所以Task。就是一个方便使用的线程池。至于把函数塞进去。肯定是在其它线程中执行的,只是这不是我们需要操心的了
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值