多线程

进程是包含程序指令和相关资源的集合,每个进程和其他进程一起参与调度,竞争CPU、内存等系统资源。每次进程切换,都存在进程资源的保存和恢复动作,这称为上下文切换。进程的引入可以解决多用户支持的问题,但是多进程系统也在如下方面产生了新的问题:进程频繁切换引起的额外开销可能会严重影响系统性能。进程间通信要求复杂的系统级实现。

同一个进程内部的多个线程,共享的是同一个进程的所有资源。比如,与每个进程独有自己的内存空间不同,同属一个进程的多个线程共享该进程的内存空间。通过线程可以支持同一个应用程序内部的并发,免去了进程频繁切换的开销,另外并发任务间通信也更简单。

在创建一个新的线程时,需要为这个线程建一个新的栈,每个栈对应一个线程。当某个栈执行到全部弹出时,对应线程完成任务,并结束。所以,多线程的进程在内存中有多个栈,多个栈之间以一定的空白区域隔开,以备栈的增长。每个线程可调用自己栈最下方的帧中的参数和变量,并与其他线程共享内存中的Text、heap和global data区域。

1.线程的创建pthread_create

它的函数原型是:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

pthread_create函数第一个参数为指向线程标识符的指针,第二个参数用来设置线程属性,第三个参数是线程运行函数的起始地址,最后一个参数是运行函数的参数。

2.等待线程结束pthread_join

pthread_join函数用来等待一个线程的结束,其函数原型为:

int pthread_join (pthread_t thread, void **retval);

第一个参数为被等待的线程标识符,第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回

3.线程的结束pthread_exit

一个线程的结束有两种途径:①函数已经结束,调用它的线程也就结束了;②通过函数pthread_exit来实现。它的函数原型为:

void pthread_exit (void *retval);

4.pthread_join和pthread_exit的区别

(1)pthread_join一般是主线程来调用,用来等待子线程退出,因为是等待,所以是阻塞的,一般主线程会依次添加所有它创建的子线程。
(2)pthread_exit一般是子线程调用,用来结束当前线程。
(3)子线程可以通过pthread_exit传递一个返回值,而主线程通过pthread_join获得该返回值,从而判断该子线程的退出是正常还是异常。

#include <stdio.h>
#include <pthread.h>
void* say_hello(void* args){
    /*线程的运行函数,必须void*,没说的表示返回通用指针、输入通用指针*/
    printf("hello from thread\n");
    pthread_exit((void*)1);
}
int main(){
    pthread_t tid;
    int iRet = pthread_create(&tid, NULL, say_hello, NULL);
    /*参数依次是:创建的线程id,线程参数,调用函数名,传入的函数参数*/
    if (iRet){
        printf("pthread_create error: iRet=%d\n",iRet);
        return iRet;
    }
    void *retval;
    iRet=pthread_join(tid,&retval);
    if (iRet){
        printf("pthread_join error: iRet=%d\n",iRet);
        return iRet;
    }
    printf("retval=%ld\n",(long)retval);
    return 0;
}

5.线程属性

线程有一组属性是可以在线程被创建时指定的。该组属性被封装在一个对象中,该对象可用来设置一个或一组线程的属性。线程属性对象的类型为pthread_attr_t。pthread_attr_t包含在pthread.h头文件中。
线程属性结构如下所示。

typedef struct
{
    int                   etachstate;          // 线程的分离状态
    int                   schedpolicy;        // 线程调度策略
    structsched_param     schedparam;          // 线程的调度参数
    int                   inheritsched;          // 线程的继承性
    int                   scope;               // 线程的作用域
    size_t                guardsize;          // 线程栈末尾的警戒缓冲区大小
    int                   stackaddr_set;     // 线程的栈设置
    void*                 stackaddr;          // 线程栈的位置
    size_t                stacksize;          // 线程栈的大小
}pthread_attr_t;

属性值不能直接设置,必须使用相关函数进行操作,初始化的函数为pthread_attr_init,且这个函数必须在pthread_create函数之前调用,之后必须用pthread_attr_destroy函数来释放资源。线程属性主要包括如下属性:作用域(scope)、栈尺寸(stack size)、栈地址(stack address)、优先级(priority)、分离的状态(detached state)、调度策略和参数(scheduling policy and parameters)等。默认的属性为非绑定、非分离、默认1MB大小的堆栈、与父进程同样级别的优先级

