问题

C 语言线程间怎么通信?

回答
在 C 语言中,让不同线程之间能够交流信息、协同工作,这本身就是多线程编程中最核心也是最需要仔细处理的部分。别把它想得太玄乎,无非就是大家共享一块内存,然后约定好怎么读写这块内存罢了。只不过,这“约定”怎么立得住,不让大家互相捣乱,才是关键。

咱们把线程通信这事儿,拆解成几个层面来说。

1. 共享内存:一切通信的基础

你想啊,每个线程都有自己的独立空间,它们怎么能知道对方在干啥?最直接的办法就是,让它们看看同一份数据。这就是所谓的“共享内存”。

你可以声明一些全局变量,或者在创建线程时传递一个指向某个结构体或数组的指针。所有线程都可以访问到这块内存区域。

举个简单的例子:

```c
include
include

// 共享变量
int shared_counter = 0;

// 线程函数
void thread_function(void arg) {
printf("Thread starting... ");
// 访问共享变量
shared_counter++;
printf("Shared counter after increment: %d ", shared_counter);
printf("Thread finishing... ");
return NULL;
}

int main() {
pthread_t tid1, tid2;

// 创建第一个线程
if (pthread_create(&tid1, NULL, thread_function, NULL) != 0) {
perror("Error creating thread 1");
return 1;
}

// 创建第二个线程
if (pthread_create(&tid2, NULL, thread_function, NULL) != 0) {
perror("Error creating thread 2");
return 1;
}

// 等待线程结束
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);

printf("Final shared counter value: %d ", shared_counter);

return 0;
}
```

在这段代码里,`shared_counter` 就是那个共享变量。两个线程都在尝试增加它。

2. 同步机制:告别混乱的“赛跑”

问题来了,你看看上面那个例子,`shared_counter` 最后的值是多少?是 2 吗?也可能不是。为什么?因为线程的执行顺序不是绝对确定的。

想象一下,两个线程同时看到了 `shared_counter` 是 0。

线程 1 读到 0,准备加 1。
线程 2 读到 0,也准备加 1。
线程 1 将结果 1 写回。
线程 2 将结果 1 写回。

结果就是,虽然两个线程都执行了加操作,但 `shared_counter` 最后却是 1,而不是正确的 2。这就像一群人在一个狭窄的门前挤来挤去,谁都不知道谁先挤过去,结果互相推搡,最后可能谁都没过去。这种现象叫做竞态条件 (Race Condition)。

为了避免这种混乱,我们需要引入同步机制,也就是给共享数据的访问加一把“锁”,保证在同一时间只有一个线程能够修改它。

2.1. 互斥量 (Mutex)

互斥量是最常用也是最基础的同步工具。你可以把它想象成一把钥匙,谁拿到了钥匙,谁就能进入“安全区域”(访问共享数据)。

`pthread_mutex_init()`: 初始化一个互斥量。
`pthread_mutex_lock()`: 尝试获取互斥量。如果互斥量已经被其他线程持有,当前线程会阻塞(暂停执行),直到互斥量被释放。
`pthread_mutex_unlock()`: 释放互斥量,允许其他等待的线程获取。
`pthread_mutex_destroy()`: 销毁互斥量,释放相关资源。

用互斥量修改上面的例子:

```c
include
include

// 共享变量
int shared_counter = 0;
// 互斥量
pthread_mutex_t counter_mutex;

// 线程函数
void thread_function(void arg) {
printf("Thread starting... ");

// 获取互斥量
pthread_mutex_lock(&counter_mutex);

// 访问共享变量(现在是安全的)
shared_counter++;
printf("Shared counter after increment: %d ", shared_counter);

// 释放互斥量
pthread_mutex_unlock(&counter_mutex);

printf("Thread finishing... ");
return NULL;
}

int main() {
pthread_t tid1, tid2;

// 初始化互斥量
if (pthread_mutex_init(&counter_mutex, NULL) != 0) {
perror("Error initializing mutex");
return 1;
}

// 创建第一个线程
if (pthread_create(&tid1, NULL, thread_function, NULL) != 0) {
perror("Error creating thread 1");
return 1;
}

// 创建第二个线程
if (pthread_create(&tid2, NULL, thread_function, NULL) != 0) {
perror("Error creating thread 2");
return 1;
}

// 等待线程结束
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);

// 销毁互斥量
pthread_mutex_destroy(&counter_mutex);

printf("Final shared counter value: %d ", shared_counter);

return 0;
}
```

