UNIX环境高级编程 第十一章 线程

本文介绍了线程的基础概念,包括线程的创建、管理和终止,以及线程间的同步机制,如互斥锁、读写锁、条件变量和屏障等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

fork是昂贵的(把父进程的内存映像复制到子进程),并且需要进程间通信(IPC)机制。
线程的创建速度快(10-100 倍),同一进程中的线程共享相同的全局内存,线程之间容易共享信息,某些线程在阻塞的时候还有另外一些线程可以运行,所以多线程程序在单处理器上运行还是可以改善响应时间和吞吐量。但是,这就带来了同步的问题。
(1)同一进程内的所有线程除了共享全局变量,还共享:
进程指令,大多数数据,打开的文件(即文件描述符),信号处理函数和信号处置,当前工作目录,用户ID和组ID,”这些很容易出错误”
(2)不过每个线程有各自的:
线程ID,优先级,栈(用于存放局部变量和返回地址),寄存器集合(包括程序计数器和栈指针),errno,信号掩码。
主:父子进程除了描述符外不共享其余的任何东西。

(一)POSIX线程pthread:

所有可移植的操作系统实现不能把它当做整数处理。因此必须使用一个函数对两个线程ID进程比较,使用结构表示pthread_t数据类型的后果是不能使用一种可移植的方式打印该数据类型的值。
restrict限定的指针,不会出现多个指针指向同一块内存的情况,跟register关键字类似,这个也是提供给编译器优化的,因为保证只有一个指针会指向这块内存,编译器能更高效的进行一些处理而不用担心影响到别的指针。

#include <pthread.h>
int pthread_equal(pthread_t tid1,pthread_t tid2);

1. 线程创建初始化:

pthread_create()类似fork()

int pthread_create(pthread_t *restrict tid, const pthread_attr_t *restrict attr,
void* (*func) (void*), void *restrict arg);//成功返回0,出错则为整的Exxx值

注:每个线程都有许多属性(attribute):线程ID,优先级,初始栈大小,是否应该成为一个守护线程,等,通常情况下我们采纳默认设置,把attr参数指定为NULL。func是该线程执行的函数,arg是该函数的参数,如果是多个参数,就需要打包成一个结构。tid所指向的内存单元存储了新线程的线程ID;

线程创建时并不能保证哪个线程会先运行:是新创建的线程,还是调用线程。新创建的线程可以访问进程的地址空间,并且继承了调用线程的浮点环境和信号屏蔽字,但是该线程的挂起信号集会被清除。

2. 等待线程结束:

pthread_join()类似waitpid(可以不指定id,令ID=-1,来等待任意id的进程)

int pthread_join(pthread_t *tid, void **status);//成功返回0,出错则为整的Exxx值,必须指定tid

通过调用pthread_join等待一个给定的线程终止。调用线程将一直阻塞,直到指定的线程调用pthread_exit、从启动例程返回或者被取消。如果线程简单地从启动例程中返回,rval_ptr就包含返回码。如果线程被(其他线程)取消,status指定的内存单元被设置为PTHREAD_CANCELED。

如果对线程的返回值并不感兴趣,那么可以把rval_ptr设置为NULL。这种情况下,调用pthread_join函数可以等待指定线程终止,但并不获取线程的终止状态。

3. 获取自身ID:pthread_self类似getpid

pthread_t pthread_self();//返回调用线程的线程ID

当线程需要识别以线程ID作为标识的数据结构时,pthread_self函数可以与pthread_equal函数一起使用。

4. 脱离线程:pthread_detach

int pthread_detach(pthread tid);//成功返回0,出错则为整的Exxx值
pthread_detach(pthread_self());//让自己脱离

线程的终止状态会保存直到对该线程调用pthread_join。如果线程已经被分离,线程的底层存储资源可以在线程终止时立即被收回。在线程被分离后,我们不能调用pthread_join 函数等待它的终止状态,因为对分离状态的线程调用pthread_join会产生未定义行为。因此如果一个线程需要知道另一个线程什么时候终止,那就最好保持第二个线程的可汇合(joinable)状态。

注:与pthread_join相反,当一个joinable(可汇合)线程终止时,它的线程ID和退出状态将留存到另一个线程对它调用pthread_join。
而detached线程却像守护进程,当它们终止时,所有相关资源都被释放,我们不能等待它们终止。

5. pthread_exit函数:让一个线程终止

void pthread_exit(void *status)
//status参数是一个无类型指针,与传递给启动例程的单个参数类似。进程中的其他线程也可以通过调用pthread_join函数访问到这个指针。

