1.多线程哪些数据是共享的
共享的数据:代码段,已经打开的文件描述符号,全局变量和静态变量(全局区),堆区。
线程独有的数据:栈和寄存器。
2.单进程多线程 VS 多进程
需要频繁创建和销毁的用多线程
需要数据共享的数据比较多可以用多线程
方便进行跨机器部署的用多进程
数据同步比较复杂的用多进程
1.线程局部存储
2.
getpid(): 获得进程pid
pthread_self() : 获得线程识别码
线程函数要写成:void * func(void *){}形式
锁初始化:
int result;
if((result = pthread_mutex_init(&pool->_ready_mutex, NULL)) < 0)
{
U_WARN("pthread_mutex_init error, code %d", result);
apool_destroy(sev);
return -1;
}
if((result = pthread_cond_init(&pool->_ready_cond, NULL)) < 0 )
{
U_WARN("pthread_mutex_init error, code %d", result);
apool_destroy(sev);
return -1;
}
3. pthread_cond_timedwait()
pthread_cond_timedwait等待的是信号量,有信号传来就重新加锁,等不到信号就解锁,发送信号的线程抢到锁后就发送信号。
当在指定时间内有信号传过来时,pthread_cond_timedwait()返回0,否则返回一个非0数(我没有找到返回值的定义);
在使用pthread_cond_timedwait()函数时,必须有三步:
1:加互斥锁:pthread_mutex_lock(&__mutex)
2:等待:pthread_cond_timedwait(&__cond, &__mutex, &__abstime) //解锁->等待->加锁
3:解互斥锁:pthread_mutex_unlock(&__mutex)
发送信号量时,也要有三步:
1:加互斥锁:pthread_mutex_lock(&__mutex)
2:发送:pthread_cond_signal(&__cond)
3:解互斥锁:pthread_mutex_unlock(&__mutex)
4.pthread_join与pthread_detach
pthread_join()即是子线程合入主线程,主线程阻塞,等待子线程结束,然后回收子线程资源。如果主线程提前结束,子线程也会被强制结束。应该避免主线程出现异常导致子线程没有完成任务。
pthread_detach()即主线程与子线程分离,子线程结束后,资源自动回收。
pthread_detach(pthread_self());//也可以在主线程内设置
1.pthread有两种状态joinable状态和unjoinable状态,如果线程是joinable状态,当线程函数自己返回退出时或pthread_exit时都不会释放线程所占用堆栈和线程描述符(总计8K多)。只有当你调用了pthread_join之后这些资源才会被释放。若是unjoinable状态的线程,这些资源在线程函数退出时或pthread_exit时自动会被释放。
2.unjoinable属性可以在pthread_create时指定,或在线程创建后在线程中pthread_detach自己, 如:pthread_detach(pthread_self()),将状态改为unjoinable状态,确保资源的释放。或者将线程置为 joinable,然后适时调用pthread_join.
注意:pthread_detach分离后的线程不会阻塞主线程,主线程销毁后释放资源,有可能导致子线程使用已经被释放的变量。
5.pthread初始化锁
pthread_mutex_t init_lock; pthread_mutex_init(&init_lock, NULL); pthread_mutex_lock(&init_lock) //操作 pthread_mutex_unlock(&init_lock)
6.线程的调度策略:
linux内核的三种调度方法:
1,SCHED_OTHER 分时调度策略, 默认的策略
2,SCHED_FIFO实时调度策略,先到先服务
3,SCHED_RR实时调度策略,时间片轮转
SHCED_RR和SCHED_FIFO的不同:
SHCED_RR策略的进程的时间片用完,系统将重新分配时间片,并置于就绪队列队尾。放在队列尾保证了所有具有相同优先级的RR任务的调度公平。
SCHED_FIFO一旦占用cpu则一直运行。一直运行直到有更高优先级任务到达或自己放弃。
如果有相同优先级的实时进程(根据优先级计算的调度权值是一样的)已经准备好,FIFO时必须等待该进程主动放弃后才可以运行这个优先级相同的任务。而RR可以让每个任务都执行一段时间。
相同点:
RR和FIFO都只用于实时任务。
创建时优先级大于0(1-99)。
按照可抢占优先级调度算法进行。
就绪态的实时任务立即抢占非实时任务。
设置线程优先级:
int pthread_setschedparam(pthread_t target_thread, int policy, const struct sched_param *param)
线程调度策略Demo:
https://blog.csdn.net/fchyang/article/details/79268204
7.线程让出CPU
线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:
1、线程体中调用了 yield 方法让出了对 cpu 的占用权利,在后续的调度周期里再参与CPU调度,yield()方法让出CPU的时间是不确定的,并且以CPU调度时间片为单位。
yield()的实现依赖于操作系统CPU调度策略,在不同的操作系统或者同一个操作系统的不同调度策略下,表现也可能是不同的。
https://blog.csdn.net/c_base_jin/article/details/79246211
2、线程体中调用了 sleep 方法使线程进入睡眠状态,与yield不同的是,sleep休眠时间固定。
3、线程由于 IO 操作受到阻塞
4、更高优先级线程出现
5)在支持时间片的系统中,该线程的时间片用完
目前能让线程阻塞方法有:join() ,wait(), sleep()
8.线程同步方法:
四种进程或线程同步互斥的控制方法
1、临界区: 通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问,只有一个线程访问资源。(windows才有,Linux没有临界区)
2、互斥量: 为协调共同对一个共享资源的单独访问而设计的。 Mutex,只有一个线程访问资源。(Windows和Linux都有)
3、信号量: 为控制一个具有有限数量用户资源而设计。 PV操作,限制对资源的使用的线程数量,多个线程访问资源。信号量可以说是一种资源计数器。
4、事 件: 用来通知线程有一些事件已发生,从而启动后继任务的开始。
区别:
临界区是用户态锁,访问速度更快;互斥量是内核锁,访问速度慢。
临界区适用于同一进程内的不同线程间,互斥量可以用于不同的进程或不同的线程间
9.主线程取消线程子线程
int pthread_cancel(pthread_t thread);
11. 线程属性的初始化
typedef struct
{
int detachstate; // 线程的分离状态
int schedpolicy; // 线程调度策略
structsched_param schedparam; // 线程的调度参数
int inheritsched; // 线程的继承性
int scope; // 线程的作用域
size_t guardsize; // 线程栈末尾的警戒缓冲区大小
int stackaddr_set; // 线程的栈设置
void* stackaddr; // 线程栈的位置
size_t stacksize; // 线程栈的大小
} pthread_attr_t;
使用
int pthread_attr_init(pthread_attr_t *attr);
在默认情况下线程是joinable状态的,只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。
分离线程运行结束,线程也就终止了,马上释放系统资源。
程序员应该根据自己的需要,选择适当的分离状态。所以如果我们在创建线程时就知道不需要了解线程的终止状态,则可以pthread_attr_t结构中的detachstate线程属性,让线程以分离状态启动。
int pthread_attr_getdetachstate(const pthread_attr_t *attr,int *detachstate); int pthread_attr_setdetachstate(pthread_attr_t *attr,int detachstate);
12.就绪队列和等待队列
未获取资源时处于阻塞状态,处于等待队列
已经获取资源但是还未获得CPU处于就绪状态,处于就绪队列
等待队列是一种基于资源状态的线程管理的机制,它可以使线程在资源不满足的情况下处于休眠状态,让出CPU资源,而资源状态满足时唤醒线程,使其继续进行业务的处理。等待队列(wait queue)用于使线程等待某一特定的事件发生而无需频繁的轮询,进程在等待期间睡眠,在某件事发生时由内核自动唤醒。它是以双循环链表为基础数据结构,与进程的休眠唤醒机制紧密相联,是实现异步事件通知、跨进程通信、同步资源访问等技术的底层技术支撑。
在线程中,调用sleep(0)可以释放cpu时间,让线程马上重新回到就绪队列而非等待队列
sleep(0)释放当前线程所剩余的时间片(如果有剩余的话),这样可以让操作系统切换其他线程来执行,提升效率。
14.锁机制
自旋锁(spin lock)是一种非阻塞锁,也就是说,如果某线程需要获取锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取锁。线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。自旋锁不适用于单核CPU。
https://blog.51cto.com/u_9291927/2581173
#include <pthread.h> int pthread_spin_destroy(pthread_spinlock_t *lock); int pthread_spin_init(pthread_spinlock_t *lock, int pshared); int pthread_spin_lock(pthread_spinlock_t *lock); int pthread_spin_trylock(pthread_spinlock_t *lock); int pthread_spin_unlock(pthread_spinlock_t *lock);
互斥量(mutex)是阻塞锁,当某线程无法获取锁时,该线程会被直接挂起,该线程不再消耗CPU时间,当其他线程释放锁后,操作系统会激活那个被挂起的线程,让其投入运行。线程B从没有获取锁到获取锁这个过程中,就要用户态和内核态调度、上下文切换的开销和损耗。
自旋锁比较适用于锁使用者保持锁时间比较短的情况,这种情况下自旋锁的效率要远高于互斥锁。
锁的实现原理:
没有futex时,获取不到锁的任务会陷入内核并挂起,并添加到mutex对应的链表上,获得锁的任务在释放锁后会唤醒链表上的其他等待锁的任务。需要多次陷入内核:第一次陷入内入需要判断有无锁竞争,第二次陷入内核是在释放锁时判断有无任务在等待这个锁。所以普通的互斥锁效率比较低,上下文切换比较频繁。
pthread_mutex_t 锁的原理是采用了Futex机制来实现,以提高锁的效率:
Futex是一种用户态和内核态混合的同步机制。首先,同步的进程间通过mmap共享一段内存,futex变量就位于这段共享的内存中且操作是原子的,当进程尝试进入互斥区或者退出互斥区的时候,先去查看共享内存中的futex变量,如果没有竞争发生,则只修改futex,而不用再执行系统调用了。当通过访问futex变量告诉进程有竞争发生,还是需要陷入内核并挂起任务。简单的说,futex就是通过在用户态的检查,如果发现没有锁竞争就不用陷入内核了。
在用户态使用原子操作,对持有锁的持有情况进行判断,如果锁可以占用,那么更新锁的状态并直接占用,不需要进入内核态,如果锁已经占用,则进入内核态挂起当前任务,事实上这些操作对程序员不可见的,一般都是由C库提实现好了。
直接对共享内存中的变量做原子操作+1-1,根据正负来判断有无锁竞争。
18.读写锁
读写锁是用来解决读者写者问题的,读操作可以共享,写操作是排他的,读可以有多个在读,写只有唯一个在写,同时写的时候不允许读。
|
操作 |
相关函数说明 |
|---|---|
|
初始化读写锁 |
|
|
读锁 |
|
|
读取非阻塞读写锁中的锁 |
|
|
写锁 |
|
|
写入非阻塞读写锁中的锁 |
|
|
解除锁定读写锁 |
|
|
销毁读写锁 |
//创建初始化
pthread_rwlock_t *m_lock; if( ( m_lock = ( pthread_rwlock_t * )malloc( sizeof( pthread_rwlock_t ) ) ) == 0 ){ return -1; } if( pthread_rwlock_init( m_lock, NULL ) != 0 ){ free(m_lock); m_lock = 0; return -1; }
//获取读锁 pthread_rwlock_rdlock(m_lock); //do something //解锁 pthread_rwlock_unlock(m_lock);
//获取写锁 pthread_rwlock_wrlock(m_lock); pthread_rwlock_unlock(m_lock);
//销毁
if( m_lock ){ pthread_rwlock_destroy(m_lock); free(m_lock); }
15. CPU操作内存的步骤:
count++
第一步,先将 count所在内存的值加载到寄存器;
第二步,将寄存器的值自增1;
第三步,将寄存器中的值写回内存。
16.thread_local
在一个线程内部的各个函数调用都能访问、但其它线程不能访问的变量,这就需要新的机制来实现,我们称之为线程局部静态变量 或者 线程局部存储。
一次性初始化:多线程程序有时有这样的需求:不管创建多少个线程,有些数据的初始化只能发生一次。例如:在C++程序中某个类在整个进程的生命周期内只能存在一个实例对象,在多线程的情况下,为了能让该对象能够安全的初始化,一次性初始化机制就显得尤为重要了。——在设计模式中这种实现常常被称之为单例模式(Singleton)。
int pthread_once( pthread_once_t *once_control, void(*init) ( void)) ;
利用参数once_control的状态,函数pthread_once可以确保无论有多少个线程调用多少次该函数,也只会执行一次由init所指向的由调用者定义的函数。init所指向的函数没有任何参数。
19. lock_guard和unique_lock
lock_guard创建即加锁,作用范围外自动解锁
unique_lock lock加锁 unlock解锁,随时加锁,随时解锁。使用更灵活开开销大。
20.线程清理函数
“线程取消函数”即线程被取消或者下面描述的情况发生时自动调用的函数。它一般用于释放一些资源,比如释放锁,以免其它的线程永远 也不能获得锁,而造成死锁。
pthread_cleanup_push()函数执行压栈清理函数的操作,而pthread_cleanup_pop()函数执行从栈中删除清理函数的操作。
在下面三种情况下,pthread_cleanup_push()压栈的“清理函数”会被调用:
1, 线程调用pthread_exit()函数,而不是直接return.
2, 响应取消请求时,也就是有其它的线程对该线程调用pthread_cancel()函数。
3, 本线程调用pthread_cleanup_pop()函数,并且其参数非0
注意:
1.当pthread_cleanup_pop()函数的参数为0时,仅仅在线程调用pthread_exit函数或者其它线程对本线程调用
pthread_cancel函数时,才在弹出“清理函数”的同时执行该“清理函数”。
2.注意pthread_exit终止线程与线程直接return终止线程的区别:调用return函数是不会在弹出“清理函数”的同时执行该“清理函数的。
头文件:#include <pthread.h> //采用先入后出的栈结构管理 函数原型:void pthread_cleanup_push(void (*rtn)(void *), void *arg); void pthread_clean_pop(int execute); //execute参数表示执行到pthread_cleanup_pop()时是否在弹出清理函数的同时执行该函数,为0表示不执行,非0为执行;
//这个参数并不影响异常终止时清理函数的执行(及当pthread_cleanup_pop()函数的参数为0时,仅仅在线程调用pthread_exit
//函数或者其它线程对本线程调用 pthread_cancel函数时,才在弹出“清理函数”的同时执行该“清理函数”)。 void(*rtn)(void *): 线程清理函数
demo
https://blog.csdn.net/longbei9029/article/details/72871714
当pthread_cleanup_pop()函数的参数为0时,仅仅在线程调用pthread_exit函数或者其它线程对本线程调用 pthread_cancel函数时,才在弹出“清理函数”的同时执行该“清理函数”。
21.线程取消pthread_cancel
先描述一下取消一个线程的过程:
1) 其他线程通过调用pthread_cancel()函数,向目标线程发送取消请求(cancellation request)。
2) 取消请求发出后,根据目标线程的cancel state来决定取消请求是否会到达目标线程:
a. 如果目标线程的cancel state是PTHREAD_CANCEL_ENABLE(默认),取消请求会到达目标线程。
b. 如果目标线程的cancel state是PTHREAD_CANCEL_DISABLE,取消请求会被放入队列。直到目标线程的cancel state变为PTHREAD_CANCEL_ENABLE,取消请求才会从队列里取出,发到目标线程。
3) 取消请求到达目标线程后,根据目标线程的cancel type来决定线程何时取消:
a. 如果目标线程的cancel type是PTHREAD_CANCEL_DEFERRED(默认),目标线程并不会马上取消,而是在执行下一条cancellation point的时候才会取消。有很多系统函数都是cancellation point,详细的列表可以在Linux上用man 7 pthreads查看。除了列出来的cancellation point,pthread_testcancel()也是一个cancellation point。就是说目标线程执行到pthread_testcancel()函数的时候,如果该线程收到过取消请求,而且它的cancel type是PTHREAD_CANCEL_DEFERRED,那么这个线程就会在这个函数里取消(退出),这个函数就不再返回了,目标线程也没有了。
b. 如果目标线程的cancel type是PTHREAD_CANCEL_ASYNCHRONOUS,目标线程会立即取消(这里的“立即”只是说目标线程不用等执行到属于cancellation point的函数的时候才会取消,它会在获得调度之后立即取消,因为内核调度会有延时,所以并不能保证时间上的“立即”)。
void thread_function( void *arg)
{
/**
* 线程准备执行一些关键工作,在这个过程中不希望被取消。
* 所以先通过pthread_setcancelstate()将本线程的cancel state
* 设为disabled。
*/
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
/* 执行关键工作 */
...
/**
* 关键工作执行完成,可以被取消。
* 通过pthread_setcancelstate()将本线程的cancel state
* 设为enabled。
*/
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
/**
* 调用pthread_testcancel()函数,检查一下在cancel state
* 为disabled状态的时候,是否有取消请求发送给本线程。
* 如果有的话就取消(退出)。被结束的线程必须在某一个点来进行退出操作,就需要用到pthread_testcancel。
*/
pthread_testcancel();
/**
* pthread_testcancel()返回了,表明之前没有取消请求发送给本线程,
* 继续其余的工作。
* 这时候如果有取消请求发送给本线程,会在下一次执行到
* cancellation point的时候(例如sleep(), read(), write(), ...)时取消。
*/
...
/**
* 从这里开始,函数里不再包含cancellation point了。
* 如果收到取消请求,将无法取消。所以先把本线程的cancel type
* 设为asynchronous,收到取消请求将立即取消。
*/
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
/* 不包含cancellation point的代码 */
...
}
22.多进程同步方式
https://blog.csdn.net/okiwilldoit/article/details/78487507
信号量
无锁CAS
文件锁
23.互斥量和信号量区别
互斥量用于线程的互斥,信号量用于线程的同步。
这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别。
互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源
demo
多线程同步,顺序打印ABC。
https://gitee.com/lvvgitee/codes/pc4so581y2efvul0h3nbz13
24.静态局部变量的初始化存在线程安全问题,多线程初始化局部静态变量时,存在一个线程正在初始化另一个线程返回对象的问题。一般在多线程情况下不使用局部静态变量,如果需要可以使用全局锁来对初始化静态局部变量。这里需要知道,全局变量是在main函数之前的静态初始化过程中初始化并分配内存的,局部变量是在使用时在进行初始化,所以存在多线程安全的问题。
25. CAS锁
内存*reg里的值是不是oldval,如果是的话,则对其赋值newval,并返回true,表示更新成功,如果返回false,则表示修改失败。
bool compare_and_swap(int *reg,int oldval,int newval) { int reg_val = *reg; if(reg_val == oldval) { *reg = newval; return true; } return false; }
25. 无锁队列
https://blog.51cto.com/u_9291927/2588193
1. RingBuffer
适用于单消费单生产者场景,队列内部包括数据缓冲区,队首和队尾索引和队列长度,
对于单生产者和单消费者场景,由于read_index(队首索引)和write_index(队尾索引)都只会有一个线程写,因此不需要加锁也不需要原子操作,只需要判断队列满或者空,然后直接修改。
线程对write_index和read_index的读写操作如下:
(1)写操作。先判断队列时否为满,如果队列未满,则先写数据,写完数据后再修改write_index。
(2)读操作。先判断队列是否为空,如果队列不为空,则先读数据,读完再修改read_index。
2. LockFreeQueue
适用于多生产者多消费者场景
内部使用spinlock锁和开辟共享内存。LockFreeQueue并非无锁,只是使用了自旋锁。
队列内部的自旋锁结构体:
typedef struct
{
int m_lock;
inline void spinlock_init()
{
m_lock = 0;
}
inline void spinlock_lock()
{
while(!__sync_bool_compare_and_swap(&m_lock, 0, 1)) {}
}
inline void spinlock_unlock()
{
__sync_lock_release(&m_lock);
}
} spinlock_t;