(1)分离状态(detached state):

若线程终止时,线程处于分离状态,系统将不保留线程终止的状态;当不需要线程的终止状态时,可以分离线程(调用pthread_detach函数);若在线程创建的时候,就已经知道以后不需要使用线程的终止状态,可以在线程创建属性里面指定该状态,那么线程一开始就处于分离状态。通过下面两个函数,设置和获取线程的分离属性:

int pthread_attr_getdetachstate ( const pthread_attr_t *attr, int *state );
int pthread_attr_setdetachstate ( pthread_attr_t *attr, int state );

该属性的可选值有:PTHREAD_CREATE_DETACHED、PTHREAD_CREATE_JOINABLE。

(2)栈地址(stack address):

POSIX.1定义了两个常量_POSIX_THREAD_ATTR_STACKADDR和_POSIX_THREAD_ATTR_STACKSIZE以检测系统是否支持栈属性。当然也可以给sysconf函数传递_SC_THREAD_ATTR_STACKADDR或_SC_THREAD_ATTR_STACKSIZE来进行检测。当进程栈地址空间不够用时,指定新建线程使用由malloc分配的空间作为自己的栈空间。通过pthread_attr_setstackaddr和pthread_attr_getstackaddr两个函数分别设置和获取线程的栈地址。传给pthread_attr_setstackaddr函数的地址是缓冲区的低地址(不一定是栈的开始地址,栈可能从高地址往低地址增长),代码如下所示。

int pthread_attr_getstackaddr ( const pthread_attr_t *attr, void **addr );
int pthread_attr_setstackaddr ( pthread_attr_t *attr, void *addr );

(3)栈大小(stack size):

当系统中有很多线程时,可能需要减小每个线程栈的默认大小,防止进程的地址空间不够用;当线程调用的函数会分配很大的局部变量或者函数调用层次很深时,可能需要增大线程栈的默认大小。函数pthread_attr_getstacksize和pthread_attr_setstacksize被提供来解决这个问题。

int pthread_attr_getstacksize ( const pthread_attr_t *attr, size_t *size );
int pthread_attr_setstacksize ( pthread_attr_t *attr, size_t size );

函数pthread_attr_getstack和pthread_attr_setstack函数可以同时操作栈地址和栈大小两个属性,代码如下所示。

int pthread_attr_getstack ( const pthread_attr_t *attr, void **stackaddr, size_t *size );
int pthread_attr_setstack ( pthread_attr_t *attr, void *stackaddr, size_t size );

(4)栈保护区大小(stack guard size):

在线程栈顶留出一段空间,防止栈溢出。当栈指针进入这段保护区时,系统会发出错误提示,通常会发送信号给线程。该属性默认值是PAGESIZE的大小,该属性被设置时,系统会自动将该属性大小补齐为页大小的整数倍。当改变栈地址属性时,栈保护区大小通常清零。

int pthread_attr_getguardsize ( const pthread_attr_t *attr, size_t *guardsize );
int pthread_attr_setguardsize ( pthread_attr_t *attr, size_t guardsize );

(5)线程优先级(priority):

新线程的优先级为0。

int pthread_attr_getschedparam(const pthread_attr_t *restrict attr, struct sched_param *restrict param);
int pthread_attr_setschedparam(pthread_attr_t *restrict attr, const struct sched_param *restrict param);

(6)继承父进程优先级(inheritsched):

新线程不继承父线程调度优先级。

(7)调度策略(schedpolicy):

新线程使用SCHED_OTHER调度策略。线程一旦开始运行,直到被抢占或者直到线程阻塞或停止为止。

int pthread_attr_setschedpolicy(pthread_attr_t* attr, int policy)
int pthread_attr_setschedparam (pthread_attr_t* attr, struct sched_param* param);

(8)争用范围(scope):

