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.
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 <pthread.h>
void fun_thread1(char *msg);
void fun_thread2(char *msg);
pthread_mutex_t mutex;
pthread_t thread1;
pthread_t thread2;
printf("Init mutex error. \n");
exit(1);
}
printf("Init thread1 fail. \n");
exit(1);
}
printf("Init thread2 fail. \n");
exit(1);
}
sleep(1);
}
int val;
val = pthread_mutex_lock(&mutex);
printf("lock error");
}
printf("thread 1 locked, inti the g_value to 0 and add 5. \n");
printf("the g_value is %d. \n", g_value);
printf("thread 1 unlocked. \n");
int val;
val = pthread_mutex_lock(&mutex);
printf("lock error");
}
printf("thread 2 locked, inti the g_value to 0 and add 19. \n");
printf("the g_value is %d. \n", g_value);
printf("thread 2 unlocked. \n");
}
死锁问题是由至少两个锁头也就是两个互斥量(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语言代码描述这一过程就是:
-
spinlock_t lock = SPIN_LOCK_UNLOCKED;
-
spin_lock(&lock);
-
/* 临界区 */
4. spin_unlock(&lock);
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.