muduo库学习之适用场合和常用编程模型02——线程

原文链接:https://blog.csdn.net/zhoucheng05_13/article/details/97612135

1 线程创建

创建一个线程,并在主线程和子线程中打印进程id、线程id。

实例代码

#include "apue.h"
#include<pthread.h>

pthread_t tdno;

//打印进程id、线程id
void printids(const char* s){
    
    
    pid_t pid;
    pthread_t tid;

    pid = getpid();
    tid = pthread_self();
    printf("%s pid:%lu tid:%lu (0x%lx)\n",s,(unsigned long)pid,(unsigned long)tid,(unsigned long)tid);
}


void *thr_fn(void *arg){
    
    
    printids("new thread: ");
    return ((void*)0);
}


int main(void){
    
    
    int err;

    //之所以不直接使用printids,是由于pthread_create对参数的要求
    err = pthread_create(&tdno,NULL,thr_fn,NULL);
    if(err != 0)
        exit(-1);
    printids("main thread: ");
    sleep(1);
    exit(0);
}
123456789101112131415161718192021222324252627282930313233

测试结果

[ ~/unix_practice]$ ./threadCreate 
main thread:  pid:13548 tid:139868882683680 (0x7f35c3114720)
new thread:  pid:13548 tid:139868865541888 (0x7f35c20bb700)
[ ~/unix_practice]$ ./threadCreate 
main thread:  pid:13551 tid:139870974031648 (0x7f363fb8b720)
new thread:  pid:13551 tid:139870956889856 (0x7f363eb32700)
[ ~/unix_practice]$ ./threadCreate 
new thread:  pid:13553 tid:140530159355648 (0x7fcfba3a8700)
main thread:  pid:13553 tid:140530176497440 (0x7fcfbb401720)
123456789

从运行结果可以看出,主线程和新线程的运行顺序是随机的,他们的进程id相同,线程id不同,但处于同一片内存区域。

2 线程终止

2.1 进程终止

进程中的任意线程调用了exit、_Exit、或者_exit,则进程会终止。

2.2 仅线程终止

在不终止进程的情况下,有3种方法可以终止线程:

  1. 返回。线程运行完成,从启动例程中返回。
  2. 被取消。被同一进程中的其他线程调用pthread_cancel函数取消。
  3. 调用了pthread_exit函数。 在线程内部调用pthread_exit函数可以终止该线程。

2.3 获取线程终止状态

可以通过pthread_join函数获取线程的终止状态。

int pthread_join(pthread_t thread, void **rval_ptr);
1

当线程正常终止时,rval_ptr就包含返回码。如果线程被取消,有rval_ptr指向的内存单元就设置为PTHREAD_CANCELED。

注,pthread_join会阻塞当前线程,直到等待的thread终止。

2.4 线程取消

同一进程中的线程可以调用pthread_cancel函数取消另一个线程,它的效果如同使用了PTHREAD_CANCELED参数的pthread_exit函数。pthread_cancel并不等待线程终止,它仅仅是提出请求,线程可以从容调用清理程序。

2.5 线程清理程序

一个线程可以建立多个清理程序,清理程序记录在栈中,因而他们的执行顺序与注册时相反。pthread_cleanup_push用于注册清理程序,pthread_cleanup_pop用于删除清理程序。这两个函数必须成对使用(因为他们可以实现为宏,左右大括号必须配对)。

线程清理程序在下面的3种情况下会被调用:

  1. 调用pthread_exit时;
  2. 响应取消请求时;
  3. 用非零execute参数调用pthread_cleanup_pop时。

注意,如果线程是从它的启动例程中返回而终止的话,它的清理程序就不会被调用。

2.6 线程分离pthread_detach

创建一个线程默认的状态是joinable,如果一个线程结束运行但没有被join,则它的状态类似僵尸进程,即还有一部分资源没有被回收(退出状态码)。因此创建线程有义务调用pthread_join来回收子线程资源(类似于waitpid)。

但如果在调用pthread_join后,新线程并没有结束,则调用线程会被阻塞。在有些情况下我们并不希望如此,如网络服务时。这种情况下,就可以将子线程设置为分离状态(detach),在该状态下,线程运行结束后会自动释放所有资源。设置分离状态有以下两种方法:

  1. 子线程调用pthread_detach(pthread_self())。
  2. 父线程调用pthread_detach(thread_id),非阻塞,立即返回。

3 线程同步

3.1 线程同步方式汇总

  1. 互斥量:只有加/不加锁两种状态,一次只有一个线程加锁。
  2. 读写锁:有读锁、写锁、不加锁3种状态,读锁可多线程占有。
  3. 条件变量:一个由互斥量保护的变量,自主计算条件,更灵活。
  4. 自旋锁:自旋等待,免除调度。
  5. 屏障(barrier):多线程并行同步。