建立线程的争用范围(PTHREAD_SCOPE_SYSTEM或PTHREAD_SCOPE_PROCESS)。使用PTHREAD_SCOPE_SYSTEM时,此线程将与系统中的所有线程进行竞争。使用PTHREAD_SCOPE_PROCESS时,此线程将与进程中的其他线程进行竞争,又称为绑定状态,PTHREAD_SCOPE_SYSTEM(绑定的)和PTHREAD_SCOPE_PROCESS(非绑定的)。具有不同范围状态的线程可以在同一个系统甚至同一个进程中共存。进程范围只允许这种线程与同一进程中的其他线程争用资源,而系统范围则允许此类线程与系统内的其他所有线程争用资源。实际上,从Solaris 9发行版开始,系统就不再区分这两个范围。

int pthread_attr_getscope(const pthread_attr_t *restrict attr, int *restrict contentionscope);
int pthread_attr_setscope(pthread_attr_t *attr, int contentionscope);

(9)线程并行级别(concurrency)

应用程序使用pthread_setconcurrency()通知系统其所需的并发级别。

int pthread_getconcurrency(void);
int pthread_setconcurrency(int new_level);

POSIX标准指定了3种调度策略:先入先出策略(SCHED_FIFO)、循环策略(SCHED_RR)和自定义策略(SCHED_OTHER)。SCHED_FIFO是基于队列的调度程序,对于每个优先级都会使用不同的队列。SCHED_RR与FIFO相似,不同的是前者的每个线程都有一个执行时间配额。

6.分离线程

(1)使用pthread_attr_setdetachstate函数

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
void * tfn1(void * arg){
    printf("the thread\n");
    return NULL;
}
int main(void){
    int iRet;
    pthread_t tid;
    pthread_attr_t attr;
    iRet = pthread_attr_init(&attr);
    if(iRet){
        printf("can't init attr %s/n", strerror(iRet));
        return iRet;
    }
    iRet = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    if(iRet){
        printf("can't set attr %s\n", strerror(iRet));
        return iRet;
    }
    iRet = pthread_create(&tid, &attr, tfn1, NULL);
    if(iRet){
        printf("can't create thread %s\n", strerror(iRet));
        return iRet;
    }
    iRet = pthread_join(tid, NULL);
    if(iRet){
        printf("thread has been detached\n");
        return iRet;
    }
    return 0;
}

我们是在线程创建之前,就将它的属性设置为分离状态,把线程的属性设置为了线程与线程结束状态分离,代码如下:

iRet = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

创建一个线程时,带上这个属性,代码如下:

iRet = pthread_create(&tid, &attr, tfn1, NULL);

由于状态分离,因此得不到线程的结束状态信息,pthread_join函数会出错。

(2)使用pthread_detach函数

可以分离一个已经创建的线程

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
void * tfn1(void * arg){
    printf("the sub thread sleeping for 5 seconds\n");
    sleep(5);/*休眠5秒,等待主线程将该线程设置为分离状态*/
    printf("the thread done\n");
    return NULL;
}
int main(void){
    int iRet;
    pthread_t tid;
    iRet = pthread_create(&tid, NULL, tfn1, NULL);
    /*创建一个线程,这个线程和结束状态是分离的 */
    if(iRet){
        printf("can't create thread %s\n", strerror(iRet));
        return iRet;
    }
    iRet = pthread_detach(tid);/*设置线程为分离状态 */
    if(iRet){
        printf("can't detach thread  %s\n", strerror(iRet));
        return iRet;
    }
    iRet = pthread_join(tid, NULL);
    /*由于状态分离,因此得不到线程的结束状态信息,此函数会出错 */
    if(iRet){
        printf("thread has been detached\n");
    }

    printf("the main thread sleeping for 8 seconds\n");
    sleep(8);
    printf("the main thread done.\n");
    return 0;
}

用pthread_detach函数将一个已创建的线程设置为分离状态,导致用pthread_join函数获取不到它的结束状态信息。

7.多线程同步

最常见的解决竞争条件的方法是将原先分离的两个指令构成不可分割的一个原子操作,而其他任务不能插入到原子操作中。
对于多线程程序来说,同步是指在一定的时间内只允许某一个线程访问某个资源。而在此时间内,不允许其他的线程访问该资源。可以通过互斥锁(mutex)、条件变量(condition variable)、读写锁(reader-writer lock)和信号量(semphore)来同步资源。