对于可汇合的线程,它的线程ID和退出状态将留存到另一个线程对它调用pthread_join(),

另外两个终止线程的方法;
(1)启动线程的函数(creat的第三个参数)返回
(2)进程的main函数返回,或者线程调用了exit

注:函数不能返回栈上变量的地址。自动变量内存地址中的值在函数退出后是不确定的。尤其在上面这些返回指针的地方。要不然返回只读数据区中的常量,要不然就是malloc堆中的动态变量,要不然就是全局变量。

  1. pthread_cancel函数来请求取消同一进程中的其他线程

include

int pthread_cancel(pthread_t tid);

线程可以忽略取消或者控制如何被取消(在后面12章讨论)。

注意:pthread_cancel并不等待线程终止,它仅仅提出请求。

线程可以安排它退出时需要调用的函数,这与进程在退出时可以调用atexit函数安排退出时类似的。这样的函数称为线程清理处理程序(thread cleanup handler)。

一个线程可以建立多个清理处理程序。处理程序注册在栈中,也就是说,它们执行的顺序与它们注册的顺序相反。

7.

#include <pthread.h>
void pthread_cleanup_push(void (*rtn)(void *), void *arg);
void pthread_cleanup_pop(int execute);

当线程执行以下动作时,清理函数rtn是由pthread_cleanip_push函数调度的,调用时只有一个参数arg:
(1)调用pthread_exit时;
(2)响应取消请求时;
(3)用非零execute参数调用pthread_cleanup_pop时。
(4)注意!!线程从启动例程返回(正常退出)并不会调用注册的清理程序。

如果execute参数设置为0,清理函数将不被调用。不管发生上述哪种情况,pthread_cleanup_pop都将删除上次pthread_cleanip_push调用建立的清理处理程序。

这些函数有一个限制,由于它们可以实现为宏,所以必须在线程相同的作用域中以匹配对的形式使用。因为pthread_cleanip_push包含了’{‘字符,pthread_cleanup_pop包含了’}’字符.

一个多线程的简单例子:

int main(void)
{
    pthread_t tid1,tid2;
    void *tret;

    pthread_create(&tid1,NULL,fun1,1);
    pthread_create(&tid2,NULL,fun2,1);

    pthread_join(tid1,&tret);
    printf("thread1 return %d. \n",tret);
    pthread_join(tid2,&tret);
    printf("thread2 return %d \n",tret);

    return 0;
}
void *fun1(void *arg)
{
    printf("thread1 printed the message.\n");

    pthread_cleanup_push(cleanup,"thread 1 first handler");
    pthread_cleanup_push(cleanup,"thread 1 second handler");

    if(arg)
        return((void *)1);
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);

    return((void *)1);
}
void cleanup(void *arg)
{
    printf("%s. \n",arg);
}

线程函数和进程函数的相似之处:
- fork - pthread_create - 创建新的控制流
- exit - pthread_exit - 从现有的控制流中退出
- waitpid - pthread_join -从控制流中得到退出状态
- atexit - pthread_cleanup_push - 注册在退出控制流时调用的函数
- getpid - pthread_self - 获得控制流ID
- abort - pthread_calcel - 请求控制流的非正常退出

(二)线程同步

父子进程除了描述符外不共享其余的任何东西。
多个线程更改一个共享变量,当一个线程修改变量时,其他线程在读取或修改这个变量时可能会看到一个不一致的值。解决方法是使用一个互斥锁,保护这个共享变量,访问该变量的条件是持有互斥锁。

1. 互斥量:mutex

(1)开销并不大,可以使用pthread的互斥接口来保护数据,确保同一时间只有一个线程访问数据。互斥量(mutex)从本质上说是一把锁,在访问共享资源前对互斥量进行设置(加锁),在访问完成后释放(解锁)互斥量。对互斥量进行加锁以后任何其他试图再次对互斥量加锁的线程都会被阻塞直到当前线程释放该互斥锁。

(2)如果释放互斥量时有一个以上的线程阻塞,那么所有该锁上的阻塞线程都会变成可运行状态,第一个变为可运行的线程就可以对互斥量加锁,其他线程就会看到互斥量依然是锁着的,只能回去再次等待它重新变为可用。在这种方式下,每次只有一个线程可以向前执行。