3.2 互斥量

互斥量用pthread_mutex_t数据类型表示,它从本质上说是一把锁,一次只允许一个线程访问。

3.2.1 互斥量操作

互斥量主要有以下5个操作:

1pthread_mutex_init(pthread_mutex_t * mutex, const pthread_mutexattr_t *attr);
// 初始化锁变量mutex。
// attr为锁属性,NULL值为默认属性。

2pthread_mutex_lock(pthread_mutex_t *mutex);
// 加锁(阻塞操作)

3pthread_mutex_trylock(pthread_mutex_t *mutex);
// 试图加锁(不阻塞操作)
// 当互斥锁空闲时将占有该锁;否则立即返回
// 但是与2不一样的是当锁已经在使用的时候,返回为EBUSY,而不是挂起等待。

4pthread_mutex_unlock(pthread_mutex_t *mutex);
释放锁

5pthread_mutex_destroy(pthread_mutex_t *mutex);
使用完后删除
1234567891011121314151617

3.2.2 互斥量使用实例

使用两个线程独立打印0~4,用互斥量进行线程同步。

#include <unistd.h>
  2 #include <pthread.h>
  3 #include <stdio.h>
  4 
  5 pthread_mutex_t mutex;
  6 void *print_msg(void *arg){
    
    
  7         int i=0;
  8         pthread_mutex_lock(&mutex);
  9         for(i=0;i<5;i++){
    
    
 10                 printf("output : %d\n",i);
 11                 usleep(100);
 12         }
 13         pthread_mutex_unlock(&mutex);
 14 }
 15 int main(int argc,char** argv){
    
    
 16         pthread_t id1;
 17         pthread_t id2;
 18         pthread_mutex_init(&mutex,NULL);
 19         pthread_create(&id1,NULL,print_msg,NULL);
 20         pthread_create(&id2,NULL,print_msg,NULL);
 21         pthread_join(id1,NULL);
 22         pthread_join(id2,NULL);
 23         pthread_mutex_destroy(&mutex);
 24         return 1;
 25 }
12345678910111213141516171819202122232425

测试结果

[~/unix_practice]$ g++ -l pthread -o mutexOpt mutexOpt.cpp 
[ ~/unix_practice]$ ./mutexOpt 
output : 0
output : 1
output : 2
output : 3
output : 4
output : 0
output : 1
output : 2
output : 3
output : 4
123456789101112

3.2.3 避免死锁

使用互斥量需要特别小心,否则会很容易造成死锁。例如,如果一个线程试图对同一个互斥量加锁两次,那么它自身就会陷入死锁状态;程序在使用多个互斥量时,两个线程以相反的顺序获取互斥量也可能会导致死锁。

3.3 读写锁

读写锁用数据类型pthread_rwlock_t表示,它可以有3种状态,读锁状态,写锁状态,不加锁状态。读锁可多线程同时占有,读写锁非常适合于对数据结构读的次数远大于写的情况。(通常,当读写锁处于读模式锁住状态,这时有一个线程试图以写模式获取锁时,读写锁通常会阻塞随后的读模式锁请求。这样可以避免读模式锁长期占用,而等待的写模式锁请求一直得不到满足。)

3.3.1 读写锁操作

#include<pthread.h>

//1. 读写锁初始化方法
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);

//2. 读写锁销毁方法
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

//3. 读模式下加锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

//4.写模式下加锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

//5. 解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

//6. 尝试读锁
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

//7. 尝试写锁
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

//8. 带超时的读锁
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict tsptr);

//9. 带超时的写锁
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict tsptr);


//所有函数,成功返回0;否则,返回错误编号
12345678910111213141516171819202122232425262728293031

3.3.2 读写锁应用

读写锁可以应用在作业队列中,来控制队列的读和写。

struct queue{
    
    
    struct job *q_head;
    struct job *q_tail;
    pthread_rwlock_t q_lock;
};
12345

3.4 条件变量

条件变量用pthread_cond_d数据类型表示,它给多个线程提供了一个汇合的场所。条件本身由互斥量保护。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。

3.4.1 条件变量的操作

条件变量有关的操作如下所示:

#include<pthread.h>

//1.条件变量初始化方法
//静态分配的条件变量可以通过赋值PTHREAD_COND_INITIALIZER来初始化
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);

//2.条件变量销毁方法
int pthread_cond_destroy(pthread_cond_t *cond);

//3.等待条件变量变为真。调用者把锁住的互斥量传给函数,函数自动把调用线程放到等待条件的线程列表上,并对互斥量解锁。该函数返回时,互斥量再次被锁住。
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

//4. 在给定时间内等待条件变量变为真
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict tsptr);

//5. 用于通知线程条件已满足。该函数至少能唤醒一个等待该条件的线程
int pthread_cond_signal(pthread_cond_t *cond);

