Linux线程(pthread)学习笔记(一)

来源:互联网 发布:python DAG教程 编辑:程序博客网 时间:2024/05/28 05:18

声明:本文内容参考自:

http://fanqiang.chinaunix.net/a4/b8/20010811/0905001105.html Linux下的多线程编程

http://blog.csdn.net/lhf_tiger/article/details/8291984 pthrad_attr_setdetachstate

http://blog.csdn.net/ithomer/article/details/6063067 Linux多线程Pthread学习小结

等大神的博客~具体的不一一列出了 


好了,回归正文

Linux系统下的多线程遵循POSIX线程接口,称为pthread


使用多线程的优点:

  1. 众所周知创建一个线程的开销远比创建一个进程的开销要小的多.

    启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,
    这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,
    共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间

  2. 对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便

  3. 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况

  4. 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。

  5. 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。

线程常用数据类型和方法:
数据类型:
pthread_t  xxx 线程句柄
pthread_attr_t  xxx 线程属性(是一个struct结构体)
pthread_mutex_t  xxx 互斥锁
pthread_cond_t   xxx 条件变量
常用方法:(忽略参数)
pthread_create();创建线程(注意,程序执行到这里仅仅是创建一个线程,而不是执行线程.而是有睡眠的时候,系统通过调度执行线程函数)

pthread_exit():终止当前线程

pthread_join():阻塞当前的线程,直到另外一个线程运行结束

pthread_attr_init():初始化线程的属性   

pthread_attr_setdetachstate():设置脱离状态的属性(决定这个线程在终止时是否可以被结合) 

pthread_kill():向线程发送一个信号

pthread_mutex_init() 初始化互斥锁

pthread_mutex_lock():占有互斥锁(阻塞操作)

pthread_mutex_trylock():试图占有互斥锁(不阻塞操作)。当互斥锁空闲时将占有该锁;否则立即返回  
pthread_mutex_unlock(): 释放互斥锁

pthread_mutex_unlock(): 释放互斥锁   
pthread_cond_init():初始化条件变量   
pthread_cond_destroy():销毁条件变量   

pthread_cond_wait(): 等待条件变量的特殊条件发生
pthread_cond_signal(): 唤醒第一个调用pthread_cond_wait()而进入睡眠的线程


线程的合并与分离:

我们首先要明确的一个问题就是什么是线程的合并。从前面的叙述中读者们已经了解到了,pthread_create()接口负责创建了一个线程。那么线程也属于系统的资源,这跟内存没什么两样,而且线程本身也要占据一定的内存空间。众所周知的一个问题就是C或C++编程中如果要通过malloc()或new分配了一块内存,就必须使用free()或delete来回收这块内存,否则就会产生著名的内存泄漏问题。既然线程和内存没什么两样,那么有创建就必须得有回收,否则就会产生另外一个著名的资源泄漏问题,这同样也是一个严重的问题。那么线程的合并就是回收线程资源了。

线程的合并是一种主动回收线程资源的方案。当一个进程或线程调用了针对其它线程的pthread_join()接口,就是线程合并了。这个接口会阻塞调用进程或线程,直到被合并的线程结束为止。当被合并线程结束,pthread_join()接口就会回收这个线程的资源,并将这个线程的返回值返回给合并者。
与线程合并相对应的另外一种线程资源回收机制是线程分离,调用接口是pthread_detach()。线程分离是将线程资源的回收工作交由系统自动来完成,也就是说当被分离的线程结束之后,系统会自动回收它的资源。因为线程分离是启动系统的自动回收机制,那么程序也就无法获得被分离线程的返回值,这就使得pthread_detach()接口只要拥有一个参数就行了,那就是被分离线程句柄。
线程合并和线程分离都是用于回收线程资源的,可以根据不同的业务场景酌情使用。不管有什么理由,你都必须选择其中一种,否则就会引发资源泄漏的问题,这个问题与内存泄漏同样可怕。

线程的属性:
前面介绍的常用数据类型中有个pthread_attr_t的属性,这个属性由一个线程属性对象来描述。线程属性对象由pthread_attr_init()接口初始化,并由pthread_attr_destory()来销毁
int pthread_attr_init(pthread_attr_t *attr);int pthread_attr_destory(pthread_attr_t *attr);
Linux下的线程有:绑定属性、分离属性、调度属性、堆栈大小属性和满占警戒区大小属性.
pthread_create()创建线程的第二个参数就是线程属性pthread_attr_t指针,若为NULL则为默认属性,若自定义属性,如设置线程为分离状态,
pthread_attr_setdetachstate(&pthread_attr_t,PTHREAD_CREATE_DETACHED),则第二个参数需要指定为&pthread_attr_t.

