进程ID和线程ID
在Linux中,目前的线程实现是 Native POSIX Thread Libaray,简称NPTL。在这种实现下,线程⼜被称为轻量级进程(Light Weighted Process),每⼀个⽤户态的线程,在内核中都对应⼀个调度实体,也拥有⾃⼰的进程描述符 (task_struct结构体)。
没有线程之前,⼀个进程对应内核⾥的⼀个进程描述符,对应⼀个进程 ID。但是引⼊线程概念之后,情况发⽣了变化,⼀个⽤户进程下管辖 N个⽤户态线程,每个线程作为⼀个独⽴的调度实体在内核态都有⾃⼰的进程描述符,进程和内核的描述符⼀下⼦就变成了 1:N关系,POSIX标准⼜要求进程内的所有线程调⽤ getpid函数时返回相同的进程 ID,所以Linux内核引⼊了线程组的概念。
struct task_struct {
...
pid_t pid;
pid_t tgid;
...
struct task_struct *group_leader;
...
struct list_head thread_group;
...
};
多线程的进程,⼜被称为线程组,线程组内的每⼀个线程在内核之中都存在⼀个进程描述符(task_struct)与之对应。进程描述符结构体中的pid,表⾯上看对应的是进程 ID,其实不然,它对应的是线程ID;进程描述符中的tgid,含义是Thread Group ID,该值对应的是⽤户层⾯的进程 ID。
Linux提供了gettid系统调⽤来返回其线程 ID,可是glibc并没有将该系统调⽤封装起来,在开放接⼝来共程序员使⽤。如果确实需要获得线程 ID,可以采⽤如下⽅法:
#include <sys/syscall.h>
pid_t tid;
tid = syscall(SYS_gettid);
线程组内的第⼀个线程,在⽤户态被称为主线程 (main thread),在内核中被称为group leader,内核在创建第⼀个线程时,会将线程组的 ID的值设置成第⼀个线程的线程 ID,group_leader指针则指向⾃⾝,既主线程的进程描述符。所以线程组内存在⼀个线程 ID等于进程ID,⽽该线程即为线程组的主线程。
/* 线程组ID等于线程ID,group_leader指向⾃⾝ */
p->tgid = p->pid;
p->group_leader = p;
INIT_LIST_HEAD(&p->thread_group);
⾄于线程组其他线程的 ID则有内核负责分配,其线程组 ID总是和主线程的线程组 ID⼀致,⽆论是主线程直接创建线程,还是创建出来的线程再次创建线程,都是这样。
if ( clone_flags & CLONE_THREAD )
p->tgid = current->tgid;
if ( clone_flags & CLONE_THREAD ) {
P->group_lead = current->group_leader;
list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);
}
线程和进程不⼀样,进程有⽗进程的概念,但在线程组⾥⾯,所有的线程都是对等关系。
线程ID及进程地址空间布局
pthread_ create函数会产⽣⼀个线程 ID,存放在第⼀个参数指向的地址中。该线程 ID和前⾯说的线程ID不是⼀回事。
前⾯讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最⼩单位,所以需要⼀个数值来唯⼀表⽰该线程。
pthread_ create函数产⽣并标记在第⼀个参数指向的地址中的线程 ID中,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程 ID来操作线程的。
线程库NPTL提供了pthread_ self函数,可以获得线程⾃⾝的 ID:
pthread_t pthread_self(void);
线程同步与互斥
mutex (互斥量)
⼤部分情况,线程使⽤的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程⽆法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
多个线程并发的操作共享变量,会带来⼀些问题。
// 操作共享变量会有问题的售票系统代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;
void *route(void *arg)
{
char *id = (char*)arg;
while ( 1 ) {
if ( ticket > 0 ) {
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
} else {
break;
}
}
}
int main( void )
{
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, route, "thread 1");
pthread_create(&t2, NULL, route, "thread 2");
pthread_create(&t3, NULL, route, "thread 3");
pthread_create(&t4, NULL, route, "thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
}
一次执行结果:
为什么可能⽆法获得争取结果?
if语句判断条件为真以后,代码可以并发的切换到其他线程;
usleep这个模拟漫⻓业务的过程,在这个漫⻓的业务过程中,可能有很多个线程会进⼊该代码段;
–ticket操作本⾝就不是⼀个原⼦操作。
取出ticket--部分的汇编代码
objdump -d a.out > test.objdump
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34
<ticket>
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 600b34
<ticket>
操作并不是原⼦操作,⽽是对应三条汇编指令:
load:将共享变量ticket从内存加载到寄存器中
update:更新寄存器⾥⾯的值,执⾏ -1操作
store:将新值,从寄存器写回共享变量 ticket的内存地址
要解决以上问题,需要做到三点:
代码必须要有互斥⾏为:当代码进⼊临界区执⾏时,不允许其他线程进⼊该临界区。
如果多个线程同时要求执⾏临界区的代码,并且临界区没有线程在执⾏,那么只能允许⼀个线程进⼊该临界区。
如果线程不在临界区中执⾏,那么该线程不能阻⽌其他线程进⼊临界区。
要做到这三点,本质上就是需要⼀把锁。 Linux上提供的这把锁叫互斥量。
互斥量的接⼝
初始化互斥量
⽅法1,静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
⽅法2,动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr
_t *restrict attr);
参数:
mutex:要初始化的互斥量
attr:NULL
销毁互斥量
销毁互斥量需要注意:
使⽤PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁;
不要销毁⼀个已经加锁的互斥量;
已经销毁的互斥量,要确保后⾯不会有线程再尝试加锁。
int pthread_mutex_destroy(pthread_mutex_t *mutex);
互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
调⽤pthread_ lock时,可能会遇到以下情况 :
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
发起函数调⽤时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调⽤会陷⼊阻塞,等待互斥量解锁。
改进上⾯的售票系统 :
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>
int ticket = 100;
pthread_mutex_t mutex;
void *route(void *arg)
{
char *id = (char*)arg;
while ( 1 ) {
pthread_mutex_lock(&mutex);
if ( ticket > 0 ) {
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
pthread_mutex_unlock(&mutex);
// sched_yield(); 放弃CPU
} else {
pthread_mutex_unlock(&mutex);
break;
}
}
}
int main( void )
{
pthread_t t1, t2, t3, t4;
pthread_mutex_init(&mutex, NULL);
pthread_create(&t1, NULL, route, "thread 1");
pthread_create(&t2, NULL, route, "thread 2");
pthread_create(&t3, NULL, route, "thread 3");
pthread_create(&t4, NULL, route, "thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
pthread_mutex_destroy(&mutex);
}
条件变量
当⼀个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
例如⼀个线程访问队列时,发现队列为空,它只能等待,只到其它线程将⼀个节点添加到队列中。这种情况就需要⽤到条件变量。
条件变量函数:
初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *rest
rict attr);
参数:
cond:要初始化的条件变量
attr:NULL
销毁
int pthread_cond_destroy(pthread_cond_t *cond);
等待条件满⾜
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mute
x);
参数:
cond:要在这个条件变量上等待
mutex:互斥量,后⾯详细解释
唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
为什么pthread_ cond_ wait 需要互斥量?
条件等待是线程间同步的⼀种⼿段,如果只有⼀个线程,条件不满⾜,⼀直等下去都不会满⾜,所以必须要有⼀个线程通过某些操作,改变共享变量,使原先不满⾜的条件变得满⾜,并且友好的通知等待在条件变量上的线程。
条件不会⽆缘⽆故的突然变得满⾜了,必然会牵扯到共享数据的变化。所以⼀定要⽤互斥锁来保护。没有互斥锁就⽆法安全的获取和修改共享数据。
// 错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false) {
pthread_mutex_unlock(&mutex);
//解锁之后,等待之前,条件可能已经满⾜,信号已经发出,但是该信号可能被错过
pthread_cond_wait(&cond);
pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);
由于解锁和等待不是原⼦操作。调⽤解锁之后, pthread_ cond_ wait之前,如果已经有其他线程获取到互斥量,摒弃条件满⾜,发送了信号,那么 pthread_ cond_ wait将错过这个信号,可能会导致
线程永远阻塞在这个 pthread_ cond_ wait。所以解锁和等待必须是⼀个原⼦操作。
int pthread_ cond_ wait(pthread_ cond_ t cond,pthread_ mutex_ t mutex);进⼊该函数后,会去看条件量等于0不?等于,就把互斥量变成 1,直到cond_ wait返回,把条件量改成 1,把互斥量恢复成原样。
条件变量使⽤规范
等待条件代码
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
给条件发送信号代码
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);
⽣产者消费者模型
代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#define CONSUMERS_COUNT 2
#define PRODUCERS_COUNT 2
struct msg{
struct msg *next;
int num;
};
struct msg *head = NULL;
pthread_cond_t cond;
pthread_mutex_t mutex;
pthread_t threads[CONSUMERS_COUNT+PRODUCERS_COUNT];
void *consumer(void *p)
{
int num = *(int*)p;
free(p);
struct msg *mp;
for ( ; ; ) {
pthread_mutex_lock(&mutex);
while ( head == NULL ) {
printf("%d begin wait a condition...\n", num);
pthread_cond_wait(&cond, &mutex);
}
printf("%d end wait a condition...\n", num);
printf("%d begin consume product...\n", num);
mp = head;
head = mp->next;
pthread_mutex_unlock(&mutex);
printf("Consume %d\n", mp->num);
free(mp);
printf("%d end consume product...\n", num);
sleep(rand()%5);
}
}
void *producer(void *p)
{
struct msg *mp;
int num = *(int*)p;
free(p);
for ( ; ; ) {
printf("%d begin produce product...\n", num);
mp = (struct msg*)malloc(sizeof(struct msg));
mp->num = rand()%1000 + 1;
printf("produce %d\n", mp->num);
pthread_mutex_lock(&mutex);
mp->next = head;
head = mp;
printf("%d end produce product...\n", num);
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
sleep(rand()%5);
}
}
int main( void )
{
srand(time(NULL));
pthread_cond_init(&cond, NULL);
pthread_mutex_init(&mutex, NULL);
int i;
for(i=0; i<CONSUMERS_COUNT; i++) {
int *p = (int*)malloc(sizeof(int));
*p = i;
pthread_create(&threads[i], NULL, consumer, (void*)p);
}
for(i=0; i<PRODUCERS_COUNT; i++) {
int *p = (int*)malloc(sizeof(int));
*p = i;
pthread_create(&threads[CONSUMERS_COUNT+i], NULL, producer, (void*)p);
}
for (i=0; i<CONSUMERS_COUNT+PRODUCERS_COUNT; i++)
pthread_join(threads[i], NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
}
POSIX信号量
POSIX信号量和SystemV信号量作⽤相同,都是⽤于同步操作,达到⽆冲突的访问共享资源目的。 但POSIX可以⽤于线程间同步。
初始化信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表⽰线程间共享,⾮零表⽰进程间共享
value:信号量初始值
销毁信号量
int sem_destroy(sem_t *sem);
等待信号量
功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem);
发布信号量
功能:发布信号量,表⽰资源使⽤完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem);
上节⽣产者-消费者的例⼦是基于链表的 ,其空间可以动态分配,现在基于固定⼤⼩的环形队列重写这个程序(POSIX信号量):
#include <unistd.h>
#include <sys/types.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
#define CONSUMERS_COUNT 1
#define PRODUCERS_COUNT 1
#define BUFFSIZE 10
int g_buffer[BUFFSIZE];
unsigned short in = 0;
unsigned short out = 0;
unsigned short produce_id = 0;
unsigned short consume_id = 0;
sem_t g_sem_full;
sem_t g_sem_empty;
pthread_mutex_t g_mutex;
pthread_t g_thread[CONSUMERS_COUNT+PRODUCERS_COUNT];
void* consume(void *arg)
{
int i;
int num = *(int*)arg;
free(arg);
while (1)
{
printf("%d wait buffer not empty\n", num);
sem_wait(&g_sem_empty);
pthread_mutex_lock(&g_mutex);
for (i=0; i<BUFFSIZE; i++)
{
printf("%02d ", i);
if (g_buffer[i] == -1)
printf("%s", "null");
else
printf("%d", g_buffer[i]);
if (i == out)
printf("\t<--consume");
printf("\n");
}
consume_id = g_buffer[out];
printf("%d begin consume product %d\n", num, consume_id);
g_buffer[out] = -1;
out = (out + 1) % BUFFSIZE;
printf("%d end consume product %d\n", num, consume_id);
pthread_mutex_unlock(&g_mutex);
sem_post(&g_sem_full);
sleep(1);
}
return NULL;
}
void* produce(void *arg)
{
int i;
int num = *(int*)arg;
free(arg);
while (1)
{
printf("%d wait buffer not full\n", num);
sem_wait(&g_sem_full);
pthread_mutex_lock(&g_mutex);
for (i=0; i<BUFFSIZE; i++)
{
printf("%02d ", i);
if (g_buffer[i] == -1)
printf("%s", "null");
else
printf("%d", g_buffer[i]);
if (i == in)
printf("\t<--produce");
printf("\n");
}
printf("%d begin produce product %d\n", num, produce_id);
g_buffer[in] = produce_id;
in = (in + 1) % BUFFSIZE;
printf("%d end produce product %d\n", num, produce_id++);
pthread_mutex_unlock(&g_mutex);
sem_post(&g_sem_empty);
sleep(5);
}
return NULL;
}
int main(void)
{
int i;
for (i=0; i<BUFFSIZE; i++)
g_buffer[i] = -1;
sem_init(&g_sem_full, 0, BUFFSIZE);
sem_init(&g_sem_empty, 0, 0);
pthread_mutex_init(&g_mutex, NULL);
for (i=0; i<CONSUMERS_COUNT; i++) {
int *p = (int*)malloc(sizeof(int));
*p = i;
pthread_create(&g_thread[i], NULL, consume, (void*)p);
}
for (i=0; i<PRODUCERS_COUNT; i++) {
int *p = (int*)malloc(sizeof(int));
*p = i;
pthread_create(&g_thread[CONSUMERS_COUNT+i], NULL, produce, (void*)p);
}
for (i=0; i<CONSUMERS_COUNT+PRODUCERS_COUNT; i++)
pthread_join(g_thread[i], NULL);
sem_destroy(&g_sem_full);
sem_destroy(&g_sem_empty);
pthread_mutex_destroy(&g_mutex);
return 0;
}
读写锁
在编写多线程的时候,有⼀种情况是⼗分常⻅的。那就是,有些公共数据修改的机会⽐较少。相⽐较改写,它们读的机会反⽽⾼的多。通常⽽⾔,在读的过程中,往往伴随着查找的操作,中间耗时很⻓。给这种代码段加锁,会极⼤地降低我们程序的效率。所以引入了读写锁,读写锁本质上是⼀种⾃旋锁(⻓时间等⼈和短时间等⼈的例⼦)。
读写锁接⼝
初始化
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr
_t *restrict attr);
销毁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
加锁和解锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
读写锁案例:
//创建5个读者线程,3个写者线程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
int counter;
pthread_rwlock_t rwlock;
void *route_write(void *arg)
{
int t;
int i = *(int*)arg;
free(arg);
while ( 1 ) {
t = counter;
usleep(1000);
pthread_rwlock_wrlock(&rwlock);
printf("write:%d:%#X: counter=%d ++counter=%d\n", i, pthread_self(), t, ++counter);
pthread_rwlock_unlock(&rwlock);
usleep(5000);
}
}
void *route_read(void *arg)
{
int t;
int i = *(int*)arg;
free(arg);
while ( 1 ) {
pthread_rwlock_rdlock(&rwlock);
printf("read :%d:%#X: counter=%d\n", i, pthread_self(), counter);
pthread_rwlock_unlock(&rwlock);
usleep(900);
}
}
int main( void )
{
int i;
pthread_t tid[8];
pthread_rwlock_init(&rwlock, NULL);
for (i=0; i<3; i++) {
int *p = (int*)malloc(sizeof(int));
*p = i;
pthread_create(&tid[i], NULL, route_write, (void*)p);
}
for (i=0; i<5; i++) {
int *p = (int*)malloc(sizeof(int));
*p = i;
pthread_create(&tid[i+3], NULL, route_read, (void*)p);
}
for (i=0; i<8; i++)
pthread_join(tid[i], NULL);
pthread_rwlock_destroy(&rwlock);
}