正文
1 线程基础知识
1.1 线程编译
gcc -o pthread -lpthread pthread.c
1.2 线程和进程的区别
进程 | 线程 | 作用 |
---|---|---|
fork() | pthread_create() | 创建 |
exit() | pthread_exit() | 退出 |
wait()/waitpid() | pthread_join() | 回收资源 |
kill() | pthread _cancel() | 杀死进程、线程 |
getpid() | pthread_self() | 获取当前进程/线程号 |
exec | pthread_detach 子线程/进程脱离主线程/进程 |
1.3 线程状态转换
2 线程同步
大部分项目都会用到线程同步,而且下面的3种方法都会用到.
为什么要线程同步?
1 ) 防止多个线程共同访问一个全局变量或者函数产生错误. 多线程访问多一个函数的局部变量会产生错误吗?答案是不会,因为每个线程有自己的栈,局部变量保存在自己的栈中.https://www.cnblogs.com/binghe001/p/12808419.html
2 ) 线程时序控制
2.1 互斥
使用方法:锁.作用:防止多个线程访问互相影响,作用与下面2个不同.
pthread_mutex_lock(&rd);
readCount++;
pthread_mutex_unlock(&rd)
具体实例网上很多.
2.2 信号量(PV操作)
作用:互斥和同步.
原理:锁可以理解为信号量为1. 信号量可以用做资源计数器,允许N个线程同时执行一段代码,但是禁止N+1个线程同时运行.不需要人为干预,操作系统会计数. sem_post每次+1,sem_wait每次-1,当为0的时候不继续做减法,继续保持为0,这个特性很主要.个数{0,M}.c++信号量相当于java的CountDownLatch.
1)使用信号量实现线程互斥.
经典问题:哲学家问题,哲学家只有拿到一双筷子才能吃饭,
(https://img-blog.csdnimg.cn/img_convert/cca5b71d6c94925667eaa078887e822e.png)
2) 同步(控制线程时序):一个线程放一个线程取.比如:接收线程接收数据放入queue后sem_post(+1),另一个线程sem_wait(-1)(sem_wait 会阻塞线程)从queue中获取数据.同理, 一个线程业务逻辑处理完将结果放入发送queue,另一个线程发送也一样.
2.3 条件变量
作用: 同步(控制线程时序). 线程A发送cond_signal,线程B 在cond_wait 接收(cond_wait 阻塞线程)。cond_wait必须和mutex连用.cond_wait功能是先unlock,然后wait,等到signal后,再lock 。当没有cond_wait的时候,cond_signal 会被ignore.
网上例子也多的是.
其中cond_wait 有潜在的问题。核心是取消点,不是运行到哪句都立刻取消的,是到取消点即函数才取消.
https://www.cnblogs.com/mydomain/archive/2011/08/15/2139830.html
notes:java条件变量,notify,wait.
2.4 几种同步方法的异同
1 ) 锁必须是同一个线程获取以及释放, 否则会死锁.而条件变量和信号量则不必.
锁不能控制线程先后顺序.
2 ) 信号的递增与减少会被系统自动记住, 系统内部有一个计数器实现信号量,不必担心会丢失, 而唤醒一个条件变量时,如果没有相应的线程在等待该条件变量, 这次唤醒将被丢失.
3 ) 锁与信号量转换
Mutex是一把钥匙,一个人拿了就可进入一个房间,出来的时候把钥匙交给队列的第一个,一般的用法是用于串行化对临界区代码的访问,保证这段代码不会被并行的运行。
Semaphore是一件可以容纳N人的房间,如果人不满就可以进去,如果人满了,就要等待有人出来。
对于N=1的情况,称为binary semaphore,一般的用法是,用于限制对于某一资源的同时访问。在有的系统中Binary semaphore与Mutex是没有差异的.
实现:https://www.cnblogs.com/noble/p/4144015.html
4) 条件变量与信号量转换
sem_post 和sem_wait可以替换cond_signal 和cond_wait.
总之:信号量可以代替锁和条件变量,条件变量和锁联合使用也可以代替信号量.
扩展: 读写锁:https://blog.csdn.net/hik_zxw/article/details/20064577
自旋锁:线程处于就绪或者运行状态,不是处于阻塞状态(阻塞需要cpu做系统切换).这样可以随时调度,锁一被释放自己就可以拿到.
notes:synchronized锁lock类似。前者是乐观锁,后者是悲观锁.
3 智能指针与多线程#
指针不同于其他类型变量,指针内存在堆不在栈,需要释放.多线程指针释放容易产生错误,比如:在线程A使用,线程B释放的情况.
下面是chenshuo老师写的自定义智能指针. 采用无锁华变成,优化拷贝构造函数和赋值构造函数.on
// A simple reference counted smart pointer.
// make use of GCC atomic builtins and C++ move semantics
template<typename T>
class counted_ptr
{
typedef int* counted_ptr::*bool_type;
public:
counted_ptr(T* p = nullptr)
: ptr_(p),
count_(p ? new int(1) : nullptr) { }
counted_ptr(const counted_ptr& rhs) noexcept
: ptr_(rhs.ptr_),
count_(rhs.count_)
{
if (count_)
__atomic_fetch_add(count_, 1, __ATOMIC_SEQ_CST);//原子操作add
}
//右值引用简化深拷贝https://zhuanlan.zhihu.com/p/97128024
counted_ptr(counted_ptr&& rhs) noexcept
: ptr_(rhs.ptr_),
count_(rhs.count_)
{
rhs.ptr_ = nullptr;
rhs.count_ = nullptr;
}
~counted_ptr()
{
//退出子函数则调用析构
reset();
}
counted_ptr& operator=(counted_ptr rhs)
{
swap(rhs);
return *this;
}
T* get() const noexcept
{
return ptr_;
}
void reset()
{
static_assert(sizeof(T) > 0, "T must be complete type");
if (count_)
{
//原子操作;引用计数为0,则释放内存.如果引用计数不为0,则仅仅将当前的指针设置为null.内存中的东东并不释放.
if (__atomic_sub_fetch(count_, 1, __ATOMIC_SEQ_CST) == 0)
{
delete ptr_; //释放
delete count_;
}
ptr_ = nullptr;
count_ = nullptr;
}
}
void swap(counted_ptr& rhs) noexcept
{
T* tp = ptr_;
ptr_ = rhs.ptr_;
/*tp是临时变量,所以退出swap函数的时候rhs.ptr_变成指向Null.提高对rhs中指针的拷贝效率.copy-on-write*/
rhs.ptr_ = tp;
int* tc = count_;
count_ = rhs.count_;
rhs.count_ = tc;
}
T* operator->() const noexcept
{
// assert(ptr_);
return ptr_;
}
T& operator*() const noexcept
{
// assert(ptr_);
return *ptr_;
}
operator bool_type() const noexcept
{
return ptr_ ? &counted_ptr::count_ : nullptr;
}
int use_count() const noexcept
{
return count_ ? __atomic_load_n(count_, __ATOMIC_SEQ_CST) : 0;
}
private:
T* ptr_;
int* count_;
};
多线程没有引用计数。而是多线程中的指针有引用计数,计数与线程回收没关系,与内存回收有关。
4 线程池
一个线程放数据
g_async_queue_push(handle->queued_packets, pkt);
//典型的将对象放入线程池,线程池分配一个线程处理对象的queue
g_thread_pool_push(ice_send_thread_pool, handle, NULL);
一个线程发送数据
janus_ice_queued_packet *pkt = g_async_queue_try_pop(handle->queued_packets);
while (pkt != NULL) {
int32_t error = janus_send_data(pkt, handle);
pkt = g_async_queue_try_pop(handle->queued_packets);
}
下面的link是线程池的实现。android线程池和c++的不太一样,没有调度,resume这些操作,这些都封装好了。而是介绍线程池的几种使用模型. 线程池ThreadPool 全面解析 https://www.jianshu.com/p/0e4a5e70bf0e;
https://bugstack.blog.csdn.net/article/details/110946298
5 多线程实例
1)多线程参数
a) 传整数.注意是把整数当成地址传递.
例如: int i=5 ;pthread_create(&m_threadId[i],NULL,m_cbf,(void *)i).
传递原理: 因为地址可能无效,所以编译的时候会有一条warning.这是一个tricky,把数当成地址传.如(void *) 5表示传地址5.
pthread_create取值:执行函数是参数3. 执行函数不是取地址5中的值,而且将地址5强制转换成整数. 把地址当成数使用,按整数读取func(__args ) { k=(int)__args;} 即k=5;
b) 为什么不直接传整数的地址
int *pi=5; 传pi不就完了.
why thread 采用这种形式传参数4呢,而不是存数值5所在地址呢?
因为主线程传的是i的地址,所以每个线程从该地址取值。一定是最后那个5(前面的被冲掉了)
for (i=0;i<10;i++) {
p=&i;
pthread_create(&m_threadId[i],NULL,m_cbf,p);
}
子线程读取k=(int )q; run的结果是主传0-5,结果全是5.是同一个变量的同一个地址。而(void )i,是传的地址0,1,2,3,4,5是传的不同的地址。
2) 向线程参数是传对象
a) c++需要传对象引用,c语言需要传struct地址,否则是传值拷贝.
b) 线程函数是回调函数,所以必须是static 或者全局函数.
对于c语言来说,只有上述两种函数. 对于c++ 创建线程时,线程函数如果要设置成类的成员函数,则必须是静态成员函数,在此函数种不能使用非静态成员变量,如果要使用非静态*成员变量,则一种比较适合线程的方法是:建立线程的时候把this指针传进去.
5.1 c语言实现
void *wait(void *t)
{
long tid;
tid = (long)t;
...
pthread_exit(NULL);
}
rc = pthread_create(&threads[i], NULL, wait, (void *)&i );
rc = pthread_join(threads[i], &status);
5.2 c++实现
c++的线程是全局函数std:thread,直接向thread 传执行函数和参数。中的对象和外面的对象不一样.
bar1,bar2,bar3为自定义函数.bar1是成员非静态函数,bar2,bar3是成员静态函数.
thread t1(&foo::bar1, &f, 5); //注意在调用非静态类成员函数时,需要加上实例变量。
thread t2(&foo::bar2, 4);
thread t2(&foo::bar3, &node); //传入引用
8 死锁的调试
死锁现象及分析方法 之一 Linux C https://www.jianshu.com/p/3efe896fbabc
linux下多线程死锁调试 https://www.jianshu.com/p/2af2a0ab8e08
参考:https://blog.csdn.net/fdsafwagdagadg6576/article/details/43925029
** other:自旋锁**
自旋锁:单cpu 切换出来,另一个线程就是空转。没意义。---放入lock or 多线程blog
所以用于多cpu:一个访问临界区,另一个空转等待。
详见: https://www.jianshu.com/p/4b097aac2c9d
wait,notify c语言是进程通信。在c++和java变成线程通信
notify,wait pthread_cond_signal,pthread_cond_wait没发现不同
网友评论