现在,`shared_counter` 的值就稳定地是 2 了。哪个线程先获取到 `counter_mutex`,就先执行加操作,另一个线程就得等,直到第一个线程释放了锁。

2.2. 读写锁 (ReadWrite Lock)

互斥量虽然安全,但它有个缺点:同一时间只能一个线程访问,无论是读还是写。 如果你的共享数据经常被读取,很少被修改,那么用互斥量就会造成不必要的等待。

读写锁就是为了解决这个问题。它允许多个线程同时读取共享数据,但只允许一个线程写入。当有写操作发生时,读操作会被阻塞,反之亦然。

`pthread_rwlock_init()`: 初始化读写锁。
`pthread_rwlock_rdlock()`: 获取读锁定。
`pthread_rwlock_wrlock()`: 获取写锁定。
`pthread_rwlock_unlock()`: 释放读写锁(无论之前是读还是写)。
`pthread_rwlock_destroy()`: 销毁读写锁。

使用场景: 考虑一个新闻网站的后台数据库,很多人在同时浏览新闻(读操作),偶尔才有人发布新文章(写操作)。这时用读写锁就比互斥量效率高很多。

2.3. 条件变量 (Condition Variable)

条件变量不是用来保护共享数据的,而是用来协调线程的等待和通知。它通常和互斥量一起使用。

你可以把它想象成一个“等待室”,线程可以在这里睡觉,直到某个条件满足了,另一个线程才会叫醒它。

`pthread_cond_init()`: 初始化条件变量。
`pthread_cond_wait()`: 等待一个条件变量。它会原子地释放一个互斥量,然后让当前线程阻塞,直到该条件变量被 `pthread_cond_signal()` 或 `pthread_cond_broadcast()` 唤醒。醒来后,它会重新获取那个互斥量。
`pthread_cond_signal()`: 唤醒一个(任意一个)正在等待该条件变量的线程。
`pthread_cond_broadcast()`: 唤醒所有正在等待该条件变量的线程。
`pthread_cond_destroy()`: 销毁条件变量。

经典例子:生产者消费者模型

生产者线程负责生产数据,放入一个缓冲区。
消费者线程负责从缓冲区取出数据并消费。

问题:
1. 缓冲区满了,生产者不能再生产,需要等待。
2. 缓冲区空了,消费者不能再消费,需要等待。
3. 生产者生产了新数据,需要通知等待的消费者。
4. 消费者消费了数据,需要通知等待的生产者。

这里就需要条件变量了:

```c
include
include
include // for malloc, free, rand, srand
include // for sleep

define BUFFER_SIZE 5

// 共享缓冲区
int buffer[BUFFER_SIZE];
int count = 0; // 当前缓冲区中的元素数量
int in = 0; // 放入数据的索引
int out = 0; // 取出数据的索引

// 互斥量,保护对缓冲区的访问
pthread_mutex_t buffer_mutex;
// 条件变量:缓冲区非满(生产者等待)
pthread_cond_t not_full;
// 条件变量:缓冲区非空(消费者等待)
pthread_cond_t not_empty;

// 生产者函数
void producer(void arg) {
for (int i = 0; i < 10; i++) {
// 生产一个物品
int item = rand() % 100;

// 加锁保护缓冲区
pthread_mutex_lock(&buffer_mutex);

// 如果缓冲区满了,等待“缓冲区非满”条件
while (count == BUFFER_SIZE) {
printf("Producer: Buffer is full. Waiting... ");
pthread_cond_wait(¬_full, &buffer_mutex);
}

// 将物品放入缓冲区
buffer[in] = item;
in = (in + 1) % BUFFER_SIZE;
count++;
printf("Producer: Produced %d. Count: %d ", item, count);

// 通知等待的消费者,缓冲区非空了
pthread_cond_signal(¬_empty);

// 解锁
pthread_mutex_unlock(&buffer_mutex);

// 模拟生产耗时
sleep(rand() % 2);
}
return NULL;
}

// 消费者函数
void consumer(void arg) {
for (int i = 0; i < 10; i++) {
// 加锁保护缓冲区
pthread_mutex_lock(&buffer_mutex);

// 如果缓冲区空了,等待“缓冲区非空”条件
while (count == 0) {
printf("Consumer: Buffer is empty. Waiting... ");
pthread_cond_wait(¬_empty, &buffer_mutex);
}

// 从缓冲区取出物品
int item = buffer[out];
out = (out + 1) % BUFFER_SIZE;
count;
printf("Consumer: Consumed %d. Count: %d ", item, count);

// 通知等待的生产者,缓冲区非满了
pthread_cond_signal(¬_full);

// 解锁
pthread_mutex_unlock(&buffer_mutex);

// 模拟消费耗时
sleep(rand() % 3);
}
return NULL;
}

int main() {
pthread_t prod_tid, cons_tid;

// 初始化互斥量和条件变量
if (pthread_mutex_init(&buffer_mutex, NULL) != 0) {
perror("Error initializing mutex");
return 1;
}
if (pthread_cond_init(¬_full, NULL) != 0) {
perror("Error initializing not_full cond");
pthread_mutex_destroy(&buffer_mutex); // 清理已初始化的互斥量
return 1;
}
if (pthread_cond_init(¬_empty, NULL) != 0) {
perror("Error initializing not_empty cond");
pthread_mutex_destroy(&buffer_mutex);
pthread_cond_destroy(¬_full); // 清理已初始化的条件变量
return 1;
}

// 创建生产者线程
if (pthread_create(∏_tid, NULL, producer, NULL) != 0) {
perror("Error creating producer thread");
// 清理资源
pthread_mutex_destroy(&buffer_mutex);
pthread_cond_destroy(¬_full);
pthread_cond_destroy(¬_empty);
return 1;
}

// 创建消费者线程
if (pthread_create(&cons_tid, NULL, consumer, NULL) != 0) {
perror("Error creating consumer thread");
// 清理资源
pthread_join(prod_tid, NULL); // 先等生产者完成,避免问题
pthread_mutex_destroy(&buffer_mutex);
pthread_cond_destroy(¬_full);
pthread_cond_destroy(¬_empty);
return 1;
}

// 等待线程结束
pthread_join(prod_tid, NULL);
pthread_join(cons_tid, NULL);

// 销毁互斥量和条件变量
pthread_mutex_destroy(&buffer_mutex);
pthread_cond_destroy(¬_full);
pthread_cond_destroy(¬_empty);

printf("All threads finished. ");

return 0;
}
```

注意 `pthread_cond_wait` 的用法: 它接受两个参数:条件变量本身和互斥量。这个函数有两个关键操作:
1. 原子地释放传入的互斥量。
2. 让当前线程阻塞,直到条件被满足(被 `signal` 或 `broadcast`)。

当线程被唤醒后,它会尝试重新获取之前释放的互斥量。只有在成功获取互斥量后,`pthread_cond_wait` 函数才会返回。

为什么用 `while` 而不是 `if` 来检查条件?
这叫做“伪唤醒”(Spurious Wakeup)。在某些罕见的情况下,线程可能在没有实际满足条件时就被唤醒。所以,每次被唤醒后,都必须重新检查条件。用 `while` 循环就能确保条件真正满足了,才继续执行。

3. 原子操作

对于一些非常简单的操作,比如给一个整数加一,我们不仅要避免竞态条件,还希望操作本身是不可分割的,就像一个“原子弹”一样,要么就执行完,要么就不执行,不可能执行到一半被中断。

C 语言提供了 `stdatomic.h` 头文件,其中的函数可以实现原子操作。

`atomic_init()`: 初始化原子类型。
`atomic_fetch_add()`: 原子地将一个值加到原子变量上,并返回修改前的值。
`atomic_store()`: 原子地将一个值存储到原子变量。
`atomic_load()`: 原子地从原子变量加载值。

使用原子操作修改计数器例子:

```c
include
include
include // 引入原子操作头文件

// 使用原子类型定义共享变量
atomic_int shared_counter_atomic = ATOMIC_VAR_INIT(0);

// 线程函数
void thread_function_atomic(void arg) {
printf("Thread starting... ");

// 原子地增加共享变量
// atomic_fetch_add 返回修改前的值
int old_value = atomic_fetch_add(&shared_counter_atomic, 1);
printf("Shared counter after increment (atomic): %d (was %d) ", old_value + 1, old_value);

printf("Thread finishing... ");
return NULL;
}

int main() {
pthread_t tid1, tid2;

// 创建第一个线程
if (pthread_create(&tid1, NULL, thread_function_atomic, NULL) != 0) {
perror("Error creating thread 1");
return 1;
}

// 创建第二个线程
if (pthread_create(&tid2, NULL, thread_function_atomic, NULL) != 0) {
perror("Error creating thread 2");
return 1;
}

// 等待线程结束
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);

printf("Final shared counter value (atomic): %d ", atomic_load(&shared_counter_atomic));

return 0;
}
```

这种方式非常简洁高效,尤其适合于简单的计数、标志位设置等场景。通常情况下,原子操作比使用互斥量开销更小。

4. 消息队列 (Message Queues)

前面说的都是共享内存,直接操作内存数据。但有时候,我们更希望线程之间传递的是“消息”或者“数据包”,而不是直接操作同一块内存区域。

消息队列提供了一种异步的、解耦的通信方式。一个线程可以发送一条消息,而另一个线程可以接收这条消息,而不需要知道消息是从哪个线程发来的,也不需要关心消息的发送速度和接收速度是否一致。

Linux 系统提供了 POSIX 消息队列和 System V 消息队列。POSIX 消息队列使用起来更现代、更方便。

主要操作:
`mq_open()`: 打开或创建消息队列。
`mq_send()`: 向消息队列发送消息。
`mq_receive()`: 从消息队列接收消息。
`mq_close()`: 关闭消息队列。
`mq_unlink()`: 删除消息队列。

例子会比较复杂,这里就不直接上完整代码了,但可以描述一下思路:

一个线程(发送者)调用 `mq_send()` 将一个包含数据的缓冲区发送到指定名称的消息队列。另一个线程(接收者)则调用 `mq_receive()` 从该消息队列读取数据。如果队列中没有消息,`mq_receive()` 会阻塞,直到有消息到来。发送者和接收者之间不需要显式地管理锁,消息队列本身就处理了这些同步问题。

消息队列的优点:
解耦性强:发送者和接收者不需要直接交互,甚至不知道对方的存在。
异步性:发送者发送后可以继续做自己的事情,不用等待接收者。
可靠性:消息队列可以配置为持久化存储,即使进程崩溃,消息也不会丢失。
流量控制:消息队列可以限制队列的最大长度,防止生产者发送过快导致内存耗尽。

总结一下

线程间通信,说白了就是如何安全、有效地让线程共享和交换信息。

共享内存是基础,但要小心竞态条件。
互斥量是保护共享数据最常用的工具,确保同一时间只有一个线程访问。
读写锁在读多写少的场景下比互斥量效率更高。
条件变量是协调线程等待和通知的利器,常用于生产者消费者等场景,并且通常配合互斥量使用。
原子操作能以极低的开销处理简单的、不可分割的操作。
消息队列提供了一种更高级、更解耦的通信方式,适合传递数据包。

选择哪种方式,取决于你的具体需求:需要多高的并发读写性能?需要什么样的通信模式(直接共享 vs. 消息传递)?需要多高的可靠性?理解这些工具的特性,你就能写出更健壮、更高效的多线程程序了。

网友意见

user avatar

你缺的东西还挺多的。


第一,同一个进程内部的线程间不存在通信问题,想怎么访问怎么访问;所以我们反而需要做一些事,从而主动“隔离”不同线程,避免数据脏读脏写。


第二,多线程编程(以及多进程编程)都需要操作系统方面的底子。不懂操作系统,多线程协作是做不好的。


具体到你这个案例上,简单说,不要轮询。

轮询这个动作本身就决定了,你的程序必定CPU占用奇高、发热巨大,同时运行缓慢。


这还是程序逻辑过于简单;稍微复杂一点,你这种写法,最终必然是“CPU占用跑满,程序逻辑寸步不前”,和一个死循环的垃圾没有什么差别。


第一步,先设置一个全局的、标准的锁(mutex)。

注意,第一个线程要修改内存数据,需要先申请锁,确保第二个线程不在读取数据;