(1)互斥锁

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
pthread_mutex_t mutex_x= PTHREAD_MUTEX_INITIALIZER;
int total_ticket_num=20;
void *sell_ticket(void *arg){
    for(int i=0;i<20;i++){
        pthread_mutex_lock(&mutex_x);
        if(total_ticket_num>0){
            sleep(1);
            printf("sell the %dth ticket\n",20-total_ticket_num+1);
            total_ticket_num--;
        }
        pthread_mutex_unlock(&mutex_x);
    }
    return 0;
}
int main(){
    int iRet;
    pthread_t tids[4];
    int i=0;
    for(i=0;i<4;i++){
        int iRet = pthread_create(&tids[i], NULL, &sell_ticket, NULL);
        if(iRet){
            printf("pthread_create error, iRet=%d\n",iRet);
            return iRet;
        }
    }
    sleep(30);
    void *retval;
    for(i=0;i<4;i++){
        iRet=pthread_join(tids[i], &retval);
        if(iRet){
            printf("tid=%d join error, iRet=%d\n",tids[i],iRet);
            return iRet;
        }
        printf("retval=%ld\n",(long*)retval);
    }
    return 0;
}

需要注意的时候,如果存在某个线程依然使用原先的程序,即不尝试获得mutex_x,而直接修改total_ticket_num,互斥锁不能阻止该程序修改total_ticket_num,互斥锁就失去了保护资源的意义。所以,互斥锁机制需要程序员自己来写出完善的程序来实现互斥锁的功能。
互斥锁的使用过程中,主要有pthread_mutex_init、pthread_mutex_destory、pthread_mutex_lock和pthread_mutex_unlock这几个函数,分别完成锁的初始化、锁的销毁、上锁和释放锁操作

锁的创建有两种方式,静态和动态

pthread_mutex_t mutex_x= PTHREAD_MUTEX_INITIALIZER;

另外锁可以用pthread_mutex_init函数动态地创建,函数原型如下:

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t * attr);

对锁的操作主要包括加锁pthread_mutex_lock()、解锁pthread_mutex_unlock()和测试加锁pthread_mutex_trylock()3个,代码如下:

int pthread_mutex_lock(pthread_mutex_t *mutex)
int pthread_mutex_unlock(pthread_mutex_t *mutex)
int pthread_mutex_trylock(pthread_mutex_t *mutex)

pthread_mutex_trylock()语义与pthread_mutex_lock()类似,不同的是在锁已经被占据时返回EBUSY,而不是挂起等待。
使用pthread_mutex_trylock测试加锁。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <errno.h>
pthread_mutex_t mutex_x= PTHREAD_MUTEX_INITIALIZER;
int total_ticket_num=20;
void *sell_ticket1(void *arg){
    for(int i=0;i<20;i++){
        pthread_mutex_lock(&mutex_x);
        if(total_ticket_num>0){
            printf("thread1 sell the %dth ticket\n",20-total_ticket_num+1);
            total_ticket_num--;
        }
        sleep(1);
        pthread_mutex_unlock(&mutex_x);
        sleep(1);
    }
    return 0;
}
void *sell_ticket2(void *arg){
    int iRet=0;
    for(int i=0;i<10;i++){
        iRet=pthread_mutex_trylock(&mutex_x);
        if(iRet==EBUSY){
            printf ("sell_ticket2:the variable is locked by sell_ticket1.\n");
        }else if(iRet==0){
            if(total_ticket_num>0){
                printf("thread2 sell the %dth ticket\n",20-total_ticket_num+1);
                total_ticket_num--;
            }
            pthread_mutex_unlock(&mutex_x);
        }
        sleep(1);
    }
    return 0;
}
int main(){
    pthread_t tids[2];
    int iRet = pthread_create(&tids[0], NULL, &sell_ticket1, NULL);
    if(iRet){
        printf("pthread_create error, iRet=%d\n",iRet);
        return iRet;
    }
    iRet = pthread_create(&tids[1], NULL, &sell_ticket2, NULL);
    if(iRet){
        printf("pthread_create error, iRet=%d\n",iRet);
        return iRet;
    }
    sleep(30);
    void *retval;
    iRet=pthread_join(tids[0], &retval);
    if(iRet){
        printf("tid=%d join error, iRet=%d\n",tids[0],iRet);
    }else{
        printf("retval=%ld\n",(long*)retval);
    }
    iRet=pthread_join(tids[1], &retval);
    if(iRet){
        printf("tid=%d join error, iRet=%d\n",tids[1],iRet);
    }else{
        printf("retval=%ld\n",(long*)retval);
    }
    return 0;
}