线程的同步:
虽然线程本地存储(暂时未介绍)可以避免线程访问共享数据,但是线程之间的大部分数据始终还是共享的。
在涉及到对共享数据进行读写操作时,就必须使用同步机制,否则就会造成线程们哄抢共享数据的结果,
这会把你的数据弄的七零八落理不清头绪。
Linux提供的线程同步机制主要有互斥锁和条件变量
互斥锁:
所谓的互斥就是线程之间互相排斥,获得资源的线程排斥其它没有获得资源的线程。Linux使用互斥锁来实现这种机制。
既然叫锁,就有加锁和解锁的概念。当线程获得了加锁的资格,那么它将独享这个锁,其它线程一旦试图去碰触这个锁就立即被系统“拍晕”。
当加锁的线程解开并放弃了这个锁之后,那些被“拍晕”的线程会被系统唤醒,然后继续去争抢这个锁。至于谁能抢到,只有天知道。
但是总有一个能抢到。于是其它来凑热闹的线程又被系统给“拍晕”了……如此反复。感觉线程的“头”很痛:)
从互斥锁的这种行为看,线程加锁和解锁之间的代码相当于一个独木桥,同意时刻只有一个线程能执行。从全局上看,在这个地方,
所有并行运行的线程都变成了排队运行了。比较专业的叫法是同步执行,这段代码区域叫临界区。同步执行就破坏了线程并行性的初衷了,
临界区越大破坏得越厉害。所以在实际应用中,应该尽量避免有临界区出现。实在不行,临界区也要尽量的小。
如果连缩小临界区都做不到,那还使用多线程干嘛?
互斥锁在Linux中的名字是mutex。这个似乎优点眼熟。对,在前面介绍NPTL的时候提起过,但是那个叫futex,是系统底层机制。
对于提供给用户使用的则是这个mutex。Linux初始化和销毁互斥锁的接口是pthread_mutex_init()和pthead_mutex_destroy(),
对于加锁和解锁则有pthread_mutex_lock()、pthread_mutex_trylock()和pthread_mutex_unlock()
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);int pthread_mutex_destory(pthread_mutex_t *mutex );int pthread_mutex_lock(pthread_mutex_t *mutex);int pthread_mutex_trylock(pthread_mutex_t *mutex);int pthread_mutex_unlock(pthread_mutex_t *mutex);
从这些定义中可以看到,互斥锁也是有属性的。只不过这个属性在绝大多数情况下都不需要改动,所以使用默认的属性就行。方法就是给它传递NULL。
pthread_mutex_t counter_lock;......//初始化pthread_mutex_init(&counter_lock, NULL); 
phtread_mutex_trylock()比较特别,用它试图加锁的线程永远都不会被系统“拍晕”,只是通过返回EBUSY来告诉程序员这个锁已经有人用了。至于是否继续“强闯”临界区,则由程序员决定。系统提供这个接口的目的可不是让线程“强闯”临界区的。它的根本目的还是为了提高并行性,留着这个线程去干点其它有意义的事情。当然,如果很幸运恰巧这个时候还没有人拥有这把锁,那么自然也会取得临界区的使用权.
需要补充一点,互斥锁在同一个线程内,没有互斥的特性。也就是说,线程不能利用互斥锁让系统将自己“拍晕”。解释这个现象的一个很好的理由就是,拥有锁的线程把自己“拍晕”了,谁还能再拥有这把锁呢?但是另外情况需要避免,就是两个线程已经各自拥有一把锁了,但是还想得到对方的锁,这个时候两个线程都会被“拍晕”。一旦这种情况发生,就谁都不能获得这个锁了,这种情况还有一个著名的名字——死锁。死锁是永远都要避免的事情,因为这是严重损人不利己的行为。

