C is a language, and to get better at any language you need to practice it; meaning you need to program. Programing toy problems might not be a solid motivator because it sort of feels "useless". Creating things that will be used, by you or others, is likely to be a more driving factor. With that in mind I'd suggest you create a public repository and populate it with C utilities/library that you can use when you are writing C code; utilities that will save you time. The utilities/libraries will be ones that you write yourself.
What kind of utilities/libraries should you program? Hard to say not knowing your current ability. Let me try and generalize; I'd say C is a quite verbose language in the sense that to do some basic things you need more code to achieve a specific task. This is typically because the standard library will return any and all info about the system, and then you need to pick out exactly what you need from it.
For example, (this might be outside of your C knowledge but hang in there and ignore the details, just pay attention to the point). For example, if I had a network interface eth0
on my linux system and I wanted to get the IP address of that interface from inside C, there is no straight forward function called get_ip_by_interface_name(if_name)
. The closest thing is the function getifaddrs()
which creates and returns a linked list of structures of every interface and individual IP on the local system (for more info see man 3 getifaddrs
). You then have to traverse the linked list, and compare the name you're looking for to the name of the interface. Then you have you check if the IP address is IPv4, IPv6, etc etc. That's a lot of work every time you need to just get an IP address of an interface. Had you a need for it, I would suggest you go and implement a get_ip_by_interface_name(name)
and have that in your own utility repository. Then when you are working on a project, you can go and use your own code. Here is an advanced example of where someone made crypto hashes and has them around. Don't get overwhelmed with this or even try to copy it, my point is do something like this that reflects what functions would make your life easier in your programming..
So how will I know what type of utilities will help me? That's another question that depends on who is asking it. For me, when I work with C I do a lot of networking, so I have my own utilities that check_interface_exists(if_name)
, get_ip(if_name)
, link_ready(if_name)
. For you, I would suggest working toward implementing thread-safe data structures. They are really handy to have around when programming and programming the data-structures you get good practice with the language. From your post it seems you have the basics down? You might be on a bit of a learning curve with mutexes but hey, you wanted to learn :)
Common thread-safe data structures implemented in C typically include Queues, Stacks, and Hash Tables. Linked-list might be the easier of them, but a queue being more useful. Hash tables and binary trees are more advanced but also useful. You can tackle them in the following order of increasing difficulty.
- Thread-Safe Linked List
- A thread-safe linked list enables multiple threads to perform operations like insertions, deletions, and traversals simultaneously without leading to data corruption. Here is a well written example.
- Thread-Safe Stack (LIFO)
- Similar to a thread-safe queue, a thread-safe stack ensures that multiple threads can push or pop items simultaneously without causing data corruption. The most common way to implement thread safety is also through mutex locks, ensuring only one thread can modify the stack at any one time.
- Thread-Safe Queue (FIFO)
- A thread-safe queue is implemented so that it allows multiple threads to add (enqueue) or remove (dequeue) items simultaneously without leading to race conditions. Thread-safety is often achieved through locking mechanisms like mutexes. A thread would acquire a lock before any operation (enqueue or dequeue) and release the lock after the operation is completed. For blocking queues, condition variables can be used in addition to mutexes to allow threads to wait for a condition (e.g., queue not empty for dequeue operations, queue not full for enqueue operations) to be fulfilled before proceeding.
- Thread-Safe Hash Table
- A thread-safe hash table allows multiple threads to perform operations like insertions, deletions, and lookups without causing data corruption. For such structures, fine-grained locking is often used to improve concurrency. Rather than locking the entire table for every operation, locks are associated with each bucket or entry. Thus, multiple threads can perform operations on different parts of the table concurrently. This can significantly improve performance in a multi-threaded environment.
- Thread-Safe Binary Trees
- Binary trees, like AVL or Red-Black trees, can also be made thread-safe. To allow concurrent access, similar strategies to those used for linked lists or hash tables can be employed. Nodes in the tree can be individually locked and unlocked, which allows for high degrees of concurrency. Special attention needs to be paid when rebalancing the tree, as larger portions of the tree may need to be locked.