第二个线程发现数据可用,也要先申请锁,确保第一个线程不会继续修改它。

也就是类似你过去那个“全局变量”的作用;但一定要使用标准的锁、使用标准的acquire系统调用申请锁数据读写权限。


这是因为,标准的mutex是操作系统提供的;当你的某个线程申请mutex失败时,操作系统会把它置于等待队列,在mutex可用前不会继续给它分配时间片,这就避免了忙等;而一旦mutex可用,这个线程就会被移回就绪队列,之后就可能获得时间片了。

这就避免了大量无效的CPU占用。


第二步,认真分析业务逻辑,画出两个线程的状态切换图,确定锁应该有几个、分别是什么状态(比如是否需要读写锁);确保“线程申请到锁就一定可以执行;线程无法执行就一定要进入挂起状态”。

注意,你并不能确定什么时候第二个线程正在读取数据、或者阻塞在哪里长时间没有读取。所以你必须使用足够多的标志位,确保“数据未初始化、数据初始化中、数据初始化完成等待读取、数据读取中、数据读取完成”等状态可清晰区分。否则,数据就可能丢失(线程一产生数据后,线程二尚未得到调度,线程一又用新数据覆盖了之前的数据)或者出现脏读、脏写。


当然,视业务需要,只有ture/false两个状态的锁也许已经够用了,但你必须认真评估、充分讨论之后再这么做——你的问题描述过于简略,无法确定是否能行。


第三步,重新设计共享数据结构,把“锁定时间”降到最低。

从你的描述中可知,线程1是不能停的,需要“不断的生成计算结果”;但如此一来……

而mutex的默认行为是:申请不到锁,就把申请锁的线程挂起。

于是,线程1生成计算结果时,线程2只能等着;而线程2处理计算结果那5ms,线程1也只能等着……

万一操作系统再安排不了时间片,那线程1可能就得等200ms,线程2才得到执行权;线程2执行时,线程1进了等待队列,等线程2释放锁,线程1才移回就绪队列,又等了200ms才得以执行……也就是出现了一个400ms以上的大卡顿。


这样搞的话,你其实根本就不应该用什么多线程。直接放在同一个线程里,收集50ms的数据,然后执行5ms的处理——简单,又不容易出错,效率高,响应快……


想要借多线程提高吞吐率,那么就必须搞一个更好用的数据结构。

比如,一个链表。

链表的每个节点足够容纳50ms的数据;线程1先申请一个节点,把数据写进去,写50ms后,申请这个全局链表的锁,把数据挂进链表——锁定期间只需执行一条把链表末端next指向新节点的操作(可能还需要维护一下头尾指针,不要每次都顺着链表摸到尾)。

类似的,线程2被调度后,申请锁定链表,然后把链条第一个节点移除、指针记录在本地,随即释放锁;然后就可以不受打扰的处理这个节点携带的数据了。


注意,这时候,如果还用最简单的mutex的话,因为所有关于数据结构(链表)的操作都需要先锁定,再检查有无数据;那么线程2可能就会死循环的不停上锁、检查发现没数据,释放锁,然后马上又上锁……也就是绝大部分执行时间都在加解锁上。


所以,这时候我们就不得不搞一个更复杂的东西,比如,让mutex包含多个值。

当mutex非0时,线程2才可以从链表取出节点、同时把mutex值减一,减到0线程2就必须休眠,不要再去访问链表;而线程1每成功往链表加入一个节点,就把mutex值加一……


但这时候,由于线程1/2的读写可能很频繁,如果锁定之后才读写数据的话,那么锁定时间就会是50ms/5ms,允许另一个线程访问的时间就会特别特别短;这时候另一个线程实际上是拿不到数据的——除非其中一个线程一口气把缓冲区写满、或者把所有缓冲数据处理完然后陷入阻塞,否则另一个线程可能永远得不到执行机会。这就是术语说的“饿死”,是必须避免的。

因此,请把数据准备放在锁定时间之外;锁定后,只处理一下next/head/tail指针,把节点挂入/取下,然后就马上释放锁。想办法把共享数据弄的“在大部分时候可用”,两个线程才能协作起来。

你可以把这个共享数据结构实现成一个通用的、支持多线程访问的队列,只允许通过pop/push接口访问数据;同时把加解锁放到这两个接口里,从而简化使用逻辑,杜绝错误访问。