(3)互斥变量是用pthread_mutex_t数据类型表示的。在使用互斥变量以前,必须首先对它进行初始化,可以把它设置为常量PTHREAD_MUTEX_INITIALIZER(只适用于静态分配的互斥量),也可以通过调用pthread_mutex_init函数进行初始化。如果动态分配互斥量(例如,通过调用malloc函数),在释放内存前需要调用 pthread_mutex_destroy。

#include<pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);    //默认属性为NULL
int pthread_mutex_dstroy(pthread_mutex_t *mutex);

(4)对互斥量进行加锁,需要调用pthread_mutex_lock。如果互斥量已经上锁,调用线程将阻塞直到互斥量被解锁。对互斥量解锁需要调用pthread_mutex_unlock。

int pthread_mutex_lock(pthread_mutex_t *mptr);//上锁
int pthread_mutex_unlock(pthread_mutex_t *mptr);//解锁

(5)如果线程不希望被阻塞,它可以使用pthread_mutex_trylock尝试对互斥量进行加锁。如果调用pthread_mutex_trylock时互斥量处于未锁住状态,那么pthread_mutex_trylock将锁住互斥量,不会出现阻塞直接返回0,否则就会失败,不能锁住互斥量,返回EBUSY。

int pthread_mutex_trylock(pthread_mutex_t *mutex);
//成功返回0,失败返回正值Exxx值

在每个子线程内:

pthread_mutex_t lock;  
pthread_mutex_lock(&lock);  
i++;  
Pthread_mutex_unlock(&lock); 

2.

(1)避免死锁:
如果线程试图对同一个互斥量加锁两次,那么它自身就会陷入死锁状态,但是使用互斥量时,还有其他不太明显的方式也能产生死锁。例如程序中使用一个以上的互斥量时,如果允许一个线程一直占有第一个互斥量,并且在试图锁住第二个互斥量时处于阻塞状态,但是拥有第二个互斥量的线程也在试图锁住第一个互斥量。因为两个线程都在相互请求另一个线程拥有的资源,所以这两个线程都无法向前运行,于是就产生死锁。

(2)函数pthread_mutex_timedlock
当线程试图获取一个已经加锁的互斥量时,pthread_mutex_timedlock互斥量原语允许绑定线程阻塞时间。pthread_mutex_timedlock函数与pthread_mutex_lock函数基本上是等价的,但是在达到超时时间值时,pthread_mutex_timedlock不会对互斥量进行加锁,而是返回错误码ETIMEDOUT。

#include <pthread.h>
#include <time.h>
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,const struct timespec *restrict tsptr);

超时指定愿意等待的绝对时间(注意是绝对时间)。这个超时时间是用timespec结构来表示的,它用秒和纳秒来描述时间。

3.读写锁:

也叫共享互斥锁(shared-exclisive lock)。
读写锁(read-write-lock)与互斥量类似,不过读写锁允许更高的并行性。互斥量要么是锁住状态,要么就是不加锁状态,而且一次只有一个线程可以对其加锁。

读写锁有3种状态:读模式下加锁状态,写模式下加锁状态,不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。

1.读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。
2.读写锁是读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是任何希望以写模式对此锁进行加锁的线程都会阻塞,直到所有线程释放它们的读锁为止。
3.当读写锁处于读模式锁住状态时,有一个线程试图以写模式获取锁时,读写锁通常会阻塞随后的读模式锁请求。这样可以避免读模式锁长期占有,而等待的写模式锁请求一直得不到满足。

读写锁非常适合于对数据结构读的次数远大于写的情况!!

(3.1)当读写锁是读模式锁住时

就可以说成是共享模式锁住的。当它是写模式锁住时就可以说成是互斥模式锁住的。与互斥量相比,读写锁在使用之前必须初始化,在释放它们的底层内存之前必须销毁。PTHREAD_RWLOCK_INITIALIZER常量。如果默认属性就足够的话,可以用它对静态分配的读写锁进行初始化。

#include<pthread.h>//成功返回0,失败返回正值Exxx值
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);    
//默认属性为NULL
int pthread_rwlock_dstroy(pthread_rwlock_t *rwlock);

(3.2)各种系统实现可能会对共享模式的读写锁有次数限制,所以要对pthread_rwlock_rdlock的返回值进行检查。

#include<pthread.h>//成功返回0,失败返回正值Exxx值
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
//读模式锁定
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
//写模式锁定
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
//解锁

(3.3)可以获取锁时,这两个函数返回0,否则他们返回错误EBUSY。

#include<pthread.h>//成功返回0,失败返回正值Exxx值
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

(3.4)带有超时的读写锁

