multi-thread的使用

1. 有了多进程,为什么要多线程?

->许多应用中会同时发生多种活动,某些活动会随时间的推移而被阻塞,通过将其分解成多个顺序进程,程序设计模型会变得简单起来。多线程共享地址空间和所有可用数据的能力,是多进程无法做到的。

->线程是轻量级的,它更容易创建和删除

->在一些需要大量I/O处理和大量计算的情况下,拥有多线程允许这些活动彼此重叠进行,对程序性能提升是很明显的。

一般用在耗时或大量占用处理器的任务阻塞用户界面操作,或者多个任务必须等待外部资源

2. 线程创建,删除。

 -> 线程创建
int pthread_create(pthread_t *restrict tidp,
                   const pthread_attr_t *restrict attr,
                   void *(*start_rtn)(void),
                   void *restrict arg);

return 0 if OK.

第一个参数为指向线程标识符的指针。
第二个参数用来设置线程属性。
第三个参数是线程运行函数的起始地址。
最后一个参数是运行函数的参数。

->等待线程结束。

int pthread_join(pthread_t tid, void **status);

第一个参数为被等待的线程标识符

第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。

如果线程只是从它的启动例程返回,rval_ptr将包含返回码。如果线程被取消,由rval_ptr指定的内存单元被置为PTHREAH_CANCELED.

可以通过调用pthread_join自动把线程置于分离状态,这样资源就可以恢复。如果线程已经处于分离状态,pthread_join调用就会失败,返回EINVAL.
如果对线程的返回值并不感兴趣,可以把rval_ptr置为NULL。在这种情况下,调用pthread_join函数将等待指定的线程停止,但并不获取线程的终止状态。

3. 线程分离状态

在任何一个时间点上,线程是可结合的(joinable),或者是分离的(detached)。一个可结合的线程能够被其他线程收回其资源和杀死;在被其他线程回收之前,它的存储器资源(如栈)是不释放的。相反,一个分离的线程是不能被其他线程回收或杀死的,它的存储器资源在它终止时由系统自动释放。

        线程的分离状态决定一个线程以什么样的方式来终止自己。在默认情况下线程是非分离状态的,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。而分离线程不是这样子的,它没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。程序员应该根据自己的需要,选择适当的分离状态。所以如果我们在创建线程时就知道不需要了解线程的终止状态,则可以pthread_attr_t结构中的detachstate线程属性,让线程以分离状态启动。

设置线程分离状态的函数为pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)。第二个参数可选为PTHREAD_CREATE_DETACHED(分离线程)和 PTHREAD _CREATE_JOINABLE(非分离线程)。这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timewait函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。

3. 线程同步。 

a.互斥量(mutex)

互斥量是一把锁,该锁保护一个或一些共享资源。一个线程如果需要访问该资源,必须要获得互斥量并加锁。如果这时候其他线程如果想访问该资源也必须要获得该互斥量,但是锁已经加锁,所以这些进程只能阻塞,知道获得该锁的线程解锁。

 初始化: int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutex att_t *mutexattr)

互斥操作函数:

上锁:int pthread_mutex_lock(pthread_mutex_t *mutex)// 对互斥锁上锁,若互斥锁已经上锁,则调用者一直阻塞,直到互斥锁解锁后再上锁。

解锁:int pthread_mutex_unlock(pthread_mutex_t *mutex)

尝试着上锁:int pthread_mutex_trylock(pthread_mutex_t *mutex) // 调用该函数时,若互斥锁未加锁,则上锁,返回 0;若互斥锁已加锁,则函数直接返回失败,即 EBUSY。

清除互斥锁:int pthread_mutex_destroy(pthread_mutex_t *mutex)

#include <stdio.h>
#include <pthread.h>