事实上,你的这个案例可能还可以进一步优化。

比如,如果只有这么两个线程,且线程1是生产者线程2是消费者(单生产者/消费者模型),那么这里甚至可以不用锁,实现一个标准的环形数组即可——这也是经典的、最简化的无锁编程案例。


但如果参与者更多、逻辑更复杂,那么锁就是必需的;甚至读写锁、旗语、event等东西都必须全面利用起来。

这个就太复杂了,这里一时讲不清,还是自己去看操作系统原理的相关章节吧。

user avatar

首先纠正一下题主的描述,同一个进程内部的线程不存在“通信”这个问题的,原因很简单,一个进程内部的线程共享该进程的地址空间,因此这些线程天然可以直接访问彼此的数据,因此根本不需要“通信”一说。

题主描述的问题在多线程语境下有一个专门的描述,这不叫线程通信而是叫做线程同步,你在操作系统课上学的晕头转向的并行流的同步互斥问题说的就是这。

回到题主的问题,有一个线程在计算结果,有一个线程要对结果进行处理,那么显然一个线程是数据的“生产者”,另一个线程是数据的“消费者”,这是不是一个非常经典的生产者消费者问题:

注,上图出自《你管这破玩意叫线程?》一文。

操作系统课上是不是有专门讲解过这个问题,上课没好好听讲的要反思了。

实际上很简单,生产者线程生产的数据可以直接放到队列中,消费者线程从队列中取出数据处理即可,这里的难点在于多线程需要同时读写队列,因此这里出现了两个问题:

1,确保队列的互斥访问

2,队列空时消费者线程不可以读,队列满是生产者线程不可以写

PV操作专门用来解决这个问题,随便翻一本操作系统教材都能找到答案。

user avatar
我的线程学得不太好,不知道线程有没有这样的操作,线程2平时处于休眠状态,当线程1数据生成完毕后,激活线程2,将数据交给线程2去处理,线程2处理完再进入休眠状态,如此循环往复,线程是否有这样的用法呢?

你的思路是对的。如果业务处理比较耗时,更好的模式是用一个线程池。

当计算完成之后,就从线程池里抓出一个线程来,把计算结果交给它处理。

业务处理之后,把空闲的线程又还回到线程池里。这是经典的解决方案。

user avatar

题主你差了很多东西。由于现代CPU的复杂性,你要是裸着共享数据会以很奇怪的姿势崩的。

线程间通信(或者同步)的基本“套路”,主要有这些:

最常用的跨线程设施,一个用来表达互斥的对象。在某一时刻,只有一个线程能处于“获得了锁”的状态,其它线程都会卡在“去获得锁”的这步,直到锁被释放,才会有另一个线程获得。锁通常用来保护一段代码在同一时刻只被一个线程执行。

锁通常还会有一些特性:

  • 能不能“重入”:能重入的锁,同一个线程在锁住的时候再调用“锁”命令,不会把自己卡在那。没有这个特性,同一个线程会卡住自己。能重入的锁会便于编写复杂代码,但是锁本身会变得复杂。
  • 是否涉及调度器:锁住的线程是在死循环还是通知系统调度器把自己切出去。前者实时性好,但是被锁住时性能非常糟糕。后者反之。

条件变量

线程可以“等待”在这里,直到别人通知“关门放狗”,才会放过一个或者所有的等待线程。

跨线程管道

可以一个(或几个)线程往里面塞东西,另一个(或几个)线程往外取东西。可以依此来分发任务给工作线程。

信号量

可以用来表示资源与消费者的数量,比如零为初始状态,整数表示有这么多个可用资源,负数表示有这么多个消费者线程在等待。

原子操作

一些保证了“一定被同一个线程完成,其它线程要么读到操作前状态,要么读到操作后状态”的组合操作,实际上会调用CPU的一些专用同步指令。主要包含这些:

  • 读/写:好像在x86架构里意义不大,因为我记得x86的读写本身天然就是原子的。
  • 自加/自减。
  • compare-and-swap:如果目标等于比较数值,就把它设成新的数值,并且把原来存储的数值返回给你。这个操作拥有最高的consensus value,可以(理论上)区分无穷多个线程状态。