(2)条件变量

如果线程正在等待共享数据内某个条件出现,那会发生什么呢?它可能重复对互斥对象锁定和解锁,每次都会检查共享数据结构,以查找某个值。但这是在浪费时间和资源,而且这种繁忙查询的效率非常低。

真正需要的是这样一种方法:当线程在等待满足某些条件时使线程进入睡眠状态,一旦条件满足,就唤醒因等待满足特定条件而睡眠的线程。如果能够做到这一点,线程代码将是非常高效的,并且不会占用宝贵的互斥对象锁。而这正是条件变量能做的事!
条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其他的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程,这些线程将重新锁定互斥锁并重新测试条件是否满足。

a.创建:条件变量和互斥锁一样,都有静态、动态两种创建方式。

静态方式使用PTHREAD_COND_INITIALIZER常量,函数原型是:

pthread_cond_t cond=PTHREAD_COND_INITIALIZER

动态方式则使用pthread_cond_init函数,pthread_cond_init的函数原型是:

int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr)

b.注销:注销一个条件变量需要调用pthread_cond_destroy()

它的函数原型是:

int pthread_cond_destroy(pthread_cond_t *cond)

c.等待:

等待条件有两种方式——条件等待pthread_cond_wait()和计时等待pthread_cond_timedwait()。其中计时等待方式如果在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待。

无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()(或pthread_cond_timedwait())的竞争条件。

且在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前,mutex需保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。

pthread_cond_wait()和pthread_cond_timedwait()的函数原型分别是:

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 struct timespec *abstime)

d.激发:

激发条件有两种形式:pthread_cond_signal()激活一个等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个;而pthread_cond_broadcast()则激活所有等待线程。

#include <iostream>
#include <pthread.h>
using namespace std;
pthread_cond_t qready = PTHREAD_COND_INITIALIZER;    /*初始构造条件变量*/
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;    /*初始构造锁*/
int x = 10;
int y = 20;
void *func1(void *arg){
    cout<<"func1 start"<<endl;
    pthread_mutex_lock(&qlock);
    while(x<y)
    {
        pthread_cond_wait(&qready,&qlock);
    }
    pthread_mutex_unlock(&qlock);
    sleep(3);
    cout<<"func1 end"<<endl;
}
void *func2(void *arg){
    cout<<"func2 start"<<endl;
    pthread_mutex_lock(&qlock);
    x = 20;
    y = 10;
    cout<<"has change x and y"<<endl;
    pthread_mutex_unlock(&qlock);
    if(x > y){
        pthread_cond_signal(&qready);
    }
    cout<<"func2 end"<<endl;
}
int main(int argc,char **argv){
    pthread_t tid1,tid2;
    int iRet;
    iRet = pthread_create(&tid1,NULL,func1,NULL);
    if(iRet){
        cout<<"pthread 1 create error"<<endl;
        return iRet;
    }
    sleep(2);
    iRet = pthread_create(&tid2,NULL,func2,NULL);
    if(iRet){
        cout<<"pthread 2 create error"<<endl;
        return iRet;
    }
    sleep(5);
    return 0;
}

(3)读写锁

读写锁比起互斥锁具有更高的适用性与并行性,可以有多个线程同时占用读模式的读写锁,但是只能有一个线程占用写模式的读写锁,读写锁的3种状态如下所述。
a.当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。
b.当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是以写模式对它进行加锁的线程将会被阻塞。
c.当读写锁在读模式的锁状态时,如果有另外的线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁的请求,这样可以避免读模式锁长期占用,而等待的写模式锁请求则长期阻塞。
读写锁最适用于对数据结构的读操作次数多于写操作次数的场合,因为读模式锁定时可以共享,而写模式锁定时只能由某个线程独占资源,因而读写锁也可以叫作共享-独占锁。

1)初始化和销毁读写锁