与互斥量一样,SUS提供了带有超时的读写锁加锁函数,使得应用程序在获取读写锁时避免陷入永久阻塞状态。

#include <pthread.h>
#include <time.h>
//与pthreas_mutex_timedlock函数类似,超时时间是绝对时间,而不是相对时间。
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock,const struct timespec *restrict tsptr);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock,const struct timespec *restrict tsptr);

(三)条件变量

互斥锁用于防止多个线程同时访问某个变量,但我们还需要在等待某个条件(事件)发生期间能让我们进入睡眠的机制。如果没有这个机制,线程在等待一个条件发生期间只能轮询,这显然非常浪费CPU资源。

条件变量是线程可用的另一种同步机制。条件变量给多个线程提供了一个会合的场所。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。

条件变量本身是由互斥量保护的。线程在改变条件状态之前必须首先锁住互斥量。其他线程在获得互斥量之前不会察觉到这种改变,因为互斥量必须在锁定以后才能计算条件。

1. 在使用条件变量之前,必须先对它进行初始化。

条件变量的类型是pthread_cond_t,可以用两种方式进行初始化,可以把常量PTHREAD_COND_INITIALIZER赋给静态分配的条件变量,但是如果条件变量是动态分配的,则需要使用pthread_cond_init函数对它进行初始化。在释放条件变量底层的内存空间之前,可以使用pthread_cond_destroy函数对条件变量进行反初始化。

#include <pthread.h>
#include <time.h>
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_cond_t *restrict attr);//属性可设为NULL
int pthread_cond_destroy(pthread_cond_t *cond);

2. 我们使用pthread_cond_wait等待条件变量变为真。如果在给定的时间内条件不能满足,那么会生成一个返回错误码的变量,API如下:

/*等待一个条件变量,线程进入睡眠状态*/  
int pthread_cond_wait(pthread_cond_t *cptr, pthread_mutex_t *mptr);  
int pthread_cond_timedwait(pthread_cond_t *cptr, pthread_mutex_t *mptr,  
const struct timespec *abstime;//时刻而不是时间 

传递给pthread_cond_wait的互斥量对条件进行保护。全局变量总是需要互斥锁保护,因此互斥锁和条件变量经常一起使用。这也解释了为什么pthread_cond_wait函数的两个参数一个是条件变量一个是互斥锁。

3. 另外:

/*唤醒等待在条件变量上的一个线程*/  
int pthread_cond_signal(pthread_cond_t *cptr);  
/*唤醒等待在条件变量上的所有线程*/  
int pthread_cond_broadcast(pthread_cond_t *cptr);  

4. 举个例子

我们使用一个全局变量flag标志一个事件是否发生。线程A测试flag如果为0,表明事件未发生则睡眠等待。线程B产生这个事件然后将flag标志置1,唤醒线程A。为此我们定义了以下三个变量:

int flag;  //计数器
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;  //互斥锁
pthread_cond_t  cond = PTHREAD_COND_INITIALIZER;  //条件变量

线程A使用如下的代码等待事件发生:
pthread_mutex_lock(&mutex);  
while (flag == 0)  
    pthread_cond_wait(&cond, &mutex); /*睡眠等待事件发生*///解锁,cond调用signal之后加锁
    /*下一步的动作*/  
pthread_mutex_unlock(&mutex); 

线程B使用如下的代码产生事件并唤醒线程A:
/*某个事件发生*/  
pthread_mutex_lock(&mutex);  
flag = 1;  
pthread_cond_signal(&cond); /*唤醒线程A*/  
pthread_mutex_unlock(&mutex);  

5.!!!以下很重要!!!

解释1:
(1)调用者把锁住的互斥量(先外部上锁)传递给函数
(2)函数然后自动把调用线程放到等待条件的线程表上,对互斥量解锁(再内部解锁)。这就关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道(在条件检查和等待条件时不会错过信号,因为互斥量加锁了,所以信号发布出来),这样线程就不会错过条件的任何变化。

(3)解锁之后函数就处于等待信号的状态,当信号到达时,pthread_cond_wait函数返回,互斥量再次被锁住(内部加锁),
(4)之后留给外部解锁。

解释2:
(1)由于测试条件之前总是先加锁,
(2)所以当条件不成立时pthread_cond_wait函数必须先解锁,然后把调用线程投入睡眠。
(3)当线程被唤醒时,它又再次加锁,
(4)然后返回,留给外部解锁。

解释3:
(1)在调用 pthread_cond_wait 之前,应用程序必须加锁互斥量
(2)pthread_cond_wait 自动解锁互斥量(如同执行了 pthread_unlock_mutex),并等待条件变量触发。这时线程挂起,不占用CPU时间,直到条件变量被触发。
(3)pthread_cond_wait 函数返回前,自动重新对互斥量加锁(如同执行了 pthread_lock_mutex)。
(4)解锁留给线程了。

#include <pthread>
struct msg{
    struct msg *m_next;
};

struct msg *workg;
pthread_cont_t qready = PTHREAD_COND_INITIALIZER;
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;

void process_msg(void)
{
    struct msg *mp;
    for(;;)
    {
        pthread_mutex_lock(&qlock);//锁死,外部上锁
        while(workq == NULL)
        //线程在此阻塞,将线程保存到线程表后解锁,把调用线程投入睡眠,开始等待条件变量;。
            pthread_cond_wait(&qready,&qlock);//传递给函数        
        //函数收到信号后返回,并加锁,此时workq已经不是NULL,所以继续向下执行。
        mp = workq;
        workq = mp->m_next;
        pthread_mutex_unlock(&qlock);//这里对互斥锁解锁,表明同步完成。
    }
}
void enqueue_msg(struct msg *mp)
{
    pthread_mutex_lock(&qlock);//
    mp->m_next = workq;
    workq = mp;
    pthread_mutex_unlock(&qlock);
    pthread_cond_signal(&qready);//此处发送信号。
}

6.自旋锁

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

自旋锁用在非抢占式内核中是非常有用的:除了提供互斥机制以外,它们会阻塞中断,这样中断处理程序就不会让系统陷入死锁状态,因为它需要获取已被加锁的自旋锁(把中断想成另一种抢占)。在这种类型的内核中,中断处理程序不能休眠(因为非抢占式内核不能休眠,因此不能使用互斥锁、读写锁、条件变量。),因此它们能用的同步原语只能是自旋锁。

很多互斥量的实现非常高效,以至于应用程序采用互斥锁的性能与曾经采用自旋锁的性能基本上是相同的。事实上,有些互斥量的实现在试图获取互斥量的时候会自旋一小段时间,只有在自旋计数达到某一阈值时才会休眠。这些因素再加上现代CPU的进步,使得上下文切换越来越快,也使得自旋锁在某些特定的情况下才有用。

自旋锁的接口与互斥量的接口类似,这使得它可以比较容易的从一个替换为另一个:

#include<pthread.h>
int pthread_spin_init(pthread_spin_t *lock,int pshared); 
int pthread_spin_dstroy(pthread_spin_t *lock);

pshared参数表示进程共享属性,表明自旋是如何获取的。只有在支持线程进程共享同步选项的平台上才有效。

(1)设为PTHREAD_PROCESS_SHARED,则自旋锁能被底层内存的所有线程获取,即使那些线程属于不同的进程。
(2)设为PTHREAD_PROCESS_private,则自旋锁就只能被初始化该锁的进程内部的线程所访问。

#include<pthread.h>
int pthread_spin_lock(pthread_spin_t *lock);
int pthread_spin_trylock(pthread_spin_t *lock);
int pthread_spin_unlock(pthread_spin_t *lock);

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

7.屏障

屏障(barrier)是用户协调多个线程并行工作的同步机制。屏障允许每个线程等待,直到所有的合作线程都达到某一点,然后从该点继续执行。我们已经看到一种屏障pthread_join函数就是一种屏障,允许一个线程等待,直到另一个线程退出。但屏障的概念更广,它允许任意数量的线程等待,直到所有的线程完成处理工作,而且线程不需要退出,所有线程达到屏障后可以接着工作。

使用pthread_barrier_init函数对屏障进行初始化,用pthread_barrier_destroy函数进行反初始化:

#include<pthread.h>
int pthread_barrier_init(pthread_barrier_t *restrict barrier,
const pthread_barrierattr_t *restrict attr,unsigned int count); 
int pthread_barrier_dstroy(pthread_barrier_t *barrier);

const参数指定,在允许所有线程继续运行之前,必须达到屏障的线程数目。attr参数指定屏障对象的属性
可以使用pthread_barrier_wait 函数来表明,线程已经完成工作,准备等待所有其他线程赶上来。
`c
int pthread_barrier_wait(pthread_barrier_t *barrier);

调用pthread_barrier_wait的线程在屏障计数未满足条件时,会进入休眠状态。如果该线程是最后一个调用pthread_barrier_wait的线程,就满足了屏障计数,所有的线程都被唤醒。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值