条件变量:
条件变量关键点在“变量”上。与锁的不同之处就是,当线程遇到这个“变量”,并不是类似锁那样的被系统给“拍晕”,而是根据“条件”来选择是否在那里等待。等待什么呢?等待允许通过的“信号”。这个“信号”是系统控制的吗?显然不是!它是由另外一个线程来控制的。
如果说互斥锁可以比作独木桥,那么条件变量这就好比是马路上的红绿灯。车辆遇到红绿灯肯定会根据“灯”的颜色来判断是否通行,毕竟红灯停绿灯行这个道理在幼儿园的时候老师就教了。那么谁来控制“灯”的颜色呢?一定是交警啊,至少你我都不敢动它(有人会说那是自动的,可是间隔多少时间变换也是交警设置不是?)。那么“车辆”和“交警”就是马路上的两类线程,大多数情况下都是“车”多“交警”少。
更深一步理解,条件变量是一种事件机制。由一类线程来控制“事件”的发生,另外一类线程等待“事件”的发生。为了实现这种机制,条件变量必须是共享于线程之间的全局变量。而且,条件变量也需要与互斥锁同时使用
初始化和销毁条件变量的接口是pthread_cond_init()和pthread_cond_destory();控制“事件”发生的接口是pthread_cond_signal()或pthread_cond_broadcast();等待“事件”发生的接口是pthead_cond_wait()或pthread_cond_timedwait()
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);int pthread_cond_destory(pthread_cond_t *cond);int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);int pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t *mutex, const timespec *abstime);int pthread_cond_signal(pthread_cond_t *cond);int pthread_cond_broadcast(pthread_cond_t *cond);
对于等待“事件”的接口从其名称中可以看出,一种是无限期等待,一种是限时等待。后者与互斥锁的pthread_mutex_trylock()有些类似,即当等待的“事件”经过一段时间之后依然没有发生,那就去干点别的有意义的事情去。而对于控制“事件”发生的接口则有“单播”和“广播”之说。所谓单播就是只有一个线程会得到“事件”已经发生了的“通知”,而广播就是所有线程都会得到“通知”。对于广播情况,所有被“通知”到的线程也要经过由互斥锁控制的独木桥。
#include <stdio.h>#include <pthread.h>#include <unistd.h>pthread_mutex_t counter_lock;   //互斥锁pthread_cond_t counter_nonzero; //条件变量int counter = 0;int estatus = -1;void *decrement_counter(void *argv);void *increment_counter(void *argv);//******* 主函数 *******//int main(int argc, char **argv){    printf("counter: %d/n", counter);    pthread_t thd1, thd2;    int ret;    //初始化    pthread_mutex_init(&counter_lock, NULL);    pthread_cond_init(&counter_nonzero, NULL);        ret = pthread_create(&thd1, NULL, decrement_counter, NULL); //创建线程1    if(ret){        perror("del:/n");        return 1;    }    ret = pthread_create(&thd2, NULL, increment_counter, NULL); //创建线程2    if(ret){        perror("inc: /n");        return 1;    }    int counter = 0;    while(counter != 10){        printf("counter(main): %d/n", counter); //主线程        sleep(1);        counter++;    }    pthread_exit(0);        return 0;}void *decrement_counter(void *argv){    printf("counter(decrement): %d/n", counter);    pthread_mutex_lock(&counter_lock);    while(counter == 0)        pthread_cond_wait(&counter_nonzero, &counter_lock); //进入阻塞(wait),等待激活(signal)        printf("counter--(before): %d/n", counter);        counter--; //等待signal激活后再执行    printf("counter--(after): %d/n", counter);        pthread_mutex_unlock(&counter_lock);     return &estatus;}void *increment_counter(void *argv){    printf("counter(increment): %d/n", counter);    pthread_mutex_lock(&counter_lock);    if(counter == 0)        pthread_cond_signal(&counter_nonzero); //激活(signal)阻塞(wait)的线程(先执行完signal线程,然后再执行wait线程)    printf("counter++(before): %d/n", counter);        counter++;     printf("counter++(after): %d/n", counter);        pthread_mutex_unlock(&counter_lock);    return &estatus;}
运行结果:
counter: 0
counter(main): 0
counter(decrement): 0
counter(increment): 0
counter++(before): 0
counter++(after): 1
counter--(before): 1
counter--(after): 0
counter(main): 1
counter(main): 2
counter(main): 3
counter(main): 4
counter(main): 5
counter(main): 6
counter(main): 7
counter(main): 8
counter(main): 9


从代码中会发现,等待“事件”发生的接口都需要传递一个互斥锁给它。而实际上这个互斥锁还要在调用它们之前加锁,调用之后解锁。不单如此,在调用操作“事件”发生的接口之前也要加锁,调用之后解锁。这就面临一个问题,按照这种方式,等于“发生事件”和“等待事件”是互为临界区的。也就是说,如果“事件”还没有发生,那么有线程要等待这个“事件”就会阻止“事件”的发生。更干脆一点,就是这个“生产者”和“消费者”是在来回的走独木桥.
为什么会有这样的结果呢?这就要说明一下pthread_cond_wait()接口对互斥锁做什么。
答案是:解锁.pthread_cond_wait()首先会解锁互斥锁,然后进入等待。这个时候“生产者”就能够进入临界区,然后在条件满足的时候向“消费者”发出信号。
当pthead_cond_wait()获得“通知”之后,它还要对互斥锁加锁,这样可以防止“生产者”继续工作而“撑坏”缓冲区。另外,“生产者”在缓冲区不满的情况下才能
工作的这个限定条件是很有必要的。因为在pthread_cond_wait()获得通知之后,在没有对互斥锁加锁之前,“生产者”可能已经重新进入临界区了,这样
“消费者”又被堵住了。也就是因为条件变量这种工作性质,导致它必须与互斥锁联合使用。


0 0
原创粉丝点击