对于读写锁变量的初始化可以有两种方式,一种是通过给一个静态分配的读写锁赋予常值PTHREAD_RWLOCK_INITIALIZER来初始化它;另一种方法就是通过调用pthread_rwlock_init()来动态地初始化。而当某个线程不再需要读写锁的时候,可以通过调用pthread_rwlock_destroy来销毁该锁。函数原型如下:

int pthread_rwlock_init(pthread_rwlock_t *rwptr, const pthread_rwlockattr_t *attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwptr);

这两个函数如果执行成功均返回0,如果出错则返回错误码。
在释放某个读写锁占用的内存之前,要先通过pthread_rwlock_destroy对读写锁进行清理,释放由pthread_rwlock_init所分配的资源。
在初始化某个读写锁的时候,如果属性指针attr是个空指针的话,表示使用默认属性;如果想要使用非默认属性,则要使用到下面的两个函数:

int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy(pthread_rwlockatttr_t *attr);

同样的,这两个函数如果执行成功则返回0,失败则返回错误码。

2)获取和释放读写锁
读写锁的数据类型是pthread_rwlock_t,如果这个数据类型中的某个变量是静态分配的,那么可以通过给它赋予常值PTHREAD_RWLOCK_INITIALIZAR来初始化它。pthread_rwlock_rdlock()用来获取读出锁,如果相应的读出锁已经被某个写入者占有,那么就阻塞调用线程。
pthread_rwlock_wrlock()以获取一个写入锁,如果相应的写入锁已经被其他写入者或者读出者占有(一个或多个),那么就阻塞该调用线程;pthread_rwlock_unlock()用来释放一个读出或者写入锁。函数原型如下:

int pthread_rwlock_rdlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_unlock(pthread_rwlock_t *rwptr);

这3个函数若调用成功则返回0,失败则返回错误码。要注意的是,其中获取锁的两个函数的操作都是阻塞操作,也就是说获取不到锁的话,那么调用线程不是立即返回,而是阻塞执行。

有写情况下,这种阻塞式的获取锁的方式可能不是很适用,所以,接下来引入两个采用非阻塞方式获取读写锁的函数pthread_rwlock_tryrdlock()和pthread_rwlock_trywrlock(),非阻塞方式下获取锁的时候,如果不能马上获取到,就会立即返回一个EBUSY错误提示,而不是把调用线程投入到睡眠等待。函数原型如下:

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwptr);

读写锁的使用。

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#define THREADNUM 5
pthread_rwlock_t rwlock;
void *readers(void *arg){
    pthread_rwlock_rdlock(&rwlock);
    printf("reader %ld got the lock\n", (long)arg);
    pthread_rwlock_unlock(&rwlock);
    pthread_exit((void*)0);
}
void *writers(void *arg){
    pthread_rwlock_wrlock(&rwlock);
    printf("writer %ld got the lock\n", (long)arg);
    pthread_rwlock_unlock(&rwlock);
    pthread_exit((void*)0);
}
int main(int argc, char **argv){
    int iRet, i;
    pthread_t writer_id, reader_id;
    pthread_attr_t attr;
    int nreadercount = 1, nwritercount = 1;
    iRet = pthread_rwlock_init(&rwlock, NULL);
    if (iRet) {
        fprintf(stderr, "init lock failed\n");
        return iRet;
    }
    pthread_attr_init(&attr);
    /*pthread_attr_setdetachstate用来设置线程的分离状态,也就是说一个线程怎么样终止自己,
      状态设置为PTHREAD_CREATE_DETACHED,表示以分离状态启动线程*/
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    for (i = 0; i < THREADNUM; i++){
        if (i % 3) {
            pthread_create(&reader_id, &attr, readers, (void *)nreadercount);
            printf("create reader %d\n", nreadercount++);
        } else {
            pthread_create(&writer_id, &attr, writers, (void *)nwritercount);
            printf("create writer %d\n", nwritercount++);
        }
    }
    sleep(5); /*sleep是为了等待另外的线程的执行*/
    return 0;
}

执行结果展示,当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞;当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是以写模式对它进行加锁的线程将会被阻塞。

(4)信号量

线程还可以通过信号量来实现通信。信号量和互斥锁的区别:互斥锁只允许一个线程进入临界区,而信号量允许多个线程同时进入临界区。

