线程的tid不像进程,那不是他真正的id,也不是内核的lwp,而是由pthread库维护的一个唯一值
给用户提供的线程ID,不是内核中的lwp,而是pthread库维护的一个唯一值
库内部也要承担对线程的管理
#include<stdio.h>
#include<pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
void ToHex(pthread_t tid,char*buffer){
snprintf(buffer,128,"0x%lx",(unsigned long)tid);
}
void* gothread(void* arg){
while(1){
char buffer[128];
ToHex(pthread_self,buffer);
printf("arg %s is running\n",buffer);
sleep(1);
}
}
int main(){
pthread_t tid;
pthread_create(&tid,NULL,gothread,(void*)"thread-1");
char buffer[128];
ToHex(tid,buffer);
printf("new thread tid:%s\n",buffer);
pthread_join(tid,NULL);
return 0;
}
哦,这里报的错是说我的pthread_t的类型可能不匹配,我传回的是pthread_t类型,但是在打印的时候不兼容,可能会被识别为指针对象
当然,也能正常运行
#include<stdio.h>
#include<pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
void ToHex(unsigned long tid,char*buffer){
snprintf(buffer,128,"0x%lx",( unsigned long)tid);
}
void* gothread(void* arg){
while(1){
char buffer[128];
ToHex((unsigned long)pthread_self,buffer);
printf("arg %s is running\n",buffer);
sleep(1);
}
}
int main(){
pthread_t tid;
pthread_create(&tid,NULL,gothread,(void*)"thread-1");
char buffer[128];
ToHex((unsigned long)tid,buffer);
printf("new thread tid:%s\n",buffer);
pthread_join(tid,NULL);
return 0;
}
tid在这里就是一个地址
ls /lib/x86_64-linux-gnu/libpthread.so.0 -l
这是Linux系统下的一个线程库
pthread库的本质是一个文件,我们创建进程的时候,本质上是把线程库加载到内存,映射到进程(也就是真实的地址空间)
那么库是如何对线程进行管理的?
就像操作系统对进程的管理一样,struct pthread里存储的是线程在用户级的最基本的属性,线程栈是用户级别的独立栈结构
库对线程进行先描述再组织
在库中创建描述线程的相关结构体字段属性,管理的时候只需要找到对应的线程控制块的地址就可以了
所以Linux下的线程=pthread库中的属性集+LWP
操作系统没有线程,那它势必就要为我们提供LWP的系统调用
大概就像这样:
#define _GNU_SOURCE
#include <sched.h>
int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
/* pid_t *parent_tid, void *tls, pid_t *child_tid */ );
clone来创建线程(也能创建进程)
封装一个自己的线程库
满屏警告呃呃
把比较严重的处理了一下
#include<stdio.h>
#include<pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define SIZE 10
//创建线程属性的结构体
typedef struct{
pthread_t tid;
char* name[128];
int running;
}Thread;
//线程执行的函数
void gopthread(void* args){
Thread* thread=(Thread*)args;
while(thread->running){
printf("%s is running\n", thread->name);
sleep(1);
}
return NULL;
}
//创建
void mypthread_Create(Thread *threads){
threads->running=1;
pthread_create(&threads->tid,NULL,gopthread,(void*)threads);//把参数作为结构体传入
}
//停止
void mypthread_Stop(Thread *threads){
threads->running=0;
}
//等待
void mypthread_Join(Thread *threads){
pthread_join(threads->tid,NULL);
}
int main(){
Thread threads[SIZE];
//创建线程
for(int i=0;i<SIZE;i++){
snprintf(threads[i].name,sizeof(threads[i].name),"thread-%d",i+1);
mypthread_Create(&threads[i]);
}
sleep(5);//留给线程运行
//停止线程
for(int i=0;i<SIZE;i++){
mypthread_Stop(&threads[i]);
}
//等待
for(int i=0;i<SIZE;i++){
mypthread_Join(&threads[i]);
}
return 0;
}
线程互斥
我们用线程来模拟一个抢票的过程,在票只有十张的时候,创建三个线程同时抢票
#include<stdio.h>
#include<pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int tickets=10;
void* gopthread(void* args){
char* name=(char*)args;
while(tickets>0){
tickets--;
printf("%s pthread pay a ticket,remain %d\n",name,tickets);
sleep(1);
}
pthread_exit(args);
return args;
}
int main(){
char names[10][128];
pthread_t tids[10];
for(int i=0;i<3;i++){
snprintf(names[i],sizeof(names[i]),"thread-%d",i);
pthread_create(&tids[i], NULL,gopthread,names[i]);
}
for(int i=0;i<3;i++){
void* name=NULL;
pthread_join(tids[i],&name);
printf("%s quit...\n",(char*)name);
}
return 0;
}
我模拟了很多次,也没有出现线程互斥的现象哈。。但是这是概率事件,我没执行出来
可能会出现进入线程执行的函数内的时候,票有余量;但是其他线程在此时突然进行了票的--,就会出现互斥现象
我们之前提到过信号量是原子的,而票的--并不是原子性的,本质上其实并不是原子性的;转成汇编后执行重读数据--->数据--->写回数据
load :将共享变量ticket从内存加载到寄存器中
update : 更新寄存器里面的值,执行-1操作
store :将新值,从寄存器写回共享变量ticket的内存地址
所以我们需要进行互斥行为,来防止数据竞争
并且在高并发的时候执行临界区代码,而临界区没有线程在执行,那么只能有一个线程进入临界区
继续偷励志轩的图
锁
补充一下不全的man:
sudo apt-get install glibc-doc
#include <pthread.h>
pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t recmutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;
pthread_mutex_t errchkmutex = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP;
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
锁包含互斥量,锁包含很多种:
互斥锁(Mutex):确保同一时刻只有一个线程访问共享资源。
读写锁(Read-Write Lock):允许多个线程同时读,但写操作互斥。
自旋锁(Spin Lock):线程在等待锁时不断循环检查,而不进入休眠。
递归锁(Recursive Lock):允许同一个线程多次获取锁而不死锁。
使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
不要销毁一个已经加锁的互斥量
已经销毁的互斥量,要确保后面不会有线程再尝试加锁
也就是对临界资源、临界区的代码的保护
进入临界区之前要加锁,出了临界区要解锁;进锁的时候要并行改串行
并行:多个线程独立的执行
串行:多个线程对同一片临界资源操作,需要像卖票一样排队
先写一下吧:
#include<stdio.h>
#include<pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int tickets=10;
pthread_mutex_t gmutex=PTHREAD_MUTEX_INITIALIZER;
void* gopthread(void* args){
char* name=(char*)args;
while(1){
if(tickets>0){ pthread_mutex_lock(&gmutex);
tickets--;
printf("%s pthread pay a ticket,remain %d\n",name,tickets);
pthread_mutex_unlock(&gmutex);
}
else{
pthread_mutex_lock(&gmutex);//没票了,锁上
//pthread_exit(args);没必要,线程会自己退出
break;
}
sleep(1);
}
return args;
}
int main(){
char names[10][128];
pthread_t tids[10];
for(int i=0;i<10;i++){
snprintf(names[i],sizeof(names[i]),"thread-%d",i);
pthread_create(&tids[i], NULL,gopthread,names[i]);
}
for(int i=0;i<10;i++){
void* name=NULL;
pthread_join(tids[i],&name);
printf("%s quit...\n",(char*)name);
}
return 0;
}
此处为什么没有线程退出?
因为我的检查票数是否>0的语句放在了锁外,这意味着同时肯有很多线程访问我的票数是不是>0,如果A线程和B线程同时都发现票数==1,那么他们都进入减票数的if语句中,而A的tickets--之后,B处在--的语句里,但是没有票可--了,就会像我一样阻塞,也退不出去,这是线程竞态
而且应该在确定票数>0为假时候及时解锁,避免只锁不解锁的死锁状态
应该这样:
#include<stdio.h>
#include<pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int tickets=1000;
pthread_mutex_t gmutex=PTHREAD_MUTEX_INITIALIZER;
void* gopthread(void* args){
char* name=(char*)args;
while(1){
pthread_mutex_lock(&gmutex);//检查票数和买票的线程应该在同一个锁内
if(tickets>0){
tickets--;
printf("%s pthread pay a ticket,remain %d\n",name,tickets);
pthread_mutex_unlock(&gmutex);
}
else{
pthread_mutex_unlock(&gmutex);//没票了,锁上
break;
}
sleep(1);
}
return args;
}
int main(){
char names[10][128];
pthread_t tids[10];
for(int i=0;i<10;i++){
snprintf(names[i],sizeof(names[i]),"thread-%d",i);
pthread_create(&tids[i], NULL,gopthread,names[i]);
}
for(int i=0;i<10;i++){
void* name=NULL;
pthread_join(tids[i],&name);
printf("%s quit...\n",(char*)name);
}
return 0;
}
这样就正常了
所以锁的位置很重要,锁是保护临界资源的,线程阻塞的前提是看见锁
锁本身也是临界资源(好熟悉的话,你说对不对信号量?)
所以锁像信号量一样,本身是原子性的;区别是信号量本身可以让多个线程来访问临界资源,而锁就是为了锁上后只让一个线程访问
申请锁成功的线程即使被调度走了,只要锁没开,其他线程就不可以执行临界区的代码
所以访问临界区对其他线程是原子的
这个流程在第一个有锁的线程在还完了锁之后不能立马申请(要二次申请必须排队,其他线程也必须排队),也就是说在保证临界资源安全的情况下让访问顺序合理公平
这就是:线程同步!
可以是严格的顺序性,也可以是宏观上具有相对的顺序性
原理角度理解锁
如何理解申请锁成功就允许进入临界区?申请锁失败就不允许进入临界区
允许进入临界区就是申请锁成功,pthread_mutex_lock()函数会返回
不允许进入临界区就是申请锁失败,pthread_mutex_lock()函数不返回,线程就阻塞了(阻塞之后在pthread_mutex_lock内部被重新唤醒,重新申请锁)
一个CPU只有一套寄存器,被所有的线程共享,但是寄存器内部的数据是执行流(线程、进程这类)的数据属于执行流私有的数据
CPU在执行代码时,对应的载体是进程或线程,数据到了内存中就是共享的
把数据从内存移动到CPU寄存器中本质是把数据从共享变成线程私有
放一放我学协程时候拿go写的模拟抢票:
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var ticktes = 10
var mutex sync.Mutex //定义一个互斥锁的对象
var wg sync.WaitGroup //同步等待组对象
func main() {
wg.Add(4)
fmt.Println("陶吉吉演唱会,开始!")
go TicktesSaler("1号")
go TicktesSaler("2号")
go TicktesSaler("3号")
go TicktesSaler("4号")
wg.Wait()
//time.Sleep(1 * time.Second)
fmt.Println("All Done")
}
func TicktesSaler(name string) {
rand.Seed(time.Now().UnixNano())
defer wg.Done()
for {
mutex.Lock() //从这行开始上锁,只有一个goroutine可以执行
if ticktes > 0 {
time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
ticktes--
fmt.Printf("%sTicktesSold卖出,余量:%d\n", name, ticktes) //票数出现负数是因为1号判定完票数>0后进入if分支先睡觉
//睡觉的时候被别的goroutine抢占了资源,等睡醒的时候票已经为0了,但是因为已经进入>0的分支,所以只能继续执行,变成负数
} else {
mutex.Unlock() //解锁,输出售罄需要解锁
fmt.Printf("%s做不了自己了\n", name)
break
}
mutex.Unlock() //这样就不会有负数啦
}
}