多线程

多线程

  1. 对于每个进程,系统资源都是独立的,频繁的切换进程产生额外的开销严重影响系统性能
  2. 类似于交互性的GUI程序,如果每个任务都由进程来实现,就会非常低效
  3. 每一个进程内部的多个线程共享进程的上下文资源
  4. 一个程序运行过程中,只有一个控制权,函数被调用时,该函数获得控制权,成为激活函数
  5. 多线程允许一个进程内存存在多个控制权,以便让多个函数同时处于激活状态,允许多个函数操作同时运行
  6. 栈后进先出,只有最下方的帧可以被读写,创建一个新线程的时候传建一个新的栈,每个栈对应一个线程。某个栈执行全部弹出之后,对应线程完成并结束
  7. 如上所述,多线程的进程在内存中有多个栈,多个栈之间用一定的空白区域隔开
  8. 每个线程调用自己栈最下方的帧中的参数和变量,并与其他线程共享内存中的Text、heap和global data区域

线程的创建和结束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <pthread.h>

int pthread_creat(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
/* *thread是指向线程标识符的指针*/
/* *attr设置线程属性,指定不同的线程属性*/
/* 第三个参数是线程运行函数的起始地址*/
/* *arg是运行函数的参数*/
/*若线程创建成功,则返回0。thread指向的内存单元被设置为新创建线程的线程ID,新创建的线程从*start_routine函数的位置开始执行*/
/*线程创建出错,返回出错编号,新创建的线程从*start_routine*/

int pthread_join(pthread_t thread, void **retval);
//函数等待一个线程结束
//thread表示被等待的线程标识符
//**retval为用户定义的指针,用来存放被等待线程的返回值
//这个函数是线程阻塞函数,调用它的函数一直等待到被等待的线程结束为止,函数返回时,被等待的线程的资源被回收


void pthread_exit(void *retval);
//函数结束,调用它的线程结束
//pthread_join由主线程来调用,等待子线程退出,pthread_exit由子线程调用,用来结束当前线程
//子线程通过pthread_exit传递一个返回值,主线程通过pthread_join来或者该返回值来判断该子进程的退出是正常还是异常

//获得线程ID,在线程调用函数使用pthread_self获得,或者在创建函数时生成的ID

线程的属性

  1. 线程有一组属性可以在线程被创建时指定,改组属性被封装在一个对象中,对象可以用来设置一个或一组线程的属性
  2. 线程属性对象的类型为pthread_addr_t
  3. 属性值不能直接设置,必须使用相关函数进行操作,初始化的函数为pthread_addr_init
  4. 进程地址空间不够用时,指定新建线程使用malloc分配的空间作为自己的栈空间

多线程同步

互斥锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <stdio.h>
#include <pthread.h>
#include <unistd.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); //使用互斥锁将对total_ticket_num的操作进行加锁
if(total_ticket_num>0)
{
sleep(1);
total_ticket_num--;
printf("sell the %dth ticket\n",20-total_ticket_num);
}
//pthread_mutex_unlock(&mutex_x); //使用互斥锁将对total_ticket_num的操作进行解锁
}
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); //创建成功返回0,&sell_ticket线程运行函数的起始位置
if(iRet)
{
printf("pthread_creat error, iRet=%d\n",iRet);
return iRet;
}
}
sleep(20);
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;
}
  1. 未加互斥锁之前产生竞争,并且出现票超卖
  2. 加互斥锁后
  3. 创建互斥锁有动态和静态两种,上述的加锁方式是静态的
  4. 锁可以用pthread_mutex_init动态地创建

条件变量

  1. 使用条件变量来阻塞一个进程,当条件不满足时,线程需要解开互斥锁并等待条件发生变化
  2. 条件变量也有静态和动态两种创建方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    pthread_cond_t cond=PTHREAD_COND_INITIALIZER; //静态
    int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
    //动态,当cond为NULL时,使用默认属性
    int pthread_cond_destory(pthread_cond_t *cond); //注销一个条件变量
    int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); //等待
    int pthread_cond_timewait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime); //条件等待
    //这两种等待都必须和互斥锁配合防止多个线程同时请求等待产生竞争
    pthread_cond_singal(); //激活一个等待该条件的进程,存在多个等待进程时,按照顺序激活其中一个
    pthread_cond_broadcast(): //激活所有等待线程
  3. 在某些情况下,使用条件变量没有掌握好触发的时机,可以使用计数器来记录等待的线程的个数,在决定触发条件变量前先检查该变量