a.sem_init函数

该函数用于创建信号量,其原型如下:

int sem_init(sem_t *sem, int pshared, unsigned int value);

该函数用于初始化由sem指向的信号对象,设置它的共享选项,并给它一个初始的整数值。pshared控制信号量的类型,如果其值为0,就表示这个信号量是当前进程的局部信号量,否则信号量就可以在多个进程之间共享。value为sem的初始值。调用成功时返回0,失败返回-1。

b.sem_wait函数

该函数用于以原子操作的方式将信号量的值减1。原子操作就是,如果两个线程企图同时给一个信号量加1或减1,它们之间不会互相干扰,函数的原型如下:

int sem_wait(sem_t *sem);

sem指向的对象是由sem_init调用初始化的信号量。调用成功时返回0,失败返回-1。

c.sem_post函数

该函数用于以原子操作的方式将信号量的值加1,函数的原型如下:

int sem_post(sem_t *sem);

与sem_wait一样,sem指向的对象是由sem_init调用初始化的信号量。调用成功时返回0,失败返回-1。

d.sem_destroy函数

该函数用于对用完的信号量进行清理,函数的原型如下:

int sem_destroy(sem_t *sem);

成功时返回0,失败时返回-1。

用信号量模拟窗口服务系统。
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#define CUSTOMER_NUM 10
/* @Scene: 某行业营业厅同时只能服务两个顾客。
 * 有多个顾客到来,顾客如果发现服务窗口已满,就等待,
 * 如果有可用的服务窗口,就接受服务。 */
/* 将信号量定义为全局变量,方便多个线程共享 */
sem_t sem;
/* 每个线程要运行的例程 */
void * get_service(void *thread_id){
/* 注意:立即保存thread_id的值,因为thread_id是对主线程中循环变量i的引用,它可能马上被修改*/
    int customer_id = *((int *)thread_id);
    if(sem_wait(&sem) == 0) {
        usleep(100); /* service time: 100ms */
        printf("customer %d receive service ...\n", customer_id);
        sem_post(&sem);
    }
}
int main(int argc, char *argv[]){
    /*初始化信号量,初始值为2,表示有两个顾客可以同时接受服务 */
    sem_init(&sem,0,2);
    /*为每个顾客定义一个线程id*/
    pthread_t customers[CUSTOMER_NUM];
    int i, iRet;
    /* 为每个顾客生成一个线程 */
    for(i = 0; i < CUSTOMER_NUM; i++){
        int customer_id = i;
        iRet = pthread_create(&customers[i], NULL, get_service, &customer_id);
        if(iRet){
            perror("pthread_create");
            return iRet;
        }
        else{
            printf("Customer %d arrived.\n", i);
        }
        usleep(10);
    }
    /* 等待所有顾客的线程结束 */
    /* 注意:这地方不能再用i做循环变量,因为可能线程正在访问i的值 */
    int j;
    for(j = 0; j < CUSTOMER_NUM; j++) {
        pthread_join(customers[j], NULL);
    }
    /*销毁信号量*/
    sem_destroy(&sem);
    return 0;
}

8.多线程重入

所谓“可重入函数”,是指可以由多于一个任务并发使用,而不必担心数据错误的函数。相反,“不可重入函数”则是只能由一个任务所占用,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。可重入函数可以在任意时刻被中断,稍后再继续运行,且不会丢失数据。可重入函数要在使用本地变量或在使用全局变量时保护自己的数据。

(1)可重入函数有以下特点。

1)不为连续的调用持有静态数据。
2)不返回指向静态数据的指针。
3)所有数据都由函数的调用者提供。
4)使用本地数据,或者通过制作全局数据的本地副本来保护全局数据。
5)如果必须访问全局变量,要利用互斥锁、信号量等来保护全局变量。
6)绝不调用任何不可重入函数。

(2)不可重入函数有以下特点。

1)函数中使用了静态变量,无论是全局静态变量还是局部静态变量。
2)函数返回静态变量。
3)函数中调用了不可重入函数。
4)函数体内使用了静态的数据结构。
5)函数体内调用了malloc()或者free()函数。
6)函数体内调用了其他标准I/O函数。

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值