1.线程的基本概念
多线程可以将计算密集型(实时)和I/O密集型(实际中会等待)应用分配到多个处理器上执行,提高执行效率,但是会增加调度成本。
Linux没有专门的TCB
(Thread Control Block),而是复用了进程的PCB
;不同的PCB
指向同一个地址空间,并将地址空间的数据段和代码段进行划分分配给不同的PCB
。
进程是资源分配的基本单位,线程是运算调度的基本单位。Linux线程创建时不用像进程那样创建PCB、地址空间、页表、构建物理内存的映射,而只需要创建一个PCB,并将进程的资源分配给线程。
线程的大部分资源是共享的,但是必须私有栈(临时数据等)、上下文标志(调度信息)。
用户级线程库:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void *mythread(void *arg)
{
while(1)
{
printf("new thread is running : arg val = %s; PID:%d\n", (char*)arg, getpid());
sleep(1);
}
}
int main()
{
pthread_t pid;
//注意makefile编写包含thread 库
pthread_create(&pid, NULL, mythread, (void*)"thread 1");
while(1)
{
printf("Hold thread; PID:%d\n",getpid());
sleep(1);
}
return 0;
}
main:main.c
gcc -o $@ $^ -lpthread
.PHONY:clean
clean:
rm -f main
使用ps -aL
查看轻量级进程(LWP
):
可以看到这两个线程具有相同的PID,但是LWP
的ID是不同的。Linux系统调度是根据LWP
执行的,在只有一个进程的情况下PID = LWP
。
另外多线程会降低代码健壮性(一个线程崩溃,产生的信号直接发给进程,造成进程崩溃)、缺乏访问控制、增加编程难度。
以下代码将演示线程的创建、等待(类似进程waitpid,回收系统资源)、线程终止、线程分离
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void* mythread(void* arg) //多个执行流共同执行,被重入的函数
{
//pthread_detach(pthread_self());
//线程分离,新线程不需要再等待了(不能pthread_join),业务处理完直接退出
while (1)
{
printf("thread handler : \n");
//打印线程号,这个ID号是线程库分配的(是一个虚拟内存地址,在栈和堆之间的共享区,里面存储着LWP),与OS内核的LWP号不同。
printf("new thread ID:%lu\n", pthread_self());
sleep(1);
break;
}
//三种方式终止线程 : 其中一种为pthread_cancel(),在m主线程中调用
//return (void*)("thread return\n");
pthread_exit((void*)("thread return\n")); //与exit不同,使用exit会直接终止整个进程
}
int main()
{
int i;
pthread_t pid[3];
void* status = NULL;
for (i = 0; i < 3; i++)
{
//创建线程
pthread_create(pid + i, NULL, mythread, (void*)"thread 0");
}
//pthread_cancel(tid[0]);//线程的退出码为-1, 其为宏定义:PTHREAD_CANCELED
//若取消主线程后不主动join回收,则会出现僵尸线程
//int pthread_join(pthread_t thread, void **retval); //线程等待,对于异常退出的线程进行等待,回收资源
pthread_join(pid[0], &status); //不会处理异常信号,异常由进程处理
printf("ret : %s\n", (char*)status);//获取线程退出的返回值
while (1)
{
printf("\n main thread: \n");
printf("---------------------------\n");
for (i = 0; i < 3; i++)
{
printf("线程号[%d]:", i);
printf("main thread ID:%lu; pthread_self: %lu\n", pid[i], pthread_self());
sleep(1);
}
printf("---------------------------\n");
}
return 0;
}
运行结果:
2.线程的互斥和同步
2.1 基本概念
- 临界资源:多线程执行流共享的资源
- 临界区:访问临界资源的代码
- 原子性:只有完成和未完成两种状态。
- 互斥:同一时间只能允许一个线程访问临界资源,可以加锁实现。加锁可以保证单个线程对临界资源的访问是原子的。
- 同步:让线程顺序访问临界资源,避免某个线程因抢占独占临界资源
- 线程安全:多线程执行同一代码,若执行结果是一致的就是线程安全的。
- 可重入:函数被多个执行流调用,其执行结果一致。可重入一定是线程安全的,线程安全不一定是可重入的。
2.2 互斥的实现
一段加锁的代码:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
int count = 0; //临界资源
pthread_mutex_t lock;// 锁也是临界资源,但是锁的申请是原子的
void *mythread(void *arg)
{
while(1)
{
pthread_mutex_lock(&lock);//线程加锁
//若在执行锁内的代码发生了线程切换,其他线程也无法访问临界资源,因为之前的线程是带锁的
if(count < 100)
{
printf("[%s]:count = %d\n",arg, count);
count++;
usleep(100);
}
else
{
pthread_mutex_unlock(&lock);//线程解锁
break;
}
pthread_mutex_unlock(&lock);//线程解锁
}
return (void*)("end\n");
}
int main()
{
int i;
pthread_t pid[3];
pthread_mutex_init(&lock,NULL);//初始化锁
pthread_create(pid, NULL, mythread, (void*)"thread 0");
pthread_create(pid+1, NULL, mythread, (void*)"thread 1");
pthread_create(pid+2, NULL, mythread, (void*)"thread 2");
for(i = 0; i < 3; i++)
{
pthread_join(pid[i],NULL);
}
pthread_mutex_destroy(&lock); //销毁锁
return 0;
}
加锁的理解:
mutex
(值为1)在内存中只保存了一份,当其中一个线程拿走后,内存中的mutex
会被置0;其他线程拿走0的mutex
后就会被挂起。CPU寄存器不是被所有线程共享的,他只保存了当前线程的上下文信息。mutex
与CPU寄存器的值交换是通过一条指令实现的,不存在中间过程,所以在一个时钟周期内,一个线程拿走mutex
后就不会被另一线程拿走了,因此加锁是原子的。
加锁解决了临界资源的访问问题,但是造成了性能损失,在一些情况下还会出现死锁使得程序卡死。
死锁的四个必要条件:
- 互斥:同一时间,一分资源只能被一个执行流访问
- 占有并等待:一个执行流占有一份资源(锁资源),即使自己阻塞了也不释放,使得另一个执行流无法申请该资源只能处于等待状态
- 无法抢占:执行流还未完成当前的任务,此资源无法被另一个执行流抢占
- 循环等待:对于资源的申请是无序的,每份资源被不同的执行流占有,每个执行流都在等待其他执行进行资源释放。
破坏以上任一条件都可避免死锁
2.3 同步的实现
当某个线程对锁的竞争能力较强,会出现该线程一直重复申请和占有资源,造成其他线程无法访问的情况。因此引入同步,让每个线程都能有效访问临界资源。
实现线程同步可以使用条件变量或者信号量。
条件变量实现同步:
#include <iostream>
#include <cstdlib>
#include <queue>
#include <pthread.h>
#include <ctime>
#include <unistd.h>
template<class T>
class blockchain
{
public:
blockchain(const int capacity = 64)
:_capacity(capacity)
{
pthread_mutex_init(&_lock, nullptr);
pthread_cond_init(&_condFull, nullptr);
pthread_cond_init(&_condEmpty, nullptr);
}
~blockchain()
{
pthread_cond_destroy(&_condFull);
pthread_cond_destroy(&_condEmpty);
pthread_mutex_destroy(&_lock);
}
bool queFull()
return _que.size() == _capacity;
bool queEmpty()
return _que.empty();
void Push(const T& x) //生产过程
{
pthread_mutex_lock(&_lock);
//1.需要循环检测条件就绪的状态,可以防止wait调用失败(应该挂起却没有挂起)后代码继续执行
//2.若生产者处于生产阶段,生产一个数据后向消费者发生一个signal唤醒消费,若无while,假设是if。
// if在进入后只判断了一次,之后即使条件满足仍然会跳出判断执行后面的代码,执行完消费后又发送了signal给
// 生产者,而生产者本就处于唤醒状态。不应唤醒的线程被唤醒了称为伪唤醒。
//3.在消费过程中,每一次消费完成都会发送一个signal通知生产者生产,
// 此时本因继续消费的,却又开始生产,所以使用while阻塞询问是否生产就绪,就绪了再执行生产的代码
while(queFull()) //缓冲区满了就要阻塞等待
{
pthread_cond_wait(&_condFull, &_lock);
//第二个参数作用:1.判断等待的时候,线程是拿着锁的资源挂起的,为了防止死锁,需要将_lock释放
//2.若该条件被唤醒,此函数会为线程自动竞争锁,原因:
//挂起时是在代码运行到此数被挂起的,寄存器会记录当前函数的上下文信息,被唤醒时继续在此处向后执行
//线程原来是拥有锁的资源的,因此在唤醒后要重新获取到锁资源,然后再继续执行。
}
_que.push(x);
pthread_cond_signal(&_condEmpty); //生产者线程唤醒消费者
pthread_mutex_unlock(&_lock);
}
void Pop(T& ret) //消费过程
{
pthread_mutex_lock(&_lock);
while(queEmpty()) //缓冲区空了就要阻塞等待
{
//若直接return,就相当于是消费者轮询,消费者一直在询问是否有数据
//若消费者竞争锁的能力较强,则会一直保有锁的资源,使得生产者无法生产
//因此当队列为空时,就要让消费者线程链接到等待队列,等待生产者生产
//生产者生产了一定数据后,可以向消费者线程发送signal,让消费者继续消费
//生产者部分同理。
pthread_cond_wait(&_condEmpty, &_lock);
}
ret = _que.front();
_que.pop();
pthread_cond_signal(&_condFull);
pthread_mutex_unlock(&_lock);
}
private:
std::queue<T> _que;
int _capacity;
pthread_mutex_t _lock;
pthread_cond_t _condFull; //队列为满标志,需要消费者消费
pthread_cond_t _condEmpty;//队列为空标志,需要生产者生产
};
void* ConsumerBuy(void *arg)
{
blockchain<int> *q = (blockchain<int>*)(arg);
while (1)
{
int out;
q->Pop(out);
std::cout << "Data Consumed:" << out << std::endl;
}
}
void* ProducerMake(void* arg)
{
blockchain<int> *q = (blockchain<int>*)(arg);
while (1)
{
int tmp = rand() % 100;
std::cout << "Data Produced:" << tmp << std::endl;
q->Push(tmp);
sleep(1);
}
}
int main()
{
srand((unsigned int)time(nullptr));
blockchain<int> *p = new blockchain<int>();
pthread_t consumer;
pthread_t producer;
pthread_create(&consumer, nullptr, ConsumerBuy, (void*)p);
pthread_create(&producer, nullptr, ProducerMake, (void*)p);
pthread_join(consumer, nullptr);
pthread_join(producer, nullptr);
}
以上代码是基于生产消费者模型,使用条件变量实现线程同步操作的例子。
补充:生产消费者模型
- 生产消费者模型是代码解耦的过程,使得一个任务可以并行执行,而本来的函数调用是强耦合,只能由同一线程顺序执行。而在生产消费者模型中,生产者生产一些数据后,将数据保存在一段缓冲区(临界资源)以供消费者获取,这样同一段代码可由不同的线程合作完成,提高了代码运行效率。
- 生产者和消费者都会对临界资源进行访问,因此需要加锁实现互斥。单就生产者或者单消费者来说,由于实现的任务是类似的,其存在对锁资源的竞争;而在生产者和消费者之间,为了保证生产者有空间可以存储生产的数据,消费者有资源进行消费,需满足一定的条件,可以用同步实现。
参考资料
[1]: 《Computer Systems:A Programmer’s Perspective》