读写锁

  1. 可以哟多个线程同时占用读模式,但是只允许一个线程占用写模式
  • 写加锁状态,这个锁被解锁之前,所有对这个锁加锁的线程都会被阻塞
  • 读加锁状态,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是以写模式对它进行加锁的线程都会被阻塞
  • 读模式的锁栈状态,当有线程以写模式加锁时,读写锁对随后的读模式锁的请求阻塞,避免读模式锁被长期阻塞
  1. 读写锁适用于对数据结构的读操作次数大于写操作次数的场合
  2. 读写锁有两种初始化方式:
  • 通过静态分配的读写锁赋予常值PTHREAD_RWLOCK_INITIALIZER
  • 调用pthread_rwlock_init()动态的初始化,如果执行成功则返回0,出错则返回出错码
    int pthread_rwlock_init(pthread_rwlock_t *rwptr, const pthread_rwlockattr_t *attr);
  1. 当线程不需要读写锁的时候,调用pthread_rwlock_destory(),如果执行成功则返回0,出错则返回出错码
    int pthread_rwlock_destory(pthread_rwlock_t *rwptr);
  2. 读写锁的数据类型是pthread_rwlock_t
    1
    2
    3
    4
    5
    6
    7
    8
    9
    pthread_rwlock_rdlock(pthread_rwlock_t *rwptr); //用来获取读出锁,如果相应的读出锁被某个写入者占有,则阻塞调用线程
    pthread_rwlock_wrlock(pthread_rwlock_t *rwptr); //用来获取写入锁,如果相应的写如梭被其他写入锁和读出者占用,则阻塞该调用线程
    pthread_rwlock_unlock(pthread_rwlock_t *rwptr); //用来释放一个读出锁或者写入锁
    //以上三个函数都是阻塞式的,如果获取不到锁,调用线程不是立即返回的,调用线程投入到睡眠等待
    //调用成功返回0,调用失败返回错误码

    //非阻塞式的读写锁的函数,如果不能马上获取到,立即返回EBUSY错误提示
    pthread_rwlock_rdlock(pthread_rwlock_t *tryrwptr);
    pthread_rwlock_wrlock(pthread_rwlock_t *tryrwptr);

信号量

  1. 互斥锁只允许一个线程进入临界区,信号量允许多个线程进入临界区
  2. 要使信号量同步,需要包含头文件semaphore.h
    1
    2
    3
    4
    5
    int sem_init(sem_t *sem, int pshared, unsigned int value); 
    //初始化sem指向的信号对象,设置它的共享选项,并给它一个初始的整数值,pshared=0表示这个信号量是当前进程的局部信号量,否则信号量在多个进程之间共享
    int sem_wait(sem_t *sem); //用于以原子操作的方式将信号量的值-1
    int sem_post(sem_t *sem); //用于以原子操作的方式将信号量的值+1
    int sem_destory(sem_t sem); //用于对用完的信号量进行清理

多线程可重入

  1. 可重入函数的特点:
  • 不为连续的调用持有静态数据
  • 不返回指向静态数据的指针
  • 所有数据都由函数的调用者提供
  • 使用本地数据,或者通过制作全局数据的本地副本来保护全局数据
  • 必须调用全局变量时,要利用互斥锁和信号量来保护全局变量
  • 不调用任何不可重入函数
  1. 可重入代码可以被多次调用而仍然正常工作
  2. 编写的多线程程序,通过定义宏_REENTRANT告诉编译器需要可重入功能
  3. _REENTRANT做的三件事
  • 为部分函数重新定义的可安全重入的版本
  • stdio.h中的原来以宏的形式实现一些函数将变成可安全重入的函数
  • 在error.h中定义的变量error现在将成为一个函数调用,以一种安全的多线程方式来获取真正的errno的值
WhitneyLu wechat
Contact me by scanning my public WeChat QR code
0%