//6.用于通知线程条件已满足。该函数将唤醒所有等待该条件的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
1234567891011121314151617181920

3.5 自旋锁

自旋锁用类型pthread_spinlock_t表示,在获取锁之前一直处于忙等(自旋)阻塞状态。自旋锁适用的情况是:锁被持有的时间短,而且线程并不希望在重新调度上花费太多的成本。

自旋锁也会在一定程度上导致CPU资源浪费:当线程自旋时,CPU不能做任何其他事情。很多互斥量的实现非常高效,其性能与使用自旋锁的性能基本相同。自旋锁只在某些特定情况下有用。

3.5.1 自旋锁的操作

//1. 初始化自旋锁
// pshared为PTHREAD_PROCESS_SHARED时,其他进程的线程也可以访问该锁
// pshared为PTHREAD_PROCESS_PRIVATE时,只有本进程中的线程才可以访问该锁
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);

//2. 销毁自旋锁
int pthread_spin_destroy(pthread_spinlock_t *lock);

//3. 加锁。如果不能获取锁,则一直自旋
int pthread_spin_lock(pthread_spinlock_t *lock);

//4. 尝试加锁。如果不能获取锁,则立即返回EBUSY错误,不会自旋
int pthread_spin_trylock(pthread_spinlock_t *lock);

//5. 自旋锁解锁
int pthread_spin_unlock(pthread_spinlock_t *lock);
12345678910111213141516

3.6 屏障Barrier

屏障(barrier)是用户协调多个线程并行工作的同步机制,用类型pthread_barrier_t表示。它允许每个线程等待,直到所有的合作线程都到达某一点,然后从该点继续执行。pthread_join函数就是一种特殊的屏障,允许一个线程等待,直到另一个线程退出。

3.6.1 屏障的操作

//1. 屏障初始化。count表示继续运行前到达屏障的线程数目,attr用于配置屏障的属性。
int pthread_barrier_init(pthread_barrier_t *restrict barrier, const pthread_barrierattr_t *restrict attr, nsigned int count);

//2. 销毁屏障
int pthread_barrier_destroy(pthread_barrier_t *barrier);

//3. 表明调用线程已经完成工作,进入休眠状态,等count数量的其他线程赶来
int pthread_barrier_wait(pthread_barrier_t *barrier);
12345678

3.6.2 屏障应用实例

使用多线程+屏障对百万数据进行排序,效率是单线程的6倍以上。

4 线程控制

目前主流操作系统,包括linux,对进程可以创建的最大线程数都没有确定的限制。但这并不意味着没有限制,只是不能用sysconf查出而已。

4.1 线程属性

可以通过属性参数pthread_attr_t对线程的属性进行设置,例如分离状态、线程栈最低地址、线程栈大小等。具体信息可以查阅《Unix环境高级编程 第3版》 第12章 p341。

4.2 同步属性

线程的同步对象也有属性,例如互斥量属性、读写锁属性、条件变量属性和屏障属性。

  1. 互斥量属性:pthread_mutexattr_t表示,有进程共享属性、健壮属性以及类型属性等。
  2. 读写锁属性:pthread_rwlockattr_t表示,读写锁支持的唯一属性是进程共享属性。
  3. 条件变量属性:pthread_condattr_t表示,条件变量有两个属性:进程共享属性和时钟属性。
  4. 屏障属性:pthread_barrierattr_t表示,只有进程共享属性。

4.3 线程和fork

当线程调用fork时,就为子进程创建了整个进程地址空间的副本。

POSIX.1声明,在fork返回和子进程调用其中一个exec函数之间,子进程只能调用异步信号安全的函数。

这样限制的原因在于:父进程中的线程调用fork函数生成子进程,子进程内部只存在一个线程(调用fork线程的副本)。生成的子进程拥有父进程的整个地址空间副本(也包含了父进程持有的锁),但由于没有继承所有线程,所以子进程没办法知道自己占有了哪些锁、需要释放哪些锁。因此,在父进程调用fork后,子进程调用exec清理内存空间前,子进程是不安全的,这期间只能调用异步信号安全的函数。

要清除锁状态,可以通过调用pthread_atfork函数建立fork处理程序(函数原型及说明实例见p367)。

4.4 线程和I/O

在多线程环境下,同时对一个文件进行普通的读写操作(如lseek、read和write),可能会出现相互覆盖等意料之外的结果,这是由于同一进程中的线程共享相同的文件描述符导致的。要避免这种情况,可以使用线程安全的读写函数,如pread、pwrite等。

pwrite()函数基本上等同于write(),但它不更改文件偏移量(无论是否设置了o_append),因而它是线程安全的。

猜你喜欢

转载自blog.csdn.net/qq_22473333/article/details/113506147