void fun_thread1(char *msg);
void fun_thread2(char *msg);
int g_value = 1;
pthread_mutex_t mutex;
int main(int argc, char *argv[]){
 pthread_t thread1;
 pthread_t thread2;
 if (pthread_mutex_init(&mutex, NULL) !=0){
  printf("Init mutex error. \n");
  exit(1);
 }
 if (pthread_create(&thread1, NULL, (void *)fun_thread1, NULL) != 0){
  printf("Init thread1 fail. \n");
  exit(1);
 }
 if (pthread_create(&thread2, NULL, (void *)fun_thread2, NULL) != 0){
  printf("Init thread2 fail. \n");
  exit(1);
 }

 sleep(1);
 printf("I am main thread, g_value is %d. \n", g_value);
 return 0;
}
void fun_thread1(char *msg){
 int val;
 val = pthread_mutex_lock(&mutex);
 if (val !=0){
  printf("lock error");
 }
 g_value = 0;
 printf("thread 1 locked, inti the g_value to 0 and add 5. \n");
 g_value += 5;
 printf("the g_value is %d. \n", g_value);
 pthread_mutex_unlock(&mutex);
 printf("thread 1  unlocked. \n");
}
void fun_thread2(char *msg){
 int val;
 val = pthread_mutex_lock(&mutex);
 if (val !=0){
  printf("lock error");
 }
 g_value = 0;
 printf("thread 2 locked, inti the g_value to 0 and add 19. \n");
 g_value += 19;
 printf("the g_value is %d. \n", g_value);
 pthread_mutex_unlock(&mutex);
 printf("thread 2  unlocked. \n");
}
 疑问: 互斥锁锁什么? 简单来说, 互斥锁用于限制同一时刻,其他线程执行pthread_mutex_lock 和unlock之间的指令

 死锁问题是由至少两个锁头也就是两个互斥量(mutex)才能产生。
  死锁案例:如有两互斥量lock1,lock2,且有两个线程A,B。
  ① 在线程A执行的时候,此线程先锁lock1并且成功了,这个时候准备去锁lock2…
  ② 此时,出现了上下文切换,线程B开始执行。这个线程先锁lock2,因为lock2还没有被锁,所以lock2会lock()成功。于是,线程B要去锁lock1…
  ③ 此时,线程A因为拿不到锁lock2,流程走不下去(虽然后面代码有unlock锁lock1的,但是流程走不下去,所以lock1解不开);同理,线程B因为拿不到锁lock1,流程走不下去(虽然后面代码有unlock锁lock2的,但是流程走不下去,所以lock2解不开)。这样,死锁就产生了。

互斥量中的条件变量

  互斥量和读写锁解决了多线程访问共享变量产生的竞争问题,那么条件变量的作用何在呢??
    条件变量的作用是用于多线程之间关于共享数据状态变化的通信。当一个动作需要另外一个动作完成时才能进行,即:当一个线程的行为依赖于另外一个线程对共享数据状态的改变时,这时候就可以使用条件变量
条件变量用来阻塞线程等待某个事件的发生,并且当等待的事件发生时,阻塞线程会被通知。
  互斥锁一个明显的缺点是它只有两种状态:锁定和非锁定。
        而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。
  使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。

b. 自旋锁

是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting

它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名

使用自旋锁是简单的,基本操作就是初始化锁、请求锁、释放锁,用C语言代码描述这一过程就是:

  1. spinlock_t lock = SPIN_LOCK_UNLOCKED;
  2. spin_lock(& lock);
  3. /* 临界区 */ 

  4. spin_unlock(&lock);

注意: 1. 自旋锁是不可递归的, 递推地请求同一个自旋锁会造成死锁。
2. 线程获取自旋锁之前,要禁止当前处理器上的中断, 防止获取锁的线程和终端形成竞争条件。
比如,当前线程获取自旋锁后,在临界区中被中断处理程序打断,中断处理程序正好也要获取这个锁, 于是中断处理程序会等待当前线程释放锁,而当前线程也在等待中断执行完后再执行临界区和释放锁的代码。

c. barrier

 线程屏障用来协同多线程一起工作。多线程各自做自己的工作,如果某一线程完成了工作,就等待在屏障那里,直到其他线程的工作都完成了,再一起做别的事情。

例如, 80000个数据分给8个线程去做,每个线程处理10000个数据,那么要等待所有的8个线程处理完之后,才可以在主线程中找到最大值。

step 1) pthread_barrier_init(&barrier, NULL, PTHREAD_BARRIER_SIZE(number of threads))

step 2) 在每一个被创建的thread里面(包括主线程), pthread_barrier_wait(&barrier), 等待其他线程做完

step 3) pthread_barrier_destroy(&barrier) 最后要销毁屏障。

4. 多核系统下,如何绑定每个线程到每个core

 void *threadfunc(void *arg)
{
    cpu_set_t mask;
    cpu_set_t mask;
    int cpuid = 1;
    CPU_ZERO(&mask);
    CPU_SET(cpuid, &mask);
    /* 设置cpu 亲和性(affinity)*/
    if (pthread_setaffinity_np(pthread_self(), sizeof(mask), &mask) < 0) {
        fprintf(stderr, "set thread affinity failed\n");
    }      
}

进阶: 如何让进程独占一个cpu?

需要在linux内核中设置启动参数isolcpus.

假如有4个cpu的服务器,在启动时候加入启动参数isolcpus = 2,3,那么系统启动不使用cpu3 和cpu4。 系统启动后,如果想启动,可以通过taskset命令指定哪些程序在哪些核心中运行。

设置启动参数步骤如下:

1. vim /boot/grub2.cfg 加入isolcpu=2,3

2.reboot服务器,cat /proc/cmdline, 看到isolcpus=2,3

5. 信号量

信号量的使用是用来保护共享资源,使得资源在一个时刻只有一个进程(线程)所拥有。当信号量的值为正的时候,说明它空闲,所测试的线程可以锁定而使用它。若为0说明它被占用,测试的线程要进入睡眠队列中,等待被唤醒。具体见我的另一篇blog.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值