原子操作拥有最高的编程灵活性,实际上大部分跨线程设施都是用原子操作实现的。

这些玩意里面,很多东西都同时会有操作系统的实现(比如锁和条件变量通常都会作为系统API的一部分),也会有库实现,也会有编译器扩展实现(比如原子操作)。

类似的话题

  • 回答
    在 C 语言中,让不同线程之间能够交流信息、协同工作,这本身就是多线程编程中最核心也是最需要仔细处理的部分。别把它想得太玄乎,无非就是大家共享一块内存,然后约定好怎么读写这块内存罢了。只不过,这“约定”怎么立得住,不让大家互相捣乱,才是关键。咱们把线程通信这事儿,拆解成几个层面来说。 1. 共享内存.............
  • 回答
    C 语言的设计理念是简洁、高效、接近硬件,而其对数组的设计也遵循了这一理念。从现代编程语言的角度来看,C 语言的数组确实存在一些“不改进”的地方,但这些“不改进”很大程度上是为了保持其核心特性的兼容性和效率。下面我将详细阐述 C 语言为何不“改进”数组,以及这种设计背后的权衡和原因:1. 数组在 C.............
  • 回答
    C 语言王者归来,原因何在?C 语言,这个在编程界已经沉浮数十载的老将,似乎并没有随着时间的推移而消逝,反而以一种“王者归来”的姿态,在许多领域焕发新生。它的生命力如此顽强,甚至在 Python、Java、Go 等语言层出不穷的今天,依然占据着不可动摇的地位。那么,C 语言究竟为何能实现“王者归来”.............
  • 回答
    C语言指针是否难,以及数学大V认为指针比范畴论还难的说法,是一个非常有趣且值得深入探讨的话题。下面我将尽量详细地阐述我的看法。 C语言指针:理解的“门槛”与“终点”首先,我们需要明确“难”的定义。在编程领域,“难”通常指的是: 学习曲线陡峭: 需要花费大量时间和精力去理解和掌握。 容易出错:.............
  • 回答
    C 语言中的 `void main()` 并非是语言标准规定的写法,它的出现和流传,更像是一个历史遗留问题、编译器兼容性以及开发者习惯共同作用的结果。要详细讲解,我们需要从 C 语言的诞生和演变说起。1. C 语言的起源和早期标准 (K&R C) C 语言的诞生: C 语言最初是由 Dennis.............
  • 回答
    C语言自学能到什么高度?详细解析C语言,作为一门强大且经典的编程语言,其学习曲线相对陡峭,但一旦掌握,其应用范围之广,性能之优越,是许多其他语言难以比拟的。 仅凭自学,C语言可以让你达到一个非常高的技术高度,足以让你在许多领域成为一名优秀的开发者甚至专家。以下将从多个维度详细阐述C语言自学所能达到的.............
  • 回答
    在 C 语言中判断一个数列是否为等差数列,核心思想是验证数列中任意相邻两项的差值是否恒定不变。下面我将从概念、算法实现、注意事项以及代码示例等方面进行详细讲解。 一、什么是等差数列?在数学中,等差数列(Arithmetic Progression 或 Arithmetic Sequence)是指一个.............
  • 回答
    在 C 语言中,不用 `goto` 和多处 `return` 进行错误处理,通常依靠以下几种模式和技术。这些方法旨在提高代码的可读性、可维护性,并遵循更结构化的编程原则。核心思想: 将错误处理的逻辑集中到函数退出前的某个点,或者通过特定的返回值来指示错误。 1. 集中错误处理(Single Exit.............
  • 回答
    这个问题很有意思,也触及到了C语言作为一种基础性语言的根本。很多人听到“C语言本身是用什么写的”时,会先想到“用更高级的语言写的”,比如Python或者Java。但事实并非如此,或者说,这个答案需要更深入的理解。首先,我们需要明确一点:C语言最初的实现,也就是早期的C编译器,并不是用C语言本身写的。.............
  • 回答
    C 语言中,一些自带函数返回的是指向数组的指针,而你无需手动释放这些内存。这背后涉及到 C 语言的内存管理机制以及函数设计哲学。要弄清楚这个问题,我们需要从几个关键点入手: 1. 返回指针的函数,内存的归属至关重要首先,理解函数返回指针时,内存的“所有权”是谁的,是解决这个疑问的核心。当一个函数返回.............
  • 回答
    在 C 语言中,枚举(`enum`)是一种用户定义的数据类型,它允许你为一组整数常量命名。这使得代码更具可读性和可维护性。而枚举中的 `end` 关键字,严格来说,它本身并不是 C 语言标准枚举定义的一部分,而是一种常见的编程约定或模式,用于标记枚举序列的结束。让我来详细解释一下,并尽可能剥离 AI.............
  • 回答
    在C语言中,严格来说,不能直接“判断”一个变量的类型是否是`int`或`float`。C语言是一种静态类型语言,变量的类型在编译时就已经确定,并且不能在运行时随意更改或检查。当你声明一个变量时,你就已经告诉了编译器它的类型。不过,如果你想表达的是“根据当前存储的值,推断出这个变量应该被视为整数还是浮.............
  • 回答
    在C语言中, `a > b ? a < c ? a : b : c` 这种写法是利用了三元运算符 (?:) 的嵌套。它是一种简洁的条件表达式,用来根据条件的真假返回不同的值。理解它的关键在于一步步拆解它的逻辑。咱们就来好好捋一捋这串表达式的判断过程,讲得透彻一些,保证让你明白它到底是怎么回事儿。首先.............
  • 回答
    C 语言里,一旦你用了 ` ` 来进行换行,确实就“回不去了”——至少在标准的输出流中是这样。这背后的原理,要从计算机如何处理文本输出和终端(或者说显示器)的工作方式说起。核心点:文本流与终端的坐标系统想象一下你的程序输出的文本,就像一条源源不断地向前流动的河流。` `(换行符)就是这条河流中的一个.............
  • 回答
    在C语言中,关于“乘以0.01”和“除以100”哪个更快速,这是一个非常值得探讨的话题,尤其是在追求极致性能的底层开发或者对浮点运算效率敏感的场景下。要回答这个问题,我们需要深入理解计算机如何处理这两种操作,以及它们在硬件层面的具体实现。理解基础:乘法和除法在计算机中的运算首先,我们得明白计算机进行.............
  • 回答
    朋友,咱们这话题聊得挺实在的。C语言现在还有没有“必要”学,未来还有没有“用”,这绝对是个值得深入掰扯掰扯的问题。别听那些虚头巴脑的,咱就从实际出发,好好说说。C语言现在还有没有“必要”学?我想说,如果你想在计算机底层或者和效率打交道,那 C 语言的“必要性”依然挺强的,甚至可以说是基石性的。你得明.............
  • 回答
    在 C 语言编程的世界里,选择一个趁手的编辑器就像是给了你一对飞翔的翅膀。这不仅关乎效率,更影响着你的开发体验和创造力。市面上的编辑器琳琅满目,各有千秋,要说哪个“最好用”,这其实是个非常主观的问题,取决于你的个人习惯、项目需求以及你追求的侧重点。不过,如果你想在众多选择中找到最适合你的那位,不妨先.............
  • 回答
    嘿,哥们,聊到 C 语言的“奇技淫巧”,这可就有意思了。这东西,说白了就是利用 C 语言一些不太直观,但又特别巧妙的特性,来达成一些别人想不到或者达不到的效果。很多时候,这些技巧能让你写出更精炼、更高效的代码,当然了,用不好也容易把自己绕进去。我这里给你掰扯几个比较典型的,保证不像是那种写流水账的A.............
  • 回答
    这个问题在 C 语言中,关于表达式 `a = a++;` 的结果,是初学者乃至一些有经验的程序员都可能感到困惑的地方。它的行为并非我们直观想象的那么简单,并且会涉及到 C 语言中一个非常重要的概念:未定义行为(Undefined Behavior)。首先,让我们尝试理解一下 C 语言是如何处理这个表.............
  • 回答
    在 C 语言的世界里,指针是必不可少的工具,它们就像是内存地址的“指示牌”,让我们能够更灵活地操作数据。而当我们将指针与数组、函数结合起来时,就诞生了一系列强大而又容易让人困惑的概念:指针数组、数组指针、函数指针,以及指向函数的指针。别担心,今天我们就来把它们掰开了揉碎了,让你彻底搞懂它们